封面来源:本文封面来源于 MySQL 官网,如有侵权,请联系删除。

本文参考:MySQL数据库教程天花板,mysql安装到mysql高级,强!硬!

1. 变量

在 MySQL 数据库的存储过程和存储函数中,可以使用变量来存储查询或计算的中间结果数据,或者输出最终的结果数据。

在 MySQL 数据库中,变量分为系统变量以及用户自定义变量。

1.1 系统变量分类

变量由系统定义,不是用户定义,属于服务器层面。启动 MySQL 服务,生成 MySQL 服务实例期间,MySQL 将为 MySQL 服务器内存中的系统变量赋值,这些系统变量定义了当前 MySQL 服务实例的属性、特征。这些系统变量的值要么是编译 MySQL 时参数的默认值,要么是配置文件(例如 my.ini 等)中的参数值。可以通过网址 Server System Variables 查看 MySQL 中系统变量的相关信息。

系统变量分为全局系统变量(需要添加 GLOBAL 关键字)以及会话系统变量(需要添加 SESSION 关键字),有时也把全局系统变量简称为全局变量,有时也把会话系统变量称为 local 变量。如果不写,默认会话级别。

静态变量(在 MySQL 服务实例运行期间它们的值不能使用 SET 进行动态修改)属于特殊的全局系统变量。

每一个 MySQL 客户机成功连接 MySQL 服务器后,都会产生与之对应的会话。会话期间,MySQL 服务实例会在 MySQL 服务器内存中生成与该会话对应的会话系统变量,这些会话系统变量的初始值是全局系统变量值的复制。如下图:

MySQL服务实例生成的会话系统变量

全局系统变量与会话系统变量的区别

全局系统变量针对于所有会话(连接)有效,但不能跨重启,即重启后失效。

会话系统变量仅针对于当前会话(连接)有效。会话期间,当前会话对某个会话系统变量值的修改,不会影响其他会话同一个会话系统变量的值。

会话 1 对某个全局系统变量值的修改会导致会话 2 中同一个全局系统变量值的修改。

系统变量的作用域并不是唯一的

在 MySQL 中有些系统变量的作用域只能是全局的,例如 max_connections 用于限制服务器的最大连接数;有些系统变量作用域既可以是全局又可以是会话,例如 character_set_client 用于设置客户端的字符集;有些系统变量的作用域只能是当前会话,例如 pseudo_thread_id 用于标记当前会话的 MySQL 连接 ID。

1.2 查看系统变量

查看所有或部分系统变量

1
2
3
4
5
6
# 查看所有全局变量
SHOW GLOBAL VARIABLES;
# 查看所有会话变量
SHOW SESSION VARIABLES;
# 或者直接
SHOW VARIABLES;
1
2
3
4
# 查看满足条件的部分系统变量
SHOW GLOBAL VARIABLES LIKE "%标识符%";
# 查看满足条件的部分会话变量
SHOW SESSION VARIABLES LIKE "%标识符%";

比如:

1
SHOW GLOBAL VARIABLES LIKE "admin_%";

查看指定系统变量

作为 MySQL 编码规范,MySQL 中的系统变量以两个 @ 开头,其中 @@global 仅用于标记全局系统变量,@@session 仅用于标记会话系统变量。@@ 首先标记会话系统变量,如果会话系统变量不存在,则标记全局系统变量。

1
2
3
4
5
6
7
# 查看指定的系统变量的值
SELECT @@global.变量名;

# 查看指定的会话变量的值
SELECT @@session.变量名;
# 或者
SELECT @@变量名;

修改系统变量的值

某些情况下,数据库管理员需要修改系统变量的默认值,以便修改当前会话或者 MySQL 服务实例的属性、特征。修改方式有两种:

方式一:修改 MySQL 配置文件,继而修改 MySQL 系统变量的值(该方法需要重启 MySQL 服务);

方式二:在 MySQL 服务运行期间,使用 SET 命令重新设置系统变量的值。

1
2
3
4
5
6
7
8
9
# 为某个系统变量赋值
SET @@galbal.变量名 = 变量值;
# 或者
SET GLOBAL 变量名 = 变量值;

# 为某个会话变量赋值
SET @@session.变量名 = 变量值;
# 或者
SET SESSION 变量名 = 变量值;

比如:

1
2
SELECT @@global.autocommit; # 1
SET GLOBAL autocommit = 0;
1
2
3
4
5
6
SELECT @@session.tx_isolation;
SET @@session.tx_isolation='read-uncommitted';

# MySQL 8.0 使用以下语句:
SELECT @@session.transaction_isolation; # REPEATABLE-READ
SET @@session.transaction_isolation='READ-UNCOMMITTED';
1
2
SELECT @@global.max_connections; # 151
SET GLOBAL max_connections = 1000;

1.3 用户变量分类

用户变量是用户自己定义的,作为 MySQL 编码规范,MySQL 中的用户变量以一个 @ 开头。根据作用范围不同,又分为会话用户变量和局部变量。

会话用户变量:作用域和会话变量一样,只对当前连接会话有效。

局部变量:只在 BEGINEND 语句块中有效。局部变量只能在存储过程和函数中使用。

1.4 会话用户变量

变量的定义

1
2
3
4
5
6
7
# 方式一:使用 = 或者 := 
SET @用户变量 = 值;
SET @用户变量 := 值;

# 方式二:使用 :=INTO 关键字
SELECT @用户变量 := 表达式 [FROM 等子句]
SELECT 表达式 INTO @用户变量 [FROM 等子句]

查看用户变量的值

1
SELECT @用户变量;

比如:

1
2
3
4
5
6
7
8
9
10
SET @a = 1;
SELECT @a;

SELECT @num := COUNT(*) FROM employees;
SELECT @num;

SELECT AVG(salary) INTO @avgsalary FROM employees;
SELECT @avgsalary;

SELECT @big; # 未声明的变量,返回 NULL

1.5 局部变量

可以使用 DECLARE 语句定义一个局部变量。局部变量仅仅在它所在的 BEGIN...END 之间生效,局部变量也只能放在 BEGIN...END 中,而且声明只能放在开头。

1
2
3
4
5
6
7
8
9
10
11
12
BEGIN 
# 声明局部变量
DECLARE 变量名1 变量数据类型 [DEFAULT 变量默认值];
DECLARE 变量名2, 变量名3, ... 变量数据类型 [DEFAULT 变量默认值];

# 为局部变量赋值
SET 变量名1 = 值;
SELECTINTO 变量名2 [FROM 子句];

# 查看局部变量的值
SELECT 变量1,变量2,变量3;
END

定义变量

1
2
# 未显式声明默认值时,初始值为 NULL
DECLARE 变量名 类型 [DEFAULT 默认值];

比如:

1
DECLARE myparam INT DEFAULT 100;

变量赋值

1
2
3
4
5
6
# 简单的赋值
SET 变量名 = 值;
SET 变量名 := 值;

# 复杂的赋值
SELECT 字段名或表达式 INTO 变量名 FROM 表;

使用变量

1
SELECT 局部变量名;

使用示例

示例一:声明局部变量,并分别赋值为 employees 表中 employee_id 为 102 的 last_name 和 salary。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DELIMITER //

CREATE PROCEDURE set_value()
BEGIN
DECLARE emp_name VARCHAR(25);
DECLARE sal DOUBLE(10,2);

SELECT last_name, salary INTO emp_name, sal
FROM employees
WHERE employee_id = 102;

SELECT emp_name, sal;
END //

DELIMITER ;

# 调用创建的存储过程
CALL set_value();

示例二:声明两个变量,求和并打印(分别使用会话用户变量、局部变量的方式实现)。

1
2
3
4
5
# 使用会话用户变量
SET @m = 1;
SET @n = 1;
SET @sum = @n + @m;
SELECT @sum;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用局部变量
DELIMITER //

CREATE PROCEDURE add_value()
BEGIN
DECLARE m INT DEFAULT 1;
DECLARE n INT DEFAULT 3;
DECLARE SUM INT;

SET SUM = m + n;
SELECT SUM;
END //

DELIMITER ;

# 调用存储过程
CALL add_value();

示例三:创建存储过程 different_salary 查询某员工和他领导的薪资差距,并用 IN 参数 emp_id 接收员工 id,用 OUT 参数 dif_salary 输出薪资差距结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DELIMITER //

CREATE PROCEDURE different_salary(IN emp_id INT, OUT dif_salary DOUBLE)
BEGIN
DECLARE emp_sal,mgr_sal DOUBLE DEFAULT 0.0;
DECLARE mgr_id INT;
SELECT salary INTO emp_sal FROM employees
WHERE employee_id = emp_id;
SELECT manager_id INTO mgr_id FROM employees
WHERE employee_id = emp_id;
SELECT salary INTO mgr_sal FROM employees
WHERE employee_id = mgr_id;
SET dif_salary = mgr_sal - emp_sal;
END //

DELIMITER ;

# 创建会话用户变量并调用存储过程
SET @emp_id = 102;
CALL different_salary(@emp_id, @diff_sal);

# 查看另一个用户会话变量
SELECT @diff_sal;

1.6 会话用户变量与局部变量的区别

作用域 定义位置 语法
会话用户变量 当前会话 会话的任何地方 @ 符号,不要指定类型
局部变量 定义它的 BEGIN…END 中 BEGIN…END 的第一句话 一般不加 @ 符号,需要指定类型

2. 定义条件与处理程序

定义条件是事先定义程序执行过程中可能遇到的问题,处理程序定义了在遇到问题时应当采取的处理方式,并且保证存储过程或函数在遇到警告或错误时能继续执行。这样可以增强存储程序处理问题的能力,避免程序异常停止运行。

存储过程、存储函数中支持定义条件和处理程序。

2.1 案例分析

创建一个名为 UpdateDataNoCondition 的存储过程:

1
2
3
4
5
6
7
8
9
10
11
12
DELIMITER //

CREATE PROCEDURE UpdateDataNoCondition()
BEGIN
SET @x = 1;
UPDATE employees SET email = NULL WHERE last_name = 'Abel';
SET @x = 2;
UPDATE employees SET email = 'aabbel' WHERE last_name = 'Abel';
SET @x = 3;
END //

DELIMITER ;

调用存储过程:

1
CALL UpdateDataNoCondition();

调用失败,出现以下错误信息:

1048 - Column 'email' cannot be null

再查看会话成员变量 x 的值:

1
SELECT @x;

可以看到 @x 变量的值为 1,这是因为当存储过程中执行的 SQL 语句报错,且在存储过程中未定义条件和处理程序时,MySQL 数据库会抛出错误,并退出当前 SQL 逻辑,不再向下继续执行,因此 @x 仍为 1。

2.2 定义条件

定义条件就是给 MySQL 中的错误码命名,有助于存储的程序代码更清晰。它将一个错误名字和指定的错误条件关联起来。这个名字可以被用在定义处理程序的 DECLARE HANDLER 语句中。

定义条件需要使用 DECLARE 语句,语法格式如下:

1
DECLARE 错误名称 CONDITION FOR 错误码(或错误条件);

错误码的说明

MySQL_error_codesqlstate_value 都可以表示 MySQL 的错误。

MySQL_error_code 是数值类型错误码。sqlstate_value 是长度为 5 的字符串类型错误码。

比如:

ERROR 1418 (HY000) 中,1418 是 MySQL_error_codeHY000sqlstate_value

ERROR 1142 (42000) 中,1142 是 MySQL_error_code42000sqlstate_value

使用示例

示例一:定义 Field_Not_Be_NULL 错误名与 MySQL 中违反非空约束的错误类型 ERROR 1048 (23000) 对应。

1
2
DECLARE Field_Not_Be_NULL CONDITION FOR 1048;
DECLARE Field_Not_Be_NULL CONDITION FOR SQLSTATE '230000';

示例二:定义 ERROR 1148(42000) 错误,名称为 command_not_allowed

1
2
DECLARE command_not_allowed CONDITION FOR 1148;
DECLARE command_not_allowed CONDITION FOR SQLSTATE '42000';

2.3 定义处理程序

可以为 SQL 执行过程中发生的某些类型的错误定义特殊的处理程序。语法如下:

1
DECLARE 处理方式 HANDLER FOR 错误类型 处理语句;

处理方式

有三个取值:CONTINUEEXITUNDO

  • CONTINUE:遇到错误不处理,继续执行;

  • EXIT:遇到错误马上退出;

  • UNDO:遇到错误后撤回之前的操作。 MySQL 中暂时不支持这样的操作。

错误类型

SQLSTATE '字符串错误码':长度为 5 的 sqlstate_value 类型的错误码;

MySQL_error_code:匹配数值类型错误码;

错误名称DECLARE...CONDITION 定义的错误条件名称;

SQLWARNING:匹配所有以 01 开头的 SQLSTATE 错误码;

NOT FOUND:匹配所有以 02 开头的 SQLSTATE 错误码;

SQLEXCEPTION:匹配所有没有被 SQLWARNINGNOT FOUND 捕获的 SQLSTATE 错误码。

处理语句

如果出现上述条件之一,则采用对应的处理方式,并执行指定的处理语句。语句可以是像 SET 变量 = 值 这样的简单语句,也可以是使用 BEGIN...END 编写的复合语句。

定义处理程序的使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 捕获 sqlstate_value
DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @info = 'NO_SUCH_TABLE';

# 捕获 mysql_error_value
DECLARE CONTINUE HANDLER FOR 1146 SET @info = 'NO_SUCH_TABLE';

# 先定义条件,再调用
DECLARE no_such_table CONDITION FOR 1146;
DECLARE CONTINUE HANDLER FOR NO_SUCH_TABLE SET @info = 'NO_SUCH_TABLE';

# 使用 SQLWARNING
DECLARE EXIT HANDLER FOR SQLWARNING SET @info = 'ERROR';

# 使用 NOT FOUND
DECLARE EXIT HANDLER FOR NOT FOUND SET @info = 'NO_SUCH_TABLE';

# 使用 SQLEXCEPTION
DECLARE EXIT HANDLER FOR SQLEXCEPTION SET @info = 'ERROR';

2.4 解决案例

在存储过程中,定义处理程序,捕获 sqlstate_value 值,当遇到 MySQL_error_code 值为 1048 时,执行 CONTINUE 操作,并且将 @proc_value 的值设置为 -1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DELIMITER //

CREATE PROCEDURE UpdateDataNoCondition()
BEGIN
# 定义处理程序
DECLARE CONTINUE HANDLER FOR 1048 SET @proc_value = -1;
SET @x = 1;
UPDATE employees SET email = NULL WHERE last_name = 'Abel';
SET @x = 2;
UPDATE employees SET email = 'aabbel' WHERE last_name = 'Abel';
SET @x = 3;
END //

DELIMITER ;

再次调用存储过程 UpdateDataNoCondition,并查看 @x@proc_value 的值:

1
2
3
CALL UpdateDataNoCondition(); -- 执行成功

SELECT @x, @proc_value; -- 3 -1

定义条件与处理程序的综合使用

拷贝 departments 表中的数据到 dept 表中,并在 dept 表中为 department_name 添加唯一索引:

1
2
3
4
5
CREATE TABLE dept
AS
SELECT * FROM `departments`;

ALTER TABLE dept ADD CONSTRAINT uk_dept_name UNIQUE(department_name);

定义名为 InsertDataWithCondition 的存储过程,在存储过程中,定义处理程序,捕获 sqlstate_value 值,当遇到 sqlstate_value 值为 23000 时,执行 EXIT 操作,并将 @proc_value 的值设置为 -1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DELIMITER //

CREATE PROCEDURE InsertDataWithCondition()
BEGIN
# 定义条件
DECLARE duplicate_entry CONDITION FOR SQLSTATE '23000' ;
# 定义处理程序
DECLARE EXIT HANDLER FOR duplicate_entry SET @proc_value = -1;

SET @x = 1;
INSERT INTO dept(department_name) VALUES('测试');
SET @x = 2;
# 已存在数据,插入失败
INSERT INTO dept(department_name) VALUES('测试');
SET @x = 3;
END //

DELIMITER ;

调用存储过程,并查看 @x@proc_value 的值:

1
2
3
CALL InsertDataWithCondition(); -- 执行成功

SELECT @x, @proc_value; -- 2 -1

3. 流程控制

解决复杂问题不可能通过一个 SQL 语句完成,需要执行多个 SQL 操作。流程控制语句的作用就是控制存储过程中 SQL 语句的执行顺序,是我们完成复杂操作必不可少的一部分。只要是执行的程序,流程就分为三大类:

  • 顺序结构:程序从上到下依次执行
  • 分支结构:程序按条件进行选择执行,从两条或多条路径中选择一条执行
  • 循环结构:程序满足一定条件下,重复执行一组语句

针对于 MySQL 的流程控制语句主要有 3 类:

  • 条件判断语句:IF 语句和 CASE 语句
  • 循环语句:LOOPWHILEREPEAT 语句
  • 跳转语句:ITERATELEAVE 语句

3.1 分支结构之 IF

IF 语句的语法结构:

1
2
3
4
IF 表达式1 THEN 操作1
[ELSEIF 表达式2 THEN 操作2] ......
[ELSE 操作N]
END IF

根据表达式的结果为 TRUE 或 FALSE 执行相应的语句,[] 中的内容是可选的。

特点:

1、不同的表达式对应不同的操作

2、在 BEGIN...END 中使用

使用示例

示例一:

1
2
3
4
IF val IS NULL 
THEN SELECT 'val is null';
ELSE SELECT 'val is not null';
END IF;

示例二:声明存储过程 update_salary_by_eid1,定义 IN 参数 emp_id,输入员工编号。如果该员工薪资是否低于 8000 元且入职时间超过 5 年,就涨薪 500 元;否则就不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DELIMITER //

CREATE PROCEDURE update_salary_by_eid1(IN emp_id INT)
BEGIN
DECLARE emp_salary DOUBLE;
DECLARE hire_year DOUBLE;

SELECT salary INTO emp_salary
FROM employees WHERE employee_id = emp_id;

SELECT DATEDIFF(CURDATE(), hire_date) / 365 INTO hire_year
FROM employees WHERE employee_id = emp_id;

IF emp_salary < 8000 AND hire_year > 5
THEN UPDATE employees SET salary = salary + 500 WHERE employee_id = emp_id;
END IF;
END //

DELIMITER ;

示例三:声明存储过程 update_salary_by_eid2,定义 IN 参数 emp_id,输入员工编号。如果该员工薪资低于 9000 元并且入职时间超过 5 年,就涨薪 500 元;反之涨薪 100 元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DELIMITER //

CREATE PROCEDURE update_salary_by_eid2(IN emp_id INT)
BEGIN
DECLARE emp_salary DOUBLE;
DECLARE hire_year DOUBLE;

SELECT salary INTO emp_salary
FROM employees WHERE employee_id = emp_id;

SELECT DATEDIFF(CURDATE(), hire_date) / 365 INTO hire_year
FROM employees WHERE employee_id = emp_id;

IF emp_salary < 8000 AND hire_year > 5
THEN UPDATE employees SET salary = salary + 500
WHERE employee_id = emp_id;
ELSE
UPDATE employees SET salary = salary + 100
WHERE employee_id = emp_id;
END IF;
END //

DELIMITER ;

示例四:声明存储过程 update_salary_by_eid3,定义 IN 参数 emp_id,输入员工编号。如果该员工薪资低于 9000 元,更新薪资为 9000 元;如果薪资大于等于 9000 元但低于 10000,且奖金比例为 NULL,更新奖金比例为 0.01;其他的涨薪 100 元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DELIMITER //

CREATE PROCEDURE update_salary_by_eid3(IN emp_id INT)
BEGIN
DECLARE emp_salary DOUBLE;
DECLARE bonus DECIMAL(3, 2);

SELECT salary INTO emp_salary
FROM employees WHERE employee_id = emp_id;
SELECT commission_pct INTO bonus
FROM employees WHERE employee_id = emp_id;

IF emp_salary < 9000
THEN UPDATE employees SET salary = 9000 WHERE employee_id = emp_id;
ELSEIF emp_salary < 10000 AND bonus IS NULL
THEN UPDATE employees SET commission_pct = 0.01 WHERE employee_id = emp_id;
ELSE
UPDATE employees SET salary = salary + 100 WHERE employee_id = emp_id;
END IF;
END //

DELIMITER ;

3.2 分支结构之 CASE

CASE 语句的语法结构一:

1
2
3
4
5
6
7
# 类似与 switch
CASE 表达式
WHEN1 THEN 结果1或语句1(如果是语句, 需要加分号)
WHEN2 THEN 结果2或语句2(如果是语句, 需要加分号)
...
ELSE 结果n或语句n(如果是语句, 需要加分号)
END [CASE](如果是放在BEGIN END中需要加上CASE, 如果放在SELECT后面不需要)

CASE 语句的语法结构二:

1
2
3
4
5
6
7
# 类似于多重 if
CASE
WHEN 条件1 THEN 结果1或语句1(如果是语句, 需要加分号)
WHEN 条件2 THEN 结果2或语句2(如果是语句, 需要加分号)
...
ELSE 结果n或语句n(如果是语句, 需要加分号)
END [case](如果是放在BEGIN END中需要加上CASE, 如果放在SELECT后面不需要)

使用示例

示例一:使用 CASE 流程控制语句的第 1 种格式,判断 val 值等于 1、等于 2,或者两者都不等。

1
2
3
4
5
CASE val
WHEN 1 THEN SELECT 'val is 1';
WHEN 2 THEN SELECT 'val is 2';
ELSE SELECT 'val is not 1 or 2';
END CASE;

示例二:使用 CASE 流程控制语句的第 2 种格式,判断 val 是否为空、小于 0、大于 0 或者等于 0。

1
2
3
4
5
6
CASE
WHEN val IS NULL THEN SELECT 'val is null';
WHEN val < 0 THEN SELECT 'val is less than 0';
WHEN val > 0 THEN SELECT 'val is greater than 0';
ELSE SELECT 'val is 0';
END CASE;

示例三:声明存储过程 update_salary_by_eid4,定义 IN 参数 emp_id,输入员工编号。如果该员工薪资低于 9000 元,就更新薪资为 9000 元;如果薪资大于等于 9000 元但低于 10000 的,且奖金比例为 NULL 的,就更新奖金比例为 0.01;其他的涨薪 100 元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DELIMITER //

CREATE PROCEDURE update_salary_by_eid4(IN emp_id INT)
BEGIN
DECLARE emp_sal DOUBLE;
DECLARE bonus DECIMAL(3, 2);

SELECT salary INTO emp_sal
FROM employees WHERE employee_id = emp_id;
SELECT commission_pct INTO bonus
FROM employees WHERE employee_id = emp_id;

CASE
WHEN emp_sal < 9000
THEN UPDATE employees SET salary = 9000 WHERE employee_id = emp_id;
WHEN emp_sal < 10000 AND bonus IS NULL
THEN UPDATE employees SET commission_pct = 0.01 WHERE employee_id = emp_id;
ELSE
UPDATE employees SET salary = salary + 100 WHERE employee_id = emp_id;
END CASE;
END //

DELIMITER ;

示例四:声明存储过程 update_salary_by_eid5,定义 IN 参数 emp_id,输入员工编号。如果该员工的入职年限,如果是 0 年,薪资涨 50;如果是 1 年,薪资涨 100;如果是 2 年,薪资涨 200;如果是 3 年,薪资涨 300;如果是 4 年,薪资涨 400;其他的涨薪 500。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DELIMITER //

CREATE PROCEDURE update_salary_by_eid5(IN emp_id INT)
BEGIN
DECLARE emp_sal DOUBLE;
DECLARE hire_year DOUBLE;

SELECT salary INTO emp_sal
FROM employees WHERE employee_id = emp_id;
SELECT ROUND(DATEDIFF(CURDATE(), hire_date) / 365) INTO hire_year
FROM employees WHERE employee_id = emp_id;

CASE hire_year
WHEN 0 THEN UPDATE employees SET salary = salary + 50 WHERE employee_id = emp_id;
WHEN 1 THEN UPDATE employees SET salary = salary + 100 WHERE employee_id = emp_id;
WHEN 2 THEN UPDATE employees SET salary = salary + 200 WHERE employee_id = emp_id;
WHEN 3 THEN UPDATE employees SET salary = salary + 300 WHERE employee_id = emp_id;
WHEN 4 THEN UPDATE employees SET salary = salary + 400 WHERE employee_id = emp_id;
ELSE UPDATE employees SET salary = salary + 500 WHERE employee_id = emp_id;
END CASE;
END //

DELIMITER ;

3.3 循环结构之 LOOP

LOOP 循环语句用来重复执行某些语句。LOOP 内的语句会一直重复执行直到循环被退出(使用 LEAVE 子句跳出循环),跳出循环过程。

LOOP 语句的基本格式如下:

1
2
3
[loop_label:] LOOP 
循环执行的语句
END LOOP [loop_label]

其中,loop_label 表示 LOOP 语句的标注名称,可以省略。

使用示例

示例一:使用 LOOP 语句进行循环操作,id 值小于 10 时重复执行循环过程。

1
2
3
4
5
6
DECLARE id INT DEFAULT 0;
add_loop: LOOP
SET id = id + 1;
IF id >= 10 THEN LEAVE add_loop;
END IF;
END LOOP add_loop;

示例二:声明存储过程 update_salary_loop(),声明 OUT 参数 num,输出循环次数。存储过程中使用循环实现涨薪逻辑,薪资涨为原来的 1.1 倍,直到全公司的平均薪资达到 12000 结束,并统计循环次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DELIMITER //

CREATE PROCEDURE update_salary_loop(OUT num INT)
BEGIN
DECLARE avg_salary DOUBLE;
DECLARE loop_count INT DEFAULT 0;
SELECT AVG(salary) INTO avg_salary FROM employees;

label_loop: LOOP
IF avg_salary >= 12000 THEN LEAVE label_loop;
END IF;

UPDATE employees SET salary = salary * 1.1;
SET loop_count = loop_count + 1;
SELECT AVG(salary) INTO avg_salary FROM employees;
END LOOP label_loop;

SET num = loop_count;

END //

DELIMITER ;

3.4 循环结构之 WHILE

WHILE 语句创建一个带条件判断的循环过程。WHILE 在执行语句执行时,先对指定的表达式进行判断,如果为真,就执行循环内的语句,否则退出循环。WHILE 语句的基本格式如下:

1
2
3
[while_label:] WHILE 循环条件 DO
循环体
END WHILE [while_label];

while_label 为 WHILE 语句的标注名称,可以省略;如果循环条件结果为真,WHILE 语句内的语句或语句群被执行,直至循环条件为假,退出循环。

使用示例

示例一:i 值小于 10 时,重复执行循环过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DELIMITER //

CREATE PROCEDURE test_while()
BEGIN
DECLARE i INT DEFAULT 0;
WHILE i < 10 DO
SET i = i + 1;
END WHILE;
SELECT i;
END //

DELIMITER ;
# 调用存储过程
CALL test_while(); -- 10

示例二:声明存储过程 update_salary_while(),声明 OUT 参数 num,输出循环次数。存储过程中使用循环实现降薪逻辑,薪资降为原来的 90%,直到全公司的平均薪资达到 5000 结束,同时统计循环次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DELIMITER //

CREATE PROCEDURE update_salary_while(OUT num INT)
BEGIN
DECLARE avg_sal DOUBLE;
DECLARE while_count INT DEFAULT 0;

SELECT AVG(salary) INTO avg_sal FROM employees;
WHILE avg_sal > 5000 DO
UPDATE employees SET salary = salary * 0.9;
SET while_count = while_count + 1;
SELECT AVG(salary) INTO avg_sal FROM employees;
END WHILE;

SET num = while_count;
END //

DELIMITER ;

3.5 循环结构之 REPEAT

REPEAT 循环与 WHILE 循环类似,与 WHILE 循环不同的是,REPEAT 循环首先会执行一次循环,然后在 UNTIL 中进行表达式的判断,如果满足条件就退出,即 END REPEAT;如果条件不满足,则会就继续执行循环,直到满足退出条件为止。

REPEAT 语句的基本格式如下:

1
2
3
4
[repeat_label:] REPEAT
循环体的语句
UNTIL 结束循环的条件表达式
END REPEAT [repeat_label]

repeat_label 为 REPEAT 语句的标注名称,可以省略;REPEAT 语句内的语句或语句群被重复,直至 expr_condition 为真。

注意: UNTIL 语句后的条件表达式结束没有以 ; 结尾,也不需要任何结束符。

使用示例

示例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DELIMITER //

CREATE PROCEDURE test_repeat()
BEGIN
DECLARE i INT DEFAULT 0;

REPEAT
SET i = i + 1;
UNTIL i >= 10
END REPEAT;

SELECT i;
END //

DELIMITER ;

示例二:声明存储过程 update_salary_repeat(),声明 OUT 参数 num,输出循环次数。存储过程中使用循环实现涨薪逻辑,薪资涨为原来的 1.15 倍,直到全公司的平均薪资达到 13000 结束,同时统计循环次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DELIMITER //

CREATE PROCEDURE update_salary_repeat(OUT num INT)
BEGIN
DECLARE avg_sal DOUBLE;
DECLARE repeat_count INT DEFAULT 0;

SELECT AVG(salary) INTO avg_sal FROM employees;
REPEAT
UPDATE employees SET salary = salary * 1.15;
SET repeat_count = repeat_count + 1;
SELECT AVG(salary) INTO avg_sal FROM employees;
UNTIL avg_sal >= 13000
END REPEAT;

SET num = repeat_count;
END //

DELIMITER ;

三种循环结构的对比

1、三种循环都可以省略名称,但如果在循环中添加了循环控制语句(LEAVEITERATE)就必须声明名称。

2、应用场景:

  • LOOP:实现简单的死循环
  • WHILE:先判断再执行
  • REPEAT:先执行再判断,至少执行一次

3.6 跳转语句之 LEAVE

LEAVE 可以用在循环语句内,或者以 BEGIN...END 包裹起来的程序体内,表示跳出循环或者跳出程序体的操作(与 Java 中的 break 类似)。基本格式如下:

1
LEAVE label

使用示例

示例一:创建存储过程 leave_begin(),声明 INT 类型的 IN 参数 num。给 BEGIN...END加标记名,并在 BEGIN...END 中使用 IF 语句判断 num 参数的值:

  • 如果 num <= 0,使用 LEAVE 语句退出 BEGIN...END
  • 如果 num = 1,查询 employees 表的平均薪资;
  • 如果 num = 2,查询 employees 表的最低薪资;
  • 如果 num > 2,查询 employees 表的最高薪资。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DELIMITER //

CREATE PROCEDURE leave_begin(IN num INT)
begin_label: BEGIN
IF num <= 0
THEN LEAVE begin_label;
ELSEIF num = 1
THEN SELECT AVG(salary) FROM employees;
ELSEIF num = 2
THEN SELECT MIN(salary) FROM employees;
ELSE
SELECT MAX(salary) FROM employees;
END IF;

SELECT COUNT(*) FROM employees;
END //

DELIMITER ;

示例二:声明存储过程 leave_while() ,声明 OUT 参数 num,输出循环次数,存储过程中使用 WHILE 循环实现降薪逻辑,降低薪资为原来薪资的 90%,直到全公司的平均薪资小于等于 10000,同时统计循环次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DELIMITER //

CREATE PROCEDURE leave_while(OUT num INT)
BEGIN
DECLARE avg_sal DOUBLE;
DECLARE while_count INT DEFAULT 0;

SELECT AVG(salary) INTO avg_sal FROM employees;
while_label: WHILE TRUE DO
IF avg_sal <= 10000 THEN
LEAVE while_label;
END IF;
UPDATE employees SET salary = salary * 0.9;
SET while_count = while_count + 1;
SELECT AVG(salary) INTO avg_sal FROM employees;
END WHILE;

SET num = while_count;
END //

DELIMITER ;

3.7 跳转语句之 ITERATE

ITERATE 语句只能用在循环语句(LOOPREPEATWHILE 语句)内,表示重新开始循环,将执行顺序转到语句段开头处(与 Java 中的 continue 类似)。基本格式如下:

1
ITERATE label

使用示例

示例一:定义局部变量 num,初始值为 0,循环结构中执行 num + 1 操作:

  • 如果 num < 10,继续执行循环;
  • 如果 num > 15,退出循环结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DELIMITER //

CREATE PROCEDURE test_iterate()
BEGIN
DECLARE num INT DEFAULT 0;

my_loop: LOOP
SET num = num + 1;
IF num < 10
THEN ITERATE my_loop;
ELSEIF num > 15
THEN LEAVE my_loop;
END IF;
END LOOP my_loop;
SELECT num;
END //

DELIMITER ;

CALL test_iterate(); -- 16

4. 游标

4.1 什么是游标

虽然可以通过筛选条件 WHEREHAVING,或者限定返回记录的关键字 LIMIT 返回一条记录,但无法在结果集中像指针一样,向前或向后定位一条记录,再或者是随意定位到某一条记录,并对记录的数据进行处理。

要想完成这样的需求,可以使用游标。游标提供了一种灵活的操作方式,能够对结果集中的每一条记录进行定位,并对指向的记录中的数据进行操作的数据结构。游标让 SQL 这种面向集合的语言有了面向过程开发的能力。

在 SQL 中,游标是一种临时的数据库对象,可以指向存储在数据库表中的数据行。此时游标充当了指针的作用,进而可以通过操作游标来对数据行进行操作。

MySQL 中游标可以在存储过程和函数中使用。

比如,查询 employees 数据表中工资高于 15000 的员工都有哪些:

1
SELECT employee_id, last_name, salary FROM employees WHERE salary > 15000;

工资高于15000的员工信息

如图所示,此时游标所在的行是 employee_id 为 101 的记录,可以在结果集中向上滚动游标,指向结果集中的其他行。

4.2 游标的使用

游标必须在声明处理程序之前被声明,并且变量和条件也必须在声明游标或处理程序之前被声明。

游标的使用一般需要经历四个步骤。不同的 DBMS 中,使用游标的语法可能略有不同。

第一步:声明游标

在 MySQL 中,使用 DECLARE 关键字来声明游标,基本语法如下:

1
DECLARE cursor_name CURSOR FOR select_statement;

以上语法适用于 MySQL、SQL Server、DB2 和 MariaDB。如果使用的是 Oracle 或者 PostgreSQL,则需要变为:

1
DECLARE cursor_name CURSOR IS select_statement;

select_statement 代表的是 SELECT 语句,返回一个用于创建游标的结果集。比如:

1
2
3
DECLARE cur_emp CURSOR FOR SELECT employee_id, salary FROM employees;
# 或者
DECLARE cursor_fruit CURSOR FOR SELECT f_name, f_price FROM fruits ;

第二步:打开游标

打开游标的语法如下:

1
OPEN cursor_name;

要想使用定义好的游标,必须先打开游标。打开游标时 SELECT 语句的查询结果集就会送到游标工作区,为后面游标的逐条读取结果集中的记录做准备。

1
OPEN cur_emp;

第三步:使用游标

使用游标的语法如下:

1
FETCH cursor_name INTO var_name [, var_name] ...

这句的作用是使用游标 cursor_name 来读取当前行,并且将数据保存到 var_name 变量中,然后游标指针指到下一行。如果游标读取的数据行有多个列名,则在 INTO 关键字后面赋值给多个变量名即可。

注意: var_name 必须在声明游标之前就定义好。

1
FETCH cur_emp INTO emp_id, emp_sal;

注意: 游标的查询结果集中的字段数必须更 INTO 后面的变量数一致,否则在执行存储过程时,MySQL 会报错。

第四步:关闭游标

关闭游标的语法如下:

1
CLOSE cursor_name;

有 OPEN 那就有 CLOSE,使用完游标后应该立即关闭掉该游标。游标会占用系统资源,如果不及时关闭,游标会一直保持到存储过程结束,影响系统运行的效率,而关闭游标的操作,会释放游标占用的系统资源。

关闭游标之后,就不能再检索查询结果中的数据行,如果需要检索只能再次打开游标。

1
CLOSE cur_emp;

4.3 使用示例

创建存储过程 get_count_by_limit_total_salary(),声明 DOUBLE 类型 IN 参数 limit_total_salary 和 INT 类型 OUT 参数 total_count。该存储过程实现累加薪资最高的几个员工的薪资值,直到薪资总和达到 limit_total_salary 参数的值并将累加的人数返回给 total_count。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DELIMITER // 

CREATE PROCEDURE get_count_by_limit_total_salary(IN limit_total_salary DOUBLE, OUT total_count INT)
BEGIN
DECLARE sum_salary DOUBLE DEFAULT 0; # 记录累加的总工资
DECLARE cursor_salary DOUBLE DEFAULT 0; # 记录某一个工资值
DECLARE emp_count INT DEFAULT 0; # 记录循环个数
# 定义游标
DECLARE emp_cursor CURSOR FOR SELECT salary FROM employees ORDER BY salary DESC;
# 打开游标
OPEN emp_cursor;

REPEAT
# 使用游标(从游标中获取数据)
FETCH emp_cursor INTO cursor_salary;
SET sum_salary = sum_salary + cursor_salary;
SET emp_count = emp_count + 1;
UNTIL sum_salary >= limit_total_salary
END REPEAT;

SET total_count = emp_count;

# 关闭游标
CLOSE emp_cursor;
END //

DELIMITER ;

4.4 游标小结

游标是 MySQL 的一个重要的功能,为逐条读取结果集中的数据,提供了完美的解决方案。跟在应用层面实现相同的功能相比,游标可以在存储程序中使用,效率高,程序也更加简洁。

游标的使用也会带来一些性能问题,比如在使用游标的过程中,会对数据行进行加锁,在业务并发量大的时候,不仅会影响业务之间的效率,还会消耗系统资源,造成内存不足,这是因为游标是在内存中进行的处理。

建议: 使用游标时应当养成“随手关闭”的习惯,以提高系统的整体效率。

5. MySQL 8.0 的新特性 - 全局变量的持久化

在 MySQL 中,全局变量可以通过 SET GLOBAL 语句来设置。例如设置服务器语句超时的限制,可以通过设置系统变量 max_execution_time 来实现:

1
SET GLOBAL MAX_EXECUTION_TIME = 2000;

使用 SET GLOBAL 语句设置的变量值只会临时生效。当数据库重启后,服务器又会从 MySQL 配置文件中读取变量的默认值导致先前的修改失效。

MySQL 8.0 中新增了 SET PERSIST 命令。例如,设置服务器的最大连接数为1000:

1
SET PERSIST global max_connections = 1000;

MySQL 会将该命令的配置保存到数据目录下的 mysqld-auto.cnf 文件中,下次启动时自动读取该文件内容,用其中的配置来覆盖默认的配置文件。

使用示例

查看全局变量 max_connections 的值:

1
show variables like '%max_connections%';

设置全局变量 max_connections 的值:

1
set persist max_connections = 1000;

重启 MySQL,再次查询 max_connections 的值:

1
show variables like '%max_connections%';

6. 触发器

6.1 为什么要使用触发器

在实际开发中,经常会遇到这样的情况:有 2 个或者多个相互关联的表,如商品信息和库存信息,它们分别存放在 2 个不同的数据表中。当添加一条新商品记录的时候,为了保证数据的完整性,必须同时在库存表中添加一条库存记录。

这样一来,就必须把这两个关联的操作步骤写到同一个程序里面,还要用事务包裹起来,确保这两个操作成为一个原子操作,要么全部执行,要么全部不执行。如果遇到特殊情况,需要对数据进行手动维护时,很有可能会忘记其中的一步,进而导致数据缺失。

为了解决这个问题可以使用触发器。创建一个触发器,让商品信息数据的插入操作自动触发库存数据的插入操作,这样就不用担心因为忘记添加库存数据而导致数据缺失了。

6.2 什么是触发器

MySQL 从 5.0.2 版本开始支持触发器。MySQL 的触发器和存储过程一样,都是嵌入到 MySQL 的一段程序。

触发器是由事件来触发某个操作,这些事件包括 INSERTUPDATEDELETE 事件。所谓事件就是指用户的动作触发了某项行为。如果定义了触发程序,当数据库执行这些语句时候,就相当于事件发生了,同时自动激发触发器执行相应的操作。

当对数据表中的数据执行插入、更新和删除操作,并且还需要自动执行一些数据库逻辑时,可以使用触发器来实现。

6.3 创建触发器的语法

基本语法如下:

1
2
3
4
CREATE TRIGGER 触发器名称 
{BEFORE | AFTER} {INSERT | UPDATE | DELETE} ON 表名
FOR EACH ROW
触发器执行的语句块;

说明:

1、表名表示触发器监控的对象;

2、BEFORE | AFTER 表示触发的时间点,BEFORE 表示在事件之前触发,AFTER 表示在事件之后触发;

3、INSERT | UPDATE | DELETE 表示触发的事件:

  • INSERT 表示插入记录时触发;
  • UPDATE 表示更新记录时触发;
  • DELETE 表示删除记录时触发。

4、触发器执行的语句块:可以是单条 SQL 语句,也可以是由 BEGIN...END 结构组成的复合语句块。

6.4 触发器的使用示例

示例一

创建数据表:

1
2
3
4
5
6
7
8
9
CREATE TABLE test_trigger (
id INT PRIMARY KEY AUTO_INCREMENT,
t_note VARCHAR(30)
);

CREATE TABLE test_trigger_log (
id INT PRIMARY KEY AUTO_INCREMENT,
t_log VARCHAR(30)
);

创建名为 before_insert 的触发器,向 test_trigger 数据表插入数据之前,向 test_trigger_log 数据表中插入 before_insert 的日志信息:

1
2
3
4
5
6
7
8
9
10
11
DELIMITER // 

CREATE TRIGGER before_insert
BEFORE INSERT ON test_trigger
FOR EACH ROW
BEGIN
INSERT INTO test_trigger_log (t_log)
VALUES('before_insert');
END //

DELIMITER ;

向 test_trigger 表中插入数据:

1
INSERT INTO test_trigger(t_note) VALUES ('测试 BEFORE INSERT 触发器');

查看 test_trigger_log 数据表中的数据:

1
SELECT * FROM test_trigger_log;

查看到before_insert日志

示例二

创建名称为 after_insert 的触发器,向 test_trigger 数据表插入数据之后,向 test_trigger_log 数据表中插入 after_insert 的日志信息:

1
2
3
4
5
6
7
8
9
10
11
DELIMITER //

CREATE TRIGGER after_insert
AFTER INSERT ON test_trigger
FOR EACH ROW
BEGIN
INSERT INTO test_trigger_log (t_log)
VALUES('after_insert');
END //

DELIMITER ;

向 test_trigger 数据表中插入数据:

1
INSERT INTO test_trigger(t_note) VALUES ('测试 AFTER INSERT 触发器');

查看 test_trigger_log 数据表中的数据:

1
SELECT * FROM test_trigger_log;

查看到after_insert日志

示例三

基于员工表 employees 的 INSERT 事件定义触发器 salary_check_trigger,在 INSERT 之前检查将要添加的新员工薪资是否大于他领导的薪资,如果大于领导薪资,就报 sqlstate_value 为 HY000 的错误,从而使得添加失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DELIMITER //

CREATE TRIGGER salary_check_trigger
BEFORE INSERT ON employees FOR EACH ROW
BEGIN
DECLARE mgrsalary DOUBLE;
SELECT salary INTO mgrsalary FROM employees WHERE employee_id = NEW.manager_id;
# NEW 表示执行 INSERT 语句新增的记录
IF NEW.salary > mgrsalary THEN
SIGNAL SQLSTATE 'HY000' SET MESSAGE_TEXT = '薪资高于领导薪资错误';
END IF;
END //

DELIMITER ;

6.5 查看触发器

可以使用以下三种方式查看数据库中已经存在的触发器的定义、状态和语法等信息。

查看当前数据库的所有触发器的定义

1
2
3
4
# 命令行中使用
SHOW TRIGGERS\G
# 图形化界面中使用
SHOW TRIGGERS;

查看当前数据库中某个触发器的定义

1
SHOW CREATE TRIGGER 触发器名;

比如:

1
SHOW CREATE TRIGGER after_insert;

从系统库 information_schema 的 TRIGGERS 表中查询触发器的信息。

1
SELECT * FROM information_schema.TRIGGERS;

比如:

1
SELECT * FROM information_schema.TRIGGERS WHERE TRIGGER_NAME = 'after_insert';

6.6 删除触发器

基本语法如下:

1
DROP TRIGGER IF EXISTS 触发器名称;

比如:

1
DROP TRIGGER IF EXISTS after_insert;

再次查询触发器 after_insert 的信息:

1
SHOW CREATE TRIGGER after_insert;

出现以下错误信息:

1360 - Trigger does not exist

6.7 触发器的优缺点

触发器的优点

1、触发器可以确保数据的完整性

2、触发器可以更方便地记录操作日志

3、触发器可以用在操作数据前,对数据进行合法性检查

触发器的缺点

1、触发器的可读性差

触发器存储在数据库中,并且由事件驱动,这意味着触发器有可能不受应用层的控制。

假设创建触发器用于修改会员储值操作,当触发器中的操作出了问题,会导致会员储值金额更新失败。比如执行以下语句时出现了错误:

1
UPDATE demo.membermaster SET memberdeposit = 20 WHERE memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'

错误信息显示字段 aa 不存在。这是因为定义的触发器中的数据插入操作多了一个字段,导致系统提示错误。如果编码人员不了解这个触发器,很可能会认为是更新语句本身的问题,或者是会员信息表的结构出了问题。说不定还会给会员信息表添加一个叫 aa 的字段,试图解决这个问题,结果只能是白费力。

2、相关数据的变更,可能会导致触发器出错

特别是数据表结构的变更,都可能会导致触发器出错,进而影响数据操作的正常运行。这些都是由于触发器本身的隐蔽性,影响到应用中错误原因排查的效率。

触发器的使用注意事项

注意,如果在子表中定义了外键约束,并且外键指定了 ON UPDATE/DELETE CASCADE/SET NULL子句,此时修改父表被引用的键值或删除父表被引用的记录行时,也会引起子表的修改和删除操作,但此时基于子表的 UPDATEDELETE 语句定义的触发器并不会被激活。

例如:基于子表员工表(t_employee)的 DELETE 语句定义了触发器 t1,而子表的部门编号(did)字段定义了外键约束引用了父表部门表(t_department)的主键列部门编号(did),并且该外键加了 ON DELETE SET NULL 子句。当删除部门表(t_department)中的某条数据时,且被删除数据的 did 已经在员工表(t_employee)中被引用,员工表(t_employee)中对应记录的部门编号(did)会被修改为 NULL,但并不会激活触发器 t1。只有直接对员工表(t_employee)执行 DELETE 语句时才会激活触发器 t1。