Java小白开始学习MySQL数据库,一方面,跟着视频学习,并记录下学习笔记,方便以后复习回顾。另一方面,发布学习笔记来约束自己,学习路程还很遥远,继续加油坚持!!!希望能帮助到大家!
本次更新SQL优化,包括插入优化、主键优化、order by优化、group by优化、limit优化、count优化、update优化;视图、存储过程、触发器等相关知识点。
上期链接MySQL数据库学习笔记,超详细!!(4)
另外还有我的牛客Java专项练习笔记专栏、Spring框架学习专栏也在同步更新,希望大家多多关注,一起学习!!!
9. SQL优化
9.1 插入优化
9.1.1 插入数据
如果我们需要一次性往数据库表中插入多条记录,可以从以下三个方面进行优化。
insert into tb_test values(1,'tom');
insert into tb_test values(2,'cat');
insert into tb_test values(3,'jerry');
.....
优化方案一:批量插入数据
Insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
优化方案二:手动提交事务
start transaction;
insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
insert into tb_test values(4,'Tom'),(5,'Cat'),(6,'Jerry');
insert into tb_test values(7,'Tom'),(8,'Cat'),(9,'Jerry');
commit;
优化方案三:逐渐顺序插入,性能要高于乱序插入
主键乱序插入 : 8 1 9 21 88 2 4 15 89 5 7 3
主键顺序插入 : 1 2 3 4 5 7 8 9 15 21 88 89
9.1.2 大批量插入数据
如果一次性需要插入大批量数据,使用insert语句插入性能较低,此时可以使用MySQL数据库提供的load指令进行插入

可以执行如下指令,将数据脚本文件中的数据加载到表结构中:
-- 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p
-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
-- 执行load指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table tb_user fields terminated by ',' lines terminated by '\n' ;
9.2 主键优化
主键顺序插入的性能是要高于乱序插入
数据组织方式
在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(index organized table IOT)。

行数据,都是存储在聚集索引的叶子节点上的。InnoDB的逻辑结构图:

在InnoDB引擎中,数据行是记录在逻辑结构 page 页中的,而每一个页的大小是固定的,默认16K。那也就意味着, 一个页中所存储的行也是有限的,如果插入的数据行row在该页存储不小,将会存储到下一个页中,页与页之间会通过指针连接。
页分裂
页可以为空,也可以填充一半,也可以填充100%。每个页包含2-N行数据(如果一行数据过大,会行溢出),根据主键排列
主键顺序插入

当第一个页写满之后,再写入第二个页,页与页之间会通过指针连接
主键乱序插入

1#页,已经写满了,存储不了50对应的数据了。 那么此时会开辟一个新的页 3#。但是并不会直接将50存入3#页,而是会将1#页后一半的数据,移动到3#页,然后在3#页,插入50。移动数据,并插入id为50的数据之后,那么此时,这三个页之间的数据顺序是有问题的。 1#的下一个页,应该是3#, 3#的下一个页是2#。 所以,此时,需要重新设置链表指针。上述的这种现象,称之为 "页分裂",是比较耗费性能的操作。
页合并
当删除一行记录时,实际上并没有被物理删除,只是记录被标记(flaged)为删除并且它的空间变得被允许被其他记录声明使用
当页中删除的记录MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前或后)看看是否可以将两个页合并并优化空间使用,这个里面所发生的合并页的这个现象,就称之为 "页合并"。

主键设计原则
满足业务需求情况下,尽量降低主键长度
插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键。
尽量不要使用UUID做主键或者是其他自然主键,如身份证号
业务操作时,避免对主键的修改
9.3 order by优化
MySQL的排序有两种方式:
Using filesort:通过表的索引或全表扫描,读取满足条件的数据行,然后再排序缓冲区sort buffer中完成排序操作,所有不是通过索引直接返回结果的排序都叫FileSort排序
Using index:通过有序索引排序扫描直接返回有序数据,这种情况即为using index,不需要额外排序,操作效率高
对于以上的两种排序方式,Using index的性能高,而Using filesort的性能低,在优化排序操作时,尽量要优化为 Using index。
案例:
-- 执行排序SQL
explain select id,age,phone from tb_user order by age ;
explain select id,age,phone from tb_user order by age, phone ;

创建索引后,根据age, phone进行升序排序
-- 创建索引
create index idx_user_age_phone_aa on tb_user(age,phone);
-- 创建索引后,根据age, phone进行升序排序
explain select id,age,phone from tb_user order by age;
explain select id,age,phone from tb_user order by age , phone;

建立索引之后,再次进行排序查询,就由原来的Using filesort, 变为了 Using index,性能就是比较高的了。
创建索引后,根据age, phone进行降序排序
explain select id,age,phone from tb_user order by age desc , phone desc ;

也出现Using index,但是此时Extra中出现了Backward index scan,这个代表反向扫描索引,因为在MySQL中创建的索引,默认索引的叶子节点是从小到大排序的,而此时查询的排序的从大到小,所以,在扫描时,就是反向扫描,就会出现 Backward index scan。 在MySQL8版本中,支持降序索引,我们也可以创建降序索引。
根据phone,age进行升序排序,phone在前,age在后。
explain select id,age,phone from tb_user order by phone , age;

排序时,也需要满足==最左前缀法则==,否则也会出现 filesort。因为在创建索引的时候,age是第一个
字段,phone是第二个字段,所以排序时,也就该按照这个顺序来,否则就会出现 Using filesort
根据age, phone进行降序一个升序,一个降序
explain select id,age,phone from tb_user order by age asc , phone desc ;

因为创建索引时,如果未指定顺序,默认都是按照升序排序的,而查询时,一个升序,一个降序,此时就会出现Using filesort。

为了解决上述的问题,我们可以创建一个索引,这个联合索引中 age 升序排序,phone 倒序排序。
-- 创建联合索引(age 升序排序,phone 倒序排序)
create index idx_user_age_phone_ad on tb_user(age asc ,phone desc);
-- 然后再次执行如下SQL
explain select id,age,phone from tb_user order by age asc , phone desc ;

order by优化原则:
根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则
尽量使用覆盖索引
多字段排序,一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)
如果不可避免地出现filesort,大数据量排序时,可以适当增大排序缓冲区大小sort_buff_size(默认256k)
9.4 group by优化
没有索引的情况下
explain select profession , count(*) from tb_user group by profession ;

针对于 profession , age, status 创建一个联合索引。
explain select profession , count(*) from tb_user group by profession ;

explain select age , count(*) from tb_user group by age ;

在联合索引中,也是符合最左前缀法则的。
group by优化原则:
在分组操作时,可以通过索引来提高效率。
分组操作时,索引的使用也是满足最左前缀法则的。
9.5 limit优化
在数据量比较大时,如果进行limit分页查询,在查询时,越往后,分页查询效率越低。
因为,当在进行分页查询时,如果执行 limit 2000000,10 ,此时需要MySQL排序前2000010 记录,仅仅返回 2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大 。
优化思路:一般分页查询时,通过创建 覆盖索引 能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化。
explain select t.* from tb_sku t , (select id from tb_sku order by id limit 2000000,10) a where t.id = a.id;
9.6 count优化
MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高;但是如果是带条件的count,MyISAM也慢。
InnoDB 引擎执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。
count用法 | 说明 |
count(主键) | InnoDB引擎会遍历整张表,把每一行的主键id值都取出来,返回给服务层。服务层拿到主键后,直接按行进行累加(主键不可能为null) |
count(字段) | 没有not null约束:InnoDB引擎会遍历整张表把每一行的字段值都取出来。返回给服务层,服务层判断是否为null,不为null,计数累加 有not null约束:把每一行的主键id值都取出来,返回给服务层。服务层拿到主键后,直接按行进行累加 |
count(数字) | InnoDB 引擎遍历整张表,但不取值。服务层对于返回的每一行,放一个数字“1”进去,直接按行进行累加。 |
count(*) | InnoDB引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加。 |
按照效率排序的话,count(字段) < count(主键 id) < count(1) ≈ count( ),所以尽量使用count()。
9.7 update优化
update course set name = 'javaEE' where id = 1 ;
当我们在执行删除的SQL语句时,会锁定id为1这一行的数据,然后事务提交之后,行锁释放。
但是当我们在执行如下SQL时:
update course set name = 'SpringBoot' where name = 'PHP' ;
当我们开启多个事务,在执行上述的SQL时,我们发现行锁升级为了表锁。导致该update语句的性能大大降低。
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁 ,并且该索引不能失效,否则会从行锁升级为表锁 。
10. 视图/存储过程/触发器
10.1 视图
10.1.1 介绍
视图(View)是一种虚拟存在的表。视图中的数据并不在数据库中实际存在,行和列数据来自定义视图的查询中使用的表,并且是在使用视图时动态生成的。
视图只保存了查询的SQL逻辑,不保存查询结果。
10.1.2 语法
创建
CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT 语句 [WITH [CASCADED | LOCAL] CHECK OPTION]
查询
-- 查看创建视图语句
SHOW CREATE VIEW 视图名称;
-- 查看视图数据
SELECT * FROM 视图名称 ...;
修改
-- 方式一
CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [WITH [CASCADED | LOCAL] CHECK OPTION]
-- 方式二
ALTER VIEW 视图名称[(列名列表)] AS SELECT语句 [WITH [CASCADED | LOCAL] CHECK OPTION]
删除
DROP VIEW [IF EXISTS] 视图名称 [,视图名称] ...
通过视图来插入、更新数据
create or replace view stu_v_1 as select id,name from student where id < = 10 ;
select * from stu_v_1;
insert into stu_v_1 values(6,'Tom');
insert into stu_v_1 values(17,'Tom22');
执行上述的SQL,我们会发现,id为6和17的数据都是可以成功插入的。 但是我们执行查询,查询出来的数据,却没有id为17的记录。
因为在创建视图的时候,指定的条件为 id<=10, id为17的数据,是不符合条件的,所以没有查询出来,但是这条数据确实是已经成功的插入到了基表中。
10.1.3 检查选项
当使用WITH CHECK OPTION子句创建视图时,MySQL会通过视图检查正在更改的每个行,例如:插入、更新、删除,以使其符合视图的定义,MySQL允许基于另一个视图创建视图,他还会检查依赖视图中的规则以保持一致性。为了确定检查的范围,MySQL提供了两个选项:CASCADED和LOCAL。默认值为CASCADED
CASCADED:级联
比如:v2视图是基于v1视图的,如果在v2视图创建的时候指定了检查选项为cascaded,但v1视图创建时未指定检查选项。则在执行检查时,不仅会检查v2,还会级联检查v2的关联试图v1

create or replace view v1 as select id, name from student where id < =20;
create or replace view v2 as select id, name from v1 where id > = 10 with cascaded check option ;
create or replace view v3 as select id, name from v2 where id < = 15 ;
-- 插入成功
insert into v3 values (11, tom);
-- 插入成功 v3没有加入检查 所以不会检查id < = 15
insert into v3 values (17, tom);
-- 插入失败 v3没有加入检查 所以不会检查id < = 15 但是基于v2,V2级联了V1所以会检查id < =20
insert into v3 values (28, tom);
LOCAL:本地
比如,v2视图是基于v1视图的,如果在v2视图创建的时候指定了检查选项为 local ,但是v1视图创建时未指定检查选项。 则在执行检查时,只会检查v2,不会检查v2的关联视图v1。

create or replace view v4 as select id, name from student where id <= 20;
create or replace view v5 as select id, name from v1 where id >= 10 with local check option ;
create or replace view v6 as select id, name from v2 where id <= 15 ;
-- 添加成功
insert into v6 values (11, tom);
-- 添加成功
insert into v6 values (17, tom);
-- 添加成功 v6没有加入检查,基于v5,满足v5条件,v5检查条件为local,基于v4,但是v4没有未指定检查选项,所以添加成功
insert into v6 values (28, tom);
10.1.4 视图的更新
要使视图可更新,视图中的行与基础表中的行之间必须存在一对一的关系。如果视图包含以下任何一项,则该视图不可更新:
聚合函数或窗口函数(sum()、min()、max()、count()等)
DISTINCT
GROUP BY
HAVING
UNION 或者UNION ALL
10.1.5 视图的作用
简单
视图不仅可以简化用户对数据的理解,也可以简化操作。那些被经常使用的查询可以被定义为视图,从而使得用户不必为以后的操作每次指定全部的条件
安全
数据库可以授权,但不能授权到数据库特定的行和列上。通过视图用户只能查询和修改他们所能见到的数据
数据独立
视图可帮助用户屏蔽真实表结构带来的影响
10.1.6 案例
为了保证数据库表的安全性,开发人员在操作tb_user表时,只能看到的用户的基本字段,屏蔽手机号和邮箱两个字段。
create view tb_user_view as select id,name,profession,age,gender,status,createtime from tb_user;
select * from tb_user_view;
查询每个学生所选修的课程(三张表联查),这个功能在很多的业务中都有使用到,为了简化操作,定义一个视图。
create view tb_stu_course_view as select s.name student_name , s.no student_no ,
c.name course_name from student s, student_course sc , course c where s.id =
sc.studentid and sc.courseid = c.id;
select * from tb_stu_course_view;
10.2 存储过程
10.2.1 介绍
存储过程是事先经过编译并存储在数据库中的一段 SQL 语句的集合,调用存储过程可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。
存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。
特点:
封装,复用:可以把某一业务SQL封装在存储过程中,需要用到的时候直接调用即可
可以结构参数,也可以返回数据:在存储过程中,可以传递参数,也可以接收返回值
减少网络交互,效率提升:如果涉及到多条SQL,每执行一次都是一次网络传输。 而如果封装在存储过程中,我们只需要网络交互一次可能就可以了。
10.2.2 语法
创建
CREATE PROCEDURE 存储过程名称 ([ 参数列表 ])
BEGIN
-- SQL语句
END ;
调用
CALL 名称 ([参数]);
查看
-- 查询指定数据库的存储过程及状态信息
SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'xxx';
-- 查询某个存储过程的定义
SHOW CREATE PROCEDURE 存储过程名称 ;
删除
DROP PROCEDURE [IF EXISTS] 存储过程名称 ;
注意:
在命令行中,执行创建存储过程的SQL时,需要通过关键字 delimiter 指定SQL语句的结束符。
10.2.3 变量
在MySQL中变量分为三种类型:系统变量、用户自定义变量、局部变量
10.2.3.1 系统变量
系统变量时MySQL服务器提供,不是用户自定义的,属于服务器层面。分为全局变量(GLOBAL)、会话变量(SESSION)
查看系统变量
-- 查看所有系统变量
SHOW [ SESSION | GLOBAL ] VARIABLES ;
- -可以通过LIKE模糊匹配方式查找变量
SHOW [ SESSION | GLOBAL ] VARIABLES LIKE '......';
-- 查看指定变量的值
SELECT @@[SESSION | GLOBAL] 系统变量名;
设置系统变量
SET [ SESSION | GLOBAL ] 系统变量名 = 值 ;
SET @@[SESSION | GLOBAL] 系统变量名 = 值 ;
注意:
如果没有指定SESSION/GLOBAL,默认是SESSION,会话变量。mysql服务重新启动之后,所设置的全局参数会失效,要想不失效,可以在 /etc/my.cnf 中配置。
全局变量(GLOBAL): 全局变量针对于所有的会话。
会话变量(SESSION): 会话变量针对于单个会话,在另外一个会话窗口就不生效了。
10.2.3.2 用户自定义变量
用户定义变量 是用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用 "@变量名" 使用就可以。其作用域为当前连接(会话)。
赋值
-- 方式一
SET @var_name = expr [, @var_name = expr] ... ;
SET @var_name := expr [, @var_name := expr] ... ;
-- 方式二
SELECT @var_name := expr [, @var_name := expr] ... ;
SELECT 字段名 INTO @var_name FROM 表名;
使用
SELECT @var_name ;
注意: 用户定义的变量无需对其进行声明或初始化,只不过获取到的值为NULL。
10.2.3.3 局部变量
局部变量 是根据需要定义的在局部生效的变量,访问之前,需要DECLARE声明。可用作存储过程内的局部变量和输入参数,局部变量的范围是在其内声明的BEGIN ... END块。
声明
DECLARE 变量名 变量类型 [DEFAULT ... ] ;
变量类型就是数据库字段类型:INT、BIGINT、CHAR、VARCHAR、DATE、TIME等。
赋值
SET 变量名 = 值 ;
SET 变量名 := 值 ;
SELECT 字段名 INTO 变量名 FROM 表名 ... ;
10.2.4 if
介绍
IF 条件1 THEN
.....
ELSEIF 条件2 THEN -- 可选
.....
ELSE -- 可选
.....
END IF;
在if条件判断的结构中,ELSE IF 结构可以有多个,也可以没有。 ELSE结构可以有,也可以没有。
案例
根据定义的分数score变量,判定当前分数对应的分数等级。
score >= 85分,等级为优秀。
score >= 60分 且 score < 85分,等级为及格。
score < 60分,等级为不及格。
create procedure p3()
begin
declare score int default 58;
declare result varchar(10);
if score > = 85 then
set result := '优秀';
elseif score > = 60 then
set result := '及格';
else
set result := '不及格';
end if;
select result;
end;
call p3();
10.2.5 参数
介绍
参数的类型主要分为一下三种:
类型 | 含义 |
IN | 该类参数作为输入,也就是需要调用时传入值(默认) |
OUT | 该类参数作为输出,也就是该参数可以作为返回值 |
INOUT | 既可以作为输入参数,也可以作为输出参数 |
语法
CREATE PROCEDURE 存储过程名称 ([IN/OUT/INOUT 参数名 参数类型], ...)
BEGIN
-- SQL语句
END ;
案例1
根据传入参数score,判定当前分数对应的分数等级,并返回。
score >= 85分,等级为优秀。
score >= 60分 且 score < 85分,等级为及格。
score < 60分,等级为不及格。
create procedure p4(in score int, out result varchar(10))
begin
if score > = 85 then
set result := '优秀';
elseif score > = 60 then
set result := '及格';
else
set result := '不及格';
end if;
end;
-- 定义用户变量 @result来接收返回的数据, 用户变量可以不用声明
call p4(68, @result);
select @result;
案例2
将传入的200分制的分数,进行换算,换算成百分制,然后返回。
create procedure p5(inout score double)
begin
set score := score * 0.5;
end;
set @score = 198;
call p5(@score);
select @score;
10.2.6 case
语法一
-- 含义: 当case_value的值为 when_value1时,执行statement_list1,当值为 when_value2时,
-- 执行statement_list2, 否则就执行 statement_list
CASE case_value
WHEN when_value1 THEN statement_list1
[ WHEN when_value2 THEN statement_list2] ...
[ ELSE statement_list ]
END CASE;
语法二
-- 含义: 当条件search_condition1成立时,执行statement_list1,当条件search_condition2成
-- 立时,执行statement_list2, 否则就执行 statement_list
CASE
WHEN search_condition1 THEN statement_list1
[WHEN search_condition2 THEN statement_list2] ...
[ELSE statement_list]
END CASE;
案例:
根据传入的月份,判定月份所属的季节(要求采用case结构)。
1-3月份,为第一季度
4-6月份,为第二季度
7-9月份,为第三季度
10-12月份,为第四季度
create procedure p6(in month int)
begin
declare result varchar(10);
case
when month >=1 and month <= 3 then
set result := '第一季度';
when month >=4 and month <= 6 then
set result := '第二季度';
when month >=7 and month <= 9 then
set result := '第三季度';
when month >=10 and month <= 12 then
set result := '第四季度';
else
set result := '非法参数';
end case;
select concat('您输入的月份为:', month, ',所属的季度为:', result);
end;
call p6(4)
10.2.7 while
介绍
while 循环是有条件的循环控制语句。满足条件后,再执行循环体中的SQL语句。具体语法为:
-- 先判定条件,如果条件为true,则执行逻辑,否则,不执行逻辑
WHILE 条件 DO
SQL逻辑...
END WHILE;
案例
-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对n进行减1 , 如果n减到0, 则退出循环
create procedure p7(in n int)
begin
declare total int default 0;
while n>0 do
set total := total + n;
set n := n - 1;
end while;
select total;
end;
call p7(100);
10.2.8 repeat
介绍
repeat是有条件的循环控制语句, 当满足until声明的条件的时候,则退出循环 。具体语法为:
-- 先执行一次逻辑,然后判定UNTIL条件是否满足,如果满足,则退出。如果不满足,则继续下一次循环
REPEAT
SQL逻辑...
UNTIL 条件
END REPEAT;
案例
-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对n进行-1 , 如果n减到0, 则退出循环
create procedure p8(in n int)
begin
declare total int default 0;
repeat
set total := total + n;
set n := n - 1;
until n < = 0
end repeat;
select total;
end;
call p8(10);
call p8(100);
10.2.9 loop
介绍
LOOP 实现简单的循环,如果不在SQL逻辑中增加退出循环的条件,可以用其来实现简单的死循环。
LOOP可以配合一下两个语句使用:
LEAVE :配合循环使用,退出循环。
ITERATE:必须用在循环中,作用是跳过当前循环剩下的语句,直接进入下一次循环。
[begin_label:] LOOP
SQL逻辑...
END LOOP [end_label];
LEAVE label; -- 退出指定标记的循环体
ITERATE label; -- 直接进入下一次循环
案例1
计算从1累加到n的值,n为传入的参数值。
-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对n进行-1 , 如果n减到0, 则退出循环 ----> leave xx
create procedure p9(in n int)
begin
declare total int default 0;
sum:loop
if n < = 0 then
leave sum;
end if;
set total := total + n;
set n := n - 1;
end loop sum;
select total;
end;
call p9(100);
案例2
计算从1到n之间的偶数累加的值,n为传入的参数值。
-- A. 定义局部变量, 记录累加之后的值;
-- B. 每循环一次, 就会对n进行-1 , 如果n减到0, 则退出循环 ----> leave xx
-- C. 如果当次累加的数据是奇数, 则直接进入下一次循环. --------> iterate xx
create procedure p10(in n int)
begin
declare total int default 0;
sum:loop
if n<=0 then
leave sum;
end if;
if n%2 = 1 then
set n := n - 1;
iterate sum;
end if;
set total := total + n;
set n := n - 1;
end loop sum;
select total;
end;
call p10(100);
10.2.10 游标
介绍
游标(CURSOR)是用来存储查询结果集的数据类型 , 在存储过程和函数中可以使用游标对结果集进行循环的处理。游标的使用包括游标的声明、OPEN、FETCH 和 CLOSE,其语法分别如下。
声明游标
DECLARE 游标名称 CURSOR FOR 查询语句 ;
打开游标
OPEN 游标名称 ;
获取游标记录
FETCH 游标名称 INTO 变量 [, 变量 ] ;
关闭游标
CLOSE 游标名称 ;
案例
根据传入的参数uage,来查询用户表tb_user中,所有的用户年龄小于等于uage的用户姓名(name)和专业(profession),并将用户的姓名和专业插入到所创建的一张新表(id,name,profession)中。
-- 逻辑:
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新表中
-- F. 关闭游标
create procedure p11(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name,profession from tb_user where age < = uage;
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
open u_cursor;
while true do
fetch u_cursor into uname,upro;
insert into tb_user_pro values (null, uname, upro);
end while;
close u_cursor;
end;
call p11(30);
上述的功能,虽然实现了,但是逻辑并不完善,而且程序执行完毕,获取不到数据,数据库还报错(while true do语句)。 接下来,我们就需要来完成这个存储过程,并且解决这个问题。
要想解决这个问题,就需要通过MySQL中提供的 条件处理程序 Handler 来解决。
10.2.11 条件处理程序
介绍
条件处理程序(Handler)可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。具体语法为:
DECLARE handler_action HANDLER FOR condition_value [, condition_value] ... statement ;
handler_action 的取值:
CONTINUE: 继续执行当前程序
EXIT: 终止执行当前程序
condition_value 的取值:
SQLSTATE sqlstate_value: 状态码,如 02000
SQLWARNING: 所有以01开头的SQLSTATE代码的简写
NOT FOUND: 所有以02开头的SQLSTATE代码的简写
SQLEXCEPTION: 所有没有被SQLWARNING 或 NOT FOUND捕获的SQLSTATE代码的简写
案例
继续来完成在上一小节提出的这个需求,并解决其中的问题。
根据传入的参数uage,来查询用户表tb_user中,所有的用户年龄小于等于uage的用户姓名(name)和专业(profession),并将用户的姓名和专业插入到所创建的一张新表(id,name,profession)中。
-- 逻辑:
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新表中
-- F. 关闭游标
create procedure p11(in uage int)
begin
declare uname varchar(100);
declare upro varchar(100);
declare u_cursor cursor for select name,profession from tb_user where age < = uage;
-- 声明条件处理程序 : 当SQL语句执行抛出的状态码为02000时,将关闭游标u_cursor,并退出
declare exit handler for SQLSTATE '02000' close u_cursor;
drop table if exists tb_user_pro;
create table if not exists tb_user_pro(
id int primary key auto_increment,
name varchar(100),
profession varchar(100)
);
open u_cursor;
while true do
fetch u_cursor into uname,upro;
insert into tb_user_pro values (null, uname, upro);
end while;
close u_cursor;
end;
call p11(30);
10.3 存储函数
介绍
存储函数是有返回值的存储过程,存储函数的参数只能是IN类型的
CREATE FUNCTION 存储函数名称 ([ 参数列表 ])
RETURNS type [characteristic ...]
BEGIN
-- SQL语句
RETURN ...;
END ;
characteristic说明:
DETERMINISTIN:相同的输入参数总是产生相同的结果
NO SQL:不包含SQL语句
READS SQL DATA:包含读取数据的语句,但不包含写入数据的语句
案例
计算从1累加到n的值,n为传入的参数值。
create function fun1(n int)
returns int deterministic
begin
declare total int default 0;
while n>0 do
set total := total + n;
set n := n - 1;
end while;
return total;
end;
select fun1(50);
在mysql8.0版本中binlog默认是开启的,一旦开启了,mysql就要求在定义存储过程时,需要指定characteristic特性,否则就会报错
10.4 触发器
10.4.1 介绍
触发器是与表有关的数据库对象,指在insert/update/delete之前(BEFORE)或之后(AFTER),触发并执行触发器中定义的SQL语句集合。触发器的这种特性可以协助应用在数据库端确保数据的完整性,日志记录,数据校验等操作。
使用别名OLD和NEW来引用触发器中发生变化的记录内容,这与其他数据库是类似的。现在触发器还只支持行级触发,不支持语句级触发
触发器类型 | NEW和OLD |
INSERT 触发器 | NEW表示将要或者已经新增的数据 |
UPDATE 触发器 | OLD表示修改之前的数据,NEW表示将要或已经修改的数据 |
DELETE 触发器 | OLD表示将要或者已经删除的数据 |
10.4.2 语法
创建
CREATE TRIGGER trigger_name
BEFORE/AFTER INSERT/UPDATE/DELETE
ON 表名 FOR EACH ROW -- 行级触发器
BEGIN
trigger_stmt;
END;
查看
SHOW TRIGGERS;
删除
-- 如果没有指定schema_name,默认为当前数据库
DROP TRIGGER [schema_name.] trigger_name;
10.4.3 案例
通过触发器记录 tb_user 表的数据变更日志,将变更日志插入到日志表user_logs中, 包含增加,修改,删除 ;
-- 插入数据触发器
create trigger tb_user_insert_trigger
after insert on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
VALUES
(null, 'insert', now(), new.id, concat('插入的数据内容为:
id=',new.id,',name=',new.name, ', phone=', NEW.phone, ', email=', NEW.email, ',
profession=', NEW.profession));
end;
-- 修改数据触发器
create trigger tb_user_update_trigger
after update on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
VALUES
(null, 'update', now(), new.id, concat('更新之前的数据:
id=',old.id,',name=',old.name, ', phone=', old.phone, ', email=', old.email, ',
profession=', old.profession,
'| 更新之后的数据:
id=',new.id,',name=',new.name, ', phone=', NEW.phone, ', email=', NEW.email, ',
profession=', NEW.profession));
end;
-- 更新5次
update tb_user set profession = '会计' where id < = 5;
-- 删除数据触发器
create trigger tb_user_delete_trigger
after delete on tb_user for each row
begin
insert into user_logs(id, operation, operate_time, operate_id, operate_params)
VALUES
(null, 'delete', now(), old.id, concat('删除之前的数据:
id=',old.id,',name=',old.name, ', phone=', old.phone, ', email=', old.email, ',
profession=', old.profession,));
end;