一、数据库基本概念
1.1 数据库 - Database
从字面意思看,数据库就是一个存储数据的仓库。从计算机的角度来讲,数据库(Datebase)是按照数据结构来组织、存储和管理数据的仓库。
简单来说,可以将数据库视为电子化的一个文件柜。用户可以对文件中的数据进行新增、更新和删除等操作。
数据库中的一些常见术语:
- 实体与实体集:客观存在并相互区别的事物。可客观理解为类似于面向对象中的对象,但又不完全相同。而实体集就是这些事物的集合。
- 属性:实体所具有的某一特性。可类比于面向对象中对象的属性,即对象的成员变量。
- 联系与联系集:实体与实体之间的关系,而联系集就是这些关系的集合。
- 联系一般分为三种,分别为一对一,一对多和多对多
- 比如学生实体与班级实体,就是一对多,即一个学生只属于一个班级,而一个班级里有多个学生
1.2 数据库管理系统 - DBMS
管理关系型数据库的计算机软件就是关系数据库管理系统。常用的关系数据库管理软件有Oracle、MySQL、SQL Server等。
这里以MySQL的学习为主。
1.3 关系型数据库 - RDBMS
所谓关系型数据库,是建立在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数据库中的数据。现实世界中的各种实体以及实体之间的各种联系均用关系模型来表示。简单说:关系型数据库是由多张能互相联接的二维行列表格组成的数据库。
基于关系型数据库的管理系统,我们称之为关系数据库管理系统(RDBMS)。
RDBMS的特点:
- 数据以表格的形式出现
- 每行为各种记录名称
- 每列为记录名称所对应的数据域
- 许多的行和列组成一张表单
- 若干的表单组成database
RDBMS中的一些常见术语:
-
数据库:类似于上图中学生表一样的很多个表的集合。
-
冗余:存储两倍的数据,冗余降低了性能,但提高的数据的安全性。
-
主键:唯一标识数据表的一列或多列的组合。
-
外键:关联其他表的列。
-
索引:用于提高访问数据库表中特定信息的速度。索引是对数据库表中一列或多列的值进行排序的一种结构。可类比于书籍中的目录,可提高我们查询的效率。
二、E-R 模型
实体 - 联系模型,就是 E-R 模型。它给我们提供了一种不受任何 DBMS 约束的面向用户的表达方法。在数据库设计中被广泛用作数据建模的工具。
E-R 模型的构成成分是实体集、属性和联系集。通过 E-R 模型,我们能得到一个较为清晰的关于整个数据结构的关系图,因为 E-R 模型也称为 E-R 图。
E-R 模型的表示方法:
- 矩形框表示实体,实体名写在矩形框内。
- 椭圆表示实体的属性,属性名写在椭圆内,并用无向边,即直线与其对应的实体连接。
- 菱形表示实体间的联系,联系名写在菱形框内,将有联系的两个实体分别用无向边与菱形相连,并在连线上标明联系类形,即 1 - 1、 1 - N、M - N 。
简单 E-R 图示例:
三、SQL 语句简介
3.1 什么是 SQL
SQL,全称是 Structured Query Language,即结构化查询语言,它是一种特殊的编程语言,专用于对数据库进行操作而设计的,用于对数据库进行增、删、改、查等操作。
3.2 SQL 分类
- 数据定义语言,简称为 DDL(Data Definition Language) ,用来定义数据库对象,如数据库表、列等。关键字包括:create,alter,drop 等。
- 数据操作语言,简称为 DML(Data Manipulation Language) ,用来对数据库表进行操作。关键字包括:insert,delete,update 等。
- 数据控制语言,简称为 DCL(Data Control Language) ,用来定义数据库的访问权限和安全级别以及创建用户。
- 数据查询语言,简称为 DQL(Data Query Language) ,用来查询数据库表的记录。关键字包括:select,from,where等。
3.3 SQL 的通用语法
- SQL 语句通常以分号结尾,它可以单行或多行进行书写
- 可以使用 /**/、—、#的方式进行注释
四、MySQL
MySQL是当前最流行的关系型数据库管理系统之一。
4.1 mac 下 MySQL 的安装
mac下安装软件一般有两种方式,一是在官网上下载安装包,像安装普通软件一样进行安装;另一种则是使用 mac 上的包管理器 homebrew 来进行安装。
两种安装方式可参考:
https://www.jianshu.com/p/fd3aae701db9
安装完后输入:mysql -u root -p,回车后输入密码即可登录 MySQL 命令行界面。
4.2 MySQL 常用数据类型
MySQL 中常见数据类型如下表所示:
类型 | 大小 | 范围(有符号) | 范围(无符号) | 用途 |
---|---|---|---|---|
TINYINT | 1字节 | (-128, 127) | (0, 255) | 小整数值 |
INT | 4字节 | (-2^31, 2^31-1) | (0, 2^32-1) | 较大整数值 |
BIGINT | 8字节 | (-2^65, 2^65-1) | (0, 2^64-1) | 大整数值 |
FLOAT | 4字节 | 浮点数 | ||
DOUBLE | 8字节 | 浮点数 | ||
DATE | 3字节 | YYYY-MM-DD | ||
TIME | 3字节 | HH:MM:SS | ||
VARCHAR | 0-65535字节 | 可变长度字符串 | ||
ENUM | 枚举类型 |
详细可参考:
http://www.runoob.com/mysql/mysql-data-types.html
4.3 简单的 SQL 语句
MySQL中的SQL语句是不区分大小写的,推荐 SQL 关键字使用大写。
这里以在一个 test 数据库中创建一个学生表为例。学生有学号、姓名、性别、年龄四个属性。
- 创建数据库
-- 语法:CREATE DATABSE database_name;
CREATE DATABASE test;
- 查看已有的数据库
SHOW DATABASE;
- 选择一个数据库或者切换数据库
-- 语法:USE database_name;
USE test;
- 查看当前选择的数据库
-- 第一种方式
SELECT DATABASE();
-- 第二种方式,在命令行模式下输入
status;
-- 会显示当前 MySQL 的一些基本情况,包括连接信息等
-- 其中有一项是 Current database 表示当前数据库
- 创建数据表
-- 语法:CREATE TABLE table_name (col_name1 col_type1, ... col_namen col_typen )
-- ENGINE(引擎)=InnoDB DEFAULT CHARSET=utf8 可省略
-- 在创建数据表时,最好指定表的编码
CREATE TABLE student (
-- INT UNSIGNED 表示无符号整数,即只有正整数和0
-- AUTO_INCREMENT 表示自动增长,即我们在插入数据时不用指定这个属性的值,指定了也没用
-- PRIMARY KEY 指定主键
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20),
gender ENUM('男', '女', '保密'),
age INT
);
INSERT INTO test(id, k, v) VALUES(1, 'a', '0'), (1, 'b', 'x'), (1, 'c', '9'), (1, 'ab', 'c'), (1, 'set', 'aweg');
- 查看创建的数据表
-- 语法:SHOW CREATE TABLE table_name;
-- 会显示表的创建语句
SHOW CREATE TABLE student;
-- 语法:DESC table_name;
-- 会以表格的形式展示表中的字段及字段的类型等信息
DESC student;
- 修改数据表结构
-- ALTER TABLE tabel_name operation[ADD, MODIFY, CHANGE, DROP, CHARACTER SET]
-- 给学生表添加列 班级号 - ADD
-- 语法:ALTER TABLE table_name ADD new_col_name type;
ALTER TABLE student ADD class_id int;
-- 修改学生表中学生名字字段的类型,并添加约束不能为空 - MODIFY
-- 语法:ALTER TABLE table_name MODIFY col_name new type or new retriction;
ALTER TABLE student MODIFY name VARCHAR(50) NOT NULL;
-- 修改学生表中的表名,将 class_id 改为 cls_id - CHANGE
-- 语法:ALTER TABLE table_name change old_col_name new_col_name new_col_type;
ALTER TABLE student CHANGE class_id cls_id INT UNSIGNED;
-- 删除学生中的班级号 cls_id 列
-- 语法:ALTER TABLE table_name DROP col_name;
ALTER TABLE student DROP cls_id;
-- 修改表的字符集
-- 语法:ALTER TABLE table_name CHARACTER SET new_character;
ALTER TABLE student CHARACTER SET gbk;
- 插入数据
-- 语法:INSERT INTO table_name (field1, field2,...fieldn) VALUES (v1, v2,...vn), (vv1, vv2,...,vvn);
INSERT INTO student(name, gender, age) VALUES('Demon', '男', 18), ('Semon', '女', 18)
,('haha', '男', 16), ('hehe', '男', 17), ('aaa', '女', 26);
- 查询数据
-- SELECT v1, v2... FROM table_name WHERE conditions;
-- * 表示查询所有字段,一般建议写出查询的字段名称,这样更加容易阅读
SELECT * FROM student;
-- 查询性别为男的学生
SELECT * FROM student WHERE gender = '男';
-- 查询语句是 SQL 语句中最复杂的,我们一般在编写 SQL 语句时都是写的查询语句
- LIKE 查询
-- 使用 LIKE 关键字可以对字符串进行模糊查询
-- LIKE '_XXX'; 其中下划线 _ 占位,表示任意一个字符
-- 查询学生表中以任意一个字符开头,但是以 'emon' 结尾的学生信息
SELECT * FROM student WHERE name LIKE '_semon';
-- LIKE '%XXX'; 其中 % 表示任意一串字符,% 是与 LIKE 关键字最常用的组合
-- 查询学生表中姓名包含 'mo' 的学生信息
SELECT * FROM student WHERE name LIKE '%mo%';
- 条件查询
-- 大于(>),小于(<),大于等于(>=),不等于(!=)或(<>)
-- 使用 AND 表示与
-- 查询学生表中性别为男并且年龄大于18的学生信息
SELECT * FROM student WHERE gender = '男' AND age > 18;
-- 使用 OR 表示或
-- 查询学生表中性别为女或者年龄不等于17的学生信息
SELECT * FROM student WHERE gender = '女' OR age <> 17;
SELECT * FROM student WHERE gender = '女' OR age != 17;
- 修改数据
-- UPDATE table_name SET f1 = v1, f2 = v2... WHERE conditions;
-- 将性别为男的学生的年龄修改为20
UPDATE student SET age = 20 WHERE gender = '男';
- 数据排序
-- 语法:SELECT f1,f2.. FROM table_name WHERE condifitons ORDER BY f1,f2.. [ASC [DESC]]
-- ASC 表示升序,DESC 表示降序,默认是 ASC,可省略。
-- 将学生数据信息按照年龄升序排序
SELECT * FROM student ORDER BY age [ASC];
-- 降序
SELECT * FROM student ORDER BY age DESC;
- 删除数据行
-- 语法:DELETE FROM table_name WHERE conditions;
-- 删除性别为男的学生
DELETE FROM student WHERE gender = '男';
- 修改数据库表名
RENAME TABLE old_table_name TO new_table_name;
- 删除数据表
DROP TABLE table_name;
- 删除数据库
-- 在已经登录 MySQL 后:
DROP DATABASE database_name;
-- 如果没登录前:
mysqladmin -u root -p drop database_name;
-- 回车后输入密码
4.4 聚合函数
使用聚合函数的基础就是使用分组语句,使用语法如下:
-- 语法 SELECT f1,f2.. , function(fn) FROM ... WHERE ... GROUP BY f1,f2...
-- 注意 SELECT 后的属性要和 GROUP BY 中的一致,只能比后面的少,不能超过,否则会报错
-- 查询学生信息中有哪些年龄段的学生
SELECT age FROM student GROUP BY age;
常见的聚合函数如下几种:
- AVG:求平均值
-- 在 GROUP BY 语句中,SELECT 后可以接受 function(fn)
-- 这里的 function 就是聚合函数
-- function 后接受的属性可以是任意属性
-- 求不同性别学生年龄的平均值
SELECT gender, AVG(age) FROM student GROUP BY gender;
- COUNT:求总个数
-- 求不同性别学生的总人数
SELECT gender, count(*) FROM student GROUP BY gender;
- MIN / MAX:求最小或最大值
-- 求不同性别下最大的年龄
SELECT MAX(age),gender FROM student GROUP BY gender;
- SUM:求和
-- 求不同性别下的年龄总和
SELECT SUM(age), gender FROM student GROUP BY gender;
4.5 表关联查询
准备数据,上面已经建立了一个学生表,学生应该隶属于一个班级,我们再建立一个班级表 class ,班级表的字段有班级编号、班级名称。
-- 创建班级数据库表
CREATE TABLE class (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(20));
-- 插入数据
INSERT INTO class(name) VALUES ('班级一'), ('班级三'), ('班级二'), ('班级四');
-- 设置外键
ALTER TABLE student ADD CONSTRAINT FK_ID FOREIGN KEY(class_id) REFERENCES class(id);
-- 清空学生表的数据,并重新插入测试数据
DELETE FROM student;
INSERT INTO student(name, gender, age, class_id) VALUES('Demon', '男', 18, 1), ('Semon', '女', 18, 1)
,('haha', '男', 16, 2), ('hehe', '男', 17, 3), ('aaa', '女', 26, 3);
INSERT INTO student(name, gender, age) VALUES('eee', '男', 18);
- 内联 - INNER JOIN
-- 内联的意思是 A 与 B 关联,A 与 B 都有的数据
-- 语法:SELECT ... FROM A INNER JOIN B ON conditions WHERE conditions;
-- INNER 可以省略
-- 查询学生分别在哪些班级
-- AS 表示别名
SELECT s.id AS '学号', s.name AS '学生姓名', c.name AS '班级名称' FROM student AS s JOIN class AS c ON s.class_id = c.id;
- 左关联与右关联 - LEFT JOIN or REIGHT JOIN
-- 左关联的意思是 A 与 B 关联,则以 A 中的数据为主,A的数据会显示完整
-- 右关联的意思是 A 与 B 关联,则以 B 中的数据为主,B的数据会显示完整
-- 查询班级的分配情况,这样无论班级有没有安排学生,都需要显示
SELECT c.id AS '班级编号', c.name AS '班级名称', s.name AS '学生姓名' FROM class AS c LEFT JOIN student AS s ON c.id = s.class_id;
SELECT c.id AS '班级编号', c.name AS '班级名称', s.name AS '学生姓名' FROM student AS s RIGHT JOIN class AS c ON c.id = s.class_id;
4.6 子查询
子查询的概念就是查询嵌套,简单说就是 SELECT 后或者 WHERE 条件后依然嵌套有 SELECT 语句。其实也很好理解,因为 FROM 关键字后面接受的是一个表,而一个表其实就是一个数据集,而我们使用 SELECT 语句查询得到就是一个数据集。所以我们只需要简单地将子查询里的 SELECT 语句单独看成一个数据表就能简化整个查询语句了。
-- 一个简单的例子
-- 查询年龄最大的那个学生的姓名
/*
因为使用 MAX() 函数之后,不能再查询其他的字段,和分组的情况一样
所以我们需要分两步来实现:
1. 先查询出最大的学生年龄
2. 然后在查询条件里加上年龄等于查询出的最大年龄
*/
-- 查询出最大的学生年龄
SELECT MAX(age) FROM student;
-- 查询年龄等于最大年龄的学生姓名
SELECT name FROM student WHERE age = (SELECT MAX(age) FROM student);
4.7 变量相关
- 临时变量
-- 定义在函数体或存储过程中的变量
-- 用法在讲函数时会提到
- 用户变量,也称会话变量
-- 用户变量只对当前连接用户有效,其他连接用户无法访问
-- 使用 @ 标识符声明用户变量
SET @age = 20; -- 定义一个值为 20 的 age 变量
-- 使用 SELECT 来查看用户
SELECT @age;
-- 使用 := 来在查询语句中进行赋值
SELECT @maxage := MAX(age) FROM student;
-- 注意事项:
-- 不区分大小写
-- 同一个账户,一旦退出,用户变量也不会再存在
-- 如果用户变量不存在,使用 SELECT 查看会返回 NULL
- 系统变量
-- 任何一个用户都可以访问
-- 使用 @@ 来作为标识符
-- 查看所有的系统变量
SHOW VARIABLES;
SHOW VARIABLES\G; -- 可以使用显示不用过长
-- 同样使用 SELECT 语句来进行查询
SELECT @@age;
-- 修改系统变量
-- 临时修改,只有当前用户使用这个值
SET variable_name = new_value; -- 一需要 @@
SET wait_timeout = 20;
-- 永久修改
SET GLOBAL variable_name = new_value;
SET GLOBAL wait_timeout = 20;
-- 不能自己定义一个新的系统变量
-- 不区分大小写
4.8 账户管理
-
账户管理的应用场景
- 在实际项目开发中,不可能让每个开发人员都使用 root 账户进行登录
- 根据开发人员角色的不同,会分配具有不同权限的账户
-
MySQL 账户体系
- 服务实例级账户:启动了一个 mysqld,即为启动一个数据库实例。如果某用户拥有服务实例分配的权限,那么该账号就可以删除所有的数据库,连同这些库中的表
- 数据库级别账户:对特定数据库执行增删改查 (CRUD) 的所有操作,最常用的一种级别
- 数据表级别账户:对特定表执行 CRUD 的所有操作
- 字段级别账户:对某些表的特定字段进行操作
- 存储程序级别账户:对存储程序进行 CRUD 的操作
-
查看账户
-- 需要有 服务实例级 的权限登录后进行操作
-- 账户都存储在 mysql.user 表中
DESC mysql.user; -- 查看 mysql.user 表的结构
-- 关注字段:host, user, authentication_string(即 password)
SELECT host, user, authentication_string FROM mysql.user;
-- 这里看到的密码是加密后的
- 创建账户
-- 需要使用实例级别账户登录后操作 []表示可省略
CREATE USER user_name [IDENTIFIED BY 'password'];
-- 需要注意的是,这里有 user_name 是区分大小写的,并且不能重复创建
-- 示例:
CREATE USER demon; -- 无密码,输入 mysql -u demon 即可登录
CREATE USER demon IDENTIFIED BY 'demon';
- 授权权限
-- 需要使用实例级别账户登录后操作
-- 常用权限主要包括:CREATE、ALTER、INSERT、UPDATE、DELETE、SELECT
-- 如果需要分配所有权限,使用 ALL PRIVILEGES
-- 创建账户并授权
GRANT privilege1,p2.... ON database_name to 'username'@'hostname' [IDENTIFIED 'password'];
-- 对已经存在用户进行授权
GRANT privilege1,p2.... ON database_name to username1, username2,... WITH GRANT OPTION;
-- 注意这个语句只能增加权限,不能修改权限
-- 示例
-- 创建一个新账户 semon 并对 test 数据库授予所有权限
GRANT ALL PRIVILEGES ON test.* TO 'semon'@'%' IDENTIFIED BY 'semon';
-- 注意这里数据库名后需要加 .*,表示对数据库中的所有都授予权限,包括表名,函数等
-- % 表示任意主机,一般不写死
-- 增加 demon 账户的 CREATE 权限
GRANT CREATE ON test.* TO demon WITH GRANT OPTION;
- 查看账户权限
SHOW GRANTS FOR username;
- 刷新权限设置
-- 使用这个命令使权限生效
-- 尤其是你对那些权限表user、db、host等做了update或者delete更新的时候。
-- 如果遇到使用grant后权限没有更新的情况,只要对权限做了更改就使用FLUSH PRIVILEGES命令来刷新权限。
FLUSH PRIVILEGES;
- 回收权限
-- 需要使用实例级别账户登录后操作
-- 使用 revoke 可以将用户的权限进行撤销
REVOKE privilege1, p2 ... ON database_name FROM 'usernmae'@'hostname';
-- 示例
-- 回收 semon 账户的 CREATE 权限
REVOKE CREATE ON test.* FROM 'semon'@'%';
- 删除账户
-- 语法1:使用 root 登录
DROP USER 'username'@'hostname';
-- 语法2:使用 root 登录,操作 mysql.user 表
DELETE FROM mysql.user WHERE user = 'username';
-- 修改后需要刷新
FLUSH PRIVILEGES;
- 修改密码
-- 不需要登录
-- 一般用于自己修改自己账户的密码
mysqladmin -u username -p password 'new-password';
-- 使用实例级别账户登录,如root
-- 一般用于修改自己或别人的密码,一般是级别高的人管理其他人的密码
UPDATE mysql.user SET authentication_string = PASSWORD('new-password') WHERE user = 'username';
-- 修改完后需要刷新权限
FLUSH PRIVILEGES;
4.9 函数
- 函数的创建
CREATE FUNCTION function_name(paramlist) RETURNS return_type function_body
/*
1. 其中,参数列表的格式:
参数名 参数类型, 参数名 参数类型...
2. function_body
用 begin... end 包裹
4. 临时变量的声明
所谓临时变量,就是 begin..end 间的变量
delare 变量名 变量类型 [default 默认值,可选]
4. 临时变量赋值
set 变量名 = 表达式
5. 函数体中的语句用英文的 ; 隔开
但是由于 MySQL 中标志一个 SQL 语句的结束用的就是 ;
所以,在执行函数时,遇到第一个带 ; 的语句,就结束了
因此,如果想函数正常执行,需要临时修改 SQL 语句的结束标志符
DELIMITER 要修改的新的标志符
*/
-- 一个完整的函数创建的语法
DELIMITER // -- 将 SQL 语句结束符改为 //
CREATE FUNCTION function_name(p1 type1, p2 type2...) RETURNS type
BEGIN
statement1; -- 语句之间用 ; 隔开
...
RETURN result; -- 函数是一定有返回值的
END
// -- 整体函数的结束
DELIMITER ; -- 改回为 ;
- 查看函数是否创建成功
-- 函数创建之后,会存在 mysql.proc 表
-- mysql 是 database
SELECT name, type, db FROM mysql.proc [WHERE name = 'function_name'];
- 调用函数
SELECT 函数名();
- 删除函数
-- 由上面知道,函数是在 mysql.proc 表中
-- 所以如果想删除函数,也只需要在该表中进行操作即可
DELETE FROM mysql.proc WHERE name = 'function_name';
- 简单示例
-- 定义一个实现加法的函数
DELIMITER //;
-- 注意方法名不要与 mysql.proc 中内置的一些方法名重复,会报错
CREATE FUNCTION my_add(a INT, b INT) RETURNS INT
BEGIN
DECLARE res INT;
SET res = a + b;
RETURN res;
END
//
DELIMITER ;
-- 输出 0~100 中的偶数
-- 用这个例子来介绍 while 循环语句与 if 语句
DELIMITER //
CREATE FUNCTION my_printodd() RETURNS VARCHAR(500)
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE res VARCHAR(500) DEFAULT '';
WHILE i < 100 DO
IF i % 2 = 0 THEN -- 注意这里判断相等,只有一个 = 号
SET res = CONCAT(res, ' ', i); -- CONCAT 是内置函数
END IF;
END WHILE;
RETURN res;
END
//
DELIMITER ;
- 使用 SELECT … INTO 定义一个函数
-- 函数中不能调用 SQL 语句,但是 SELECT INTO 除外
-- 示例
DELIMITER //
CREATE FUNCTION my_f() RETURNS INT
BEGIN
DECLARE res INT DEFAULT 0;
SELECT COUNT(*) FROM student INTO res;
RETURN res;
END
//
DELIMITER ;
4.10 存储过程
- 什么是存储过程以及为什么需要存储过程
/*
1. 什么是存储过程?
存储过程是存储在数据库服务器中的一组 SQL 语句。
我们可以通过在查询中调用一个特定的名称来执行这些 SQL 语句。
2. 为什么需要存储过程?
存储过程可以简单理解为“数据库中的程序”。
它可以在不借助外部程序的情况下,让数据库自己解决一个复杂的问题,比如批量处理 SQL 语句等。
*/
- 存储过程的创建
/*
存储过程的特点:
1. 存储过程与函数很相似,但比函数更加灵活
2. 它没有返回值,只注重过程
4. 它比函数更加灵活,可以在里面使用 SQL 语句
*/
-- 通用结构:
DELIMITER //
CREATE PROCEDURE procedure_name(params)
BEGIN
BODY
END
//
DELIMITER ;
- 参数定义
/*
存储过程比函数略复杂的地方,体现在存储过程中接受的参数定义类型要比函数多。
函数中,函数的参数定义是(p1, p1_type...)
而存储过程中,参数分为三种类型:
1. IN :表示传入类型的参数,如果不写默认就是 IN
2. OUT :表示传出类型的参数
4. INOUT :既能传入又能传出。一般不用,会造成语义不明确
这里其实也很好理解,因为存储过程没有 RETURN 语句,
OUT 类型的参数其实就相当于 RETURN 的作用
*/
- 临时变量定义 (BEGIN…END之间的变量)
-- 和函数中临时变量的定义相同
- 示例
-- 实现传入一个学生的 id,删除对应 id 的学生,然后返回删除后学生表的学生总人数
/*
实现分析:
1. 首先考虑用函数实现,但是删除学生,需要用到 delete 语句,所以函数不行
2. 考虑用存储过程实现,但是存储过程没有返回值
4. 可以考虑用到 out 类型的参数来实现返回数值的作用
综上,这个需求可以使用存储过程来实现
*/
-- 实现代码
DELIMITER //
CREATE PROCEDURE proc_delStuByIDAndGetCount(sid INT, OUT scount INT)
BEGIN
DELETE FROM student WHERE id = sid;
SELECT COUNT(*) FROM student INTO scount;
END
//
DELIMITER ;
- 存储过程的调用
-- 无参存储过程的调用
CALL procedure_name;
-- 如果有参数,特别是有 out 类型的参数时
-- 使用用户变量进行接受即可
-- 比如调用上述示例中的存储过程:proc_delStuByIDAndGetCount
SET @scount = 0; -- 定义一个用户变量,也可以不用定义直接传参
CALL proc_delStuByIDAndGetCount(9, @scount);
SELECT @scount;
4. 10 视图
- 什么是视图
/*
简单来说,视图就是对 SELECT 语句的封装。
对于复杂的查询,如果在多处使用,要想更改,就很麻烦。
这时候视图就能解决这一问题。
视图可以视为存储在数据库中一个张虚拟的表。
*/
- 视图的创建
CREATE VIEW view_name AS SELECT...
-- 创建视图,查询学生所在班级信息
CREATE VIEW v_stu_cls AS SELECT s.id AS '学号', s.name AS '姓名', c.name AS '班级名称'
FROM student AS s
JOIN class AS c ON s.class_id = c.id;
- 视图的查看
-- 查看创建的视图
SHOW TABLES; -- 由此也可以看出,视图就是一张表
-- 调用视图
SELECT * FROM v_stu_cls;
SELECT 学号 FROM v_stu_cls;
SELECT 学号 FROM v_stu_cls WHERE 学号 = 10;
- 视图的删除
DROP VIEW view_name;
- 函数、存储过程、视图的比较
- 函数是对一个方法的封装,在 MySQL 中可分为自定义函数与内置函数
- 函数中的参数只是一种类型,就是输入类型,函数必须有返回值
- 存储过程是数据库用于处理复杂 SQL 的一段程序,它可以实现一般 SQL 实现不了的程序
- 存储过程可以简单认为是 SQL 与程序代码的结合体
- 存储过程中的参数类型有三种,OUT 类型可类比于函数中的返回值,我们一般用一个用户变量来接受它
- 视图是对一个复杂 SELECT 语句的封装
- 视图可以简单的认为是一张表,一张由 SELECT 查询结果而组成的一张数据表
4.11 事务
- 为什么要有事务
- 简单的转账示例,转账操作至少涉及两方,A方扣钱,B方得钱。如果扣钱方成功,而得钱方失败。这样会导致重复扣钱的问题
- 事务就是为了解决这一问题而出现的,事务将整个类似 "转账" 的操作看成是一个操作集合,对整体集合进行操作规定
- 事务的特点 (ACID)
- 原子性 (Atomicity) :事务中的全部操作,要么全部完成 ,要么全部不做
- 一致性 (Consistency) :几个并行执行的事务,其执行结果必须与按某一顺序串行执行结果一致
- **隔离性 (Isolation) **:一个事务不受另一事务的影响,拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
- 持久性 (Durability) :对于已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障
- 名词解释
- Commit :事务提交,表示整个事务下的操作集全部有效
- Rollback :事务回滚,表示整个事务下的操作集全部作废,数据将还原到操作前的状态
- 事务不隔离,在并发访问时带来的问题
- 脏读:一个事务读到了另一个事务未提交的数据。比如事务一将 A 账户的钱由100改为 500,而此时一个并发的事务二读取 A 账户的钱,这时事务二读到的数据是 500 。而事务一这时又将事务回滚,导致 A 账户钱变加 100。此时若事务二再次读取,会发现数据是 100。这样,之前读取到的 500 就是脏数据。
- 不可重复读:一个事务读取到另一个事务已经提交的数据,并且这个数据是在 UPDATE 的操作下被修改的。比如上述例子,事务二先读取 A 账户的钱,发现是 100,接着事务一将钱数进行 **UPDATE ** 成 500 并提交整个事务。此时事务二再读取时,会发现得到的结果是 500。这就出现了一个事务多次查询同一个属性却得到了不同的结果值。
- 虚读/幻读:一个事务读取到另一个事务已经提交的数据,并且这个数据是在 INSERT 的操作下被修改的。比如,事务一先读取 A 账户的钱,得到 100 ,同时将 A 账户的名称改为 B。此时事务二新增一个账户 A ,并将 A 账户的钱设置为 100 。这时当事务一再次查询时,会发现 A 账户还没有修改,产生了 '幻觉'。
- 三者的区别:总体来说,可以分为两大类问题:读未提交和读已提交。脏读是读未提交事务数据;后两者则是读已提交数据。而后两者的区别在于已提交数据是如何操作的,不可重复读是执行 UPDATE 操作,而后者是执行 INSERT 操作。
- 事务的隔离级别
- 读未提交 (read uncommitted) :一个事务读到另一个事务没有提交的数据。上述三个问题都存在
- 读已提交 (read committed) :一个事务在写时禁止其他事务读写,表示必须提交后的数据才能被读取
- 未解决:不可重复读、虚读/幻读
- 解决:脏读
- 可重复读 (repeatable read):一个事务在写时禁止其他事务读写,一个事务在读时,禁止其他事务写
- 未解决:虚读/幻读
- 解决:脏读,不可重复读
- 串行化 (serializable) :每次只能执行一个事务,上述问题全部解决。但是这种级别效率低,一般不用
- 常见的数据库事务隔离:
数据库 | 默认级别 |
---|---|
MySQL | 可重复读(Repeatable Read) |
Oracle | 读提交(Read Committed) |
SQLServer | 读提交(Read Committed) |
DB2 | 读提交(Read Committed) |
PostgreSQL | 读提交(Read Committed) |
- 事务命令
-- 在 MySQL 中,表的引擎类型必须是 innodb 类型才能使用事务, innodb 是 MySQL 默认引擎
-- 修改数据的命令会触发事务,包括 INSERT、UPDATE、DELETE
-- 开启事务:开启事务后,变更会维护到本地缓存中,而不维护到物理表中
BEGIN;
-- 提交事务
COMMIT;
-- 回滚事务
ROLLBACK;
-- 在 MySQL 中我们执行 INSERT、UPDATE、DELETE 语句后,默认是自动提交事务的
-- 这个自动提交的设置是系统变量 AUTOCOMMIT 来控制的,默认值是 1
-- 如果想要关闭可修改它的值
SET [GLOBAL] AUTOCOMMIT = 0;
-- 修改后,如果想要当前窗口(表示当前事务)做出的修改,其他窗口(表示其他事务)能看到,必须手动提交
4.12 索引
- 问题引入:
-- 创建一个表 demo
CREATE TABLE demo(
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT NOT NULL,
name VARCHAR(50)
);
-- 使用存储过程来创建大量数据
DELIMITER //
CREATE PROCEDURE proc_create()
BEGIN
DECLARE i INT default 0;
WHILE i <= 100000 DO
INSERT INTO demo(name) VALUES (CONCAT('demo', i));
SET i = i + 1;
END WHILE;
END
//
DELIMITER ;
-- 调用存储过程执行插入操作
CALL proc_create();
-- 开启 SQL 运行时间监测
SET PROFILING = 1;
-- 查询 name 值为 demo10000 的数据
SELECT * FROM demo WHERE name = 'demo10000';
-- 查询执行时间
SHOW PROFILES; -- 0.02965800
-- 查询 id 值 为 10000 的数据
SELECT * FROM demo WHERE id = '10000';
-- 查询执行时间
SHOW PROFILES; -- 0.00109200
-- 执行上面语句,会发现,通过 id 查询比通过 name 查询时间要少很多。
-- 这是因为 id 是主键,有索引关联
-- 因此,如果当某个字段,经常用作查询的字段,而数据量又很大时,通常我们需要使用索引
- 索引的创建
-- 1. 如果指定字段是字符串,需要指定长度,建议长度与定义字段时的长度一致
-- 2. 如果不是字符串,可以不填写长度部分
-- 4. 语法格式如下:
-- create index 索引名称 on 表名(字符名称(长度))
-- 4. 索引的缺点:
-- 4.1 虽然它大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE,因为更新表时,MySQL不仅需要保存数据,还要保存索引文件
-- 4.2 建立索引会占用磁盘空间的索引文件
CREATE INDEX name_index ON demo(name(50));
-- 查询 name 值为 demo10000 的数据
SELECT * FROM demo WHERE name = 'demo10000';
-- 查询执行时间
SHOW PROFILES; -- 0.00082700
- 查看索引
SHOW INDEX FROM table_name;
- 删除索引
DROP INDEX index_name ON table_name;
4.13 数据库的备份与恢复
- 备份
mysqldump -uroot -p database_name > url/xxx.sql;
- 恢复
mysql -uroot -p new_database_name < url/xxx.sql;
4.14 执行外部的 sql 文件
-- 登录 MySQL 环境
mysql -u root -p
-- source url/xxx.sql
4.15 Python 与 MySQL 交互
- Python 中操作 MySQL 步骤图解
- 安装 MySQL 模块
# 以 mac + python3 环境为例
pip install pymysql
- 代码示例
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author : Demon
# date : 2018-02-27 14:26
from pymysql import *
def insert():
# 1. get the connection object
# host, user, password, database, port, charset
conn = NULL
cur = NULL
try:
conn = connect(host="localhost",
user="root",
# use your own password
password="123456",
# use your own database name
database="test",
port=3306,
# notice that it's 'utf8' not 'utf-8'
# if use 'utf-8' will raise error 'NoneType has no attribute encoding'
charset="utf8"
)
# 2. get the db operation object
cur = conn.cursor()
# 4. write SQL and execute the SQL
# insert_sql = "INSERT INTO demo(name) VALUES ('demo10000000')"
# cur.execute(insert_sql)
# update_sql = "UPDATE demo SET name = 'demo' WHERE id = 1"
# cur.execute(update_sql)
delete_sql = "DELETE FROM demo WHERE id = 1"
cur.execute(delete_sql)
# fetchone
# res = cur.fetchone()
# print(res)
# 4. if data has been changed in Python, we need commit by ourself
conn.commit()
except Exception as e:
print(e)
finally:
if cur:
cur.close()
if conn:
conn.close()
def select():
# 1. get the connection object
# host, user, password, database, port, charset
conn = NULL
cur = NULL
try:
conn = connect(host="localhost",
user="root",
# use your own password
password="123456",
# use your own database name
database="test",
port=3306,
# notice that it's 'utf8' not 'utf-8'
# if use 'utf-8' will raise error 'NoneType has no attribute encoding'
charset="utf8"
)
# 2. get the db operation object
cur = conn.cursor()
# 4. write SQL and execute the SQL
params = [2]
select_sql = "SELECT * FROM demo WHERE id = %s"
cur.execute(select_sql, params)
# 4. get the result by cur.fetchone() and cur.fetchall()
res = cur.fetchone() # the result type is tuple
print(res)
# fetchall() return nested tuple ((), (), ())
# SELECT need not commit
# conn.commit()
except Exception as e:
print(e)
finally:
if cur:
cur.close()
if conn:
conn.close()
if __name__ == "__main__":
# insert()
select()
4.16 数据库中常见的安全问题
- SQL 的注入
-- 一个简单的 SQL 插入语句
INSERT INTO student(name) VALUES ('Demon';DROP DATABASE test;');
-- 这里的用户名实际上是 Demon';DROP DATABASE test;
-- 但是我们都知道,SQL 的结束符是 ;
-- 当遇到第一个 ; 号时,第一个语句执行结束
-- 这样就会执行到 DROP 语句,那整个数据库就会被删除掉
-- 实际这里出现问题的是因为在 name 值中,多了一个引号,只要我们加上转义字符,即可达到本身的目的
INSERT INTO student(name) VALUES ('Demon\';DROP DATABASE test;');
- 撞库
- 根据在某个站点获得的用户名和密码,去登录其他站点,从而得到更多信息
- 因为通常我们的习惯是在不同网站注册,大多会使用相同的密码
- 如果某个网站设计得不好,被人窃取密码,再用相同的密码去登录其他网站,就会很危险
- 安全建议
- 对于 SQL 注入而言,容易发生在表单提交中,因此永远不要相信用户的输入,要对用户的输入先进行正则匹配,如果有特殊字符,要进行转义
- 对于撞库,最好不同网站使用不同的密码,特别是对重要的网站。同时要定期更换自己的密码