存储程序是预先在数据库服务器端存储 SQL 命令/语句,并且过后能够在数据库服务器端被执行的数据库对象。
存储程序的分类
存储例程是存储在服务器端的 SQL 语句的集合,能够用存储例程的名字重用相应的语句代码。
经常用于提高效率和安全性:
(1)减少在 MySQL 服务器和 MySQL 客户端的数据传输
(2)对存储例程的授权管理更易于结合应用系统安全性
(3)存储例程中也是一个很好的记录日志的地方
包含过程和函数:PROCEDURE 和 FUNCTION
存储过程也是能“返回”值甚至结果集的
触发器是数据库中的命名对象,与一个表关联,并且在该表上的 INSERT,UPDATE,DELETE 等更改操作前/后被触发。
定义触发器
(1)WHERE:在哪张表上
(2)WHEN:由什么操作触发
(3)WHAT:被触发时所要执行的 SQL 命令
触发器的典型应用
事件是指在 MySQL 事件调度器的调度下,在特定的时刻所执行的任务,因此也称调度事件
定义事件的要素
事件典型应用
存储例程是存储在服务器端的 SQL 语句的集合,利用存储例程的名字可重用相应的语句代码。存储过程是一种存储例程。
example:创建一个存储过程,用于备份表纪律到备份表中。
create procedure backup()
insert into t_bak select * from t;
mysql> call backup();
Query OK, 2 rows affected (0.10 sec)
example:备份表记录到备份表后,删除原表的记录
delimiter $$
create procedure backup2()
begin
insert into t_bak select * from t;
delete from t;
end $$
delimiter ;
存储过程的参数类型可以是 MySQL 的有效数据类型。存储过程的参数有 IN,OUT 和 INOUT 三种模式。
存储过程的 IN 参数
例如,要求存储过程备份那些关键字段值小于给定值的记录。IN 可以省略书写。
delimiter $$
create procedure backup3(n int)
begin
insert into t_bak select * from t where id <= n;
delete from t where id <= n;
end $$
delimiter ;
mysql> call backup3(108);
Query OK, 1 row affected (0.10 sec)
存储过程的 OUT 参数
example:修改前述存储过程,使之传回本次备份的记录数。
delimiter $$
create procedure backup4(n int, out record_count int)
begin
insert into t_bak select * from t where id <= n;
select count(*) into record_count from t where id <= n;
delete from t where id <= n;
end $$
delimiter ;
mysql> set @rec_count = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> call backup4(108, @rec_count);
Query OK, 1 row affected (0.12 sec)
mysql> select @rec_count;
+------------+
| @rec_count |
+------------+
| 1 |
+------------+
1 row in set (0.00 sec)
存储过程中允许使用 SELECT 等语句返回结果集
delimiter $$
create procedure backup5(n int)
begin
insert into t_bak select * from t where id <= n;
select * from t where id <= n;
delete from t where id <= n;
end $$
delimiter ;
mysql> call backup5(108);
+-----+--------------+
| id | name |
+-----+--------------+
| 101 | 机械工程学院 |
+-----+--------------+
1 row in set (0.08 sec)
Query OK, 1 row affected (0.10 sec)
执行存储过程中的安全上下文
有权执行某个存储过程中的用户在执行存储过程时,存储过程内的 SQL 语句的执行,按定义者(默认)或调用者的权限进行检查。
谁是调用者,谁是定义者
调用者:执行 CALL 语句的用户
定义者:默认是执行 CREATE PROCEDURE 语句的用户,也可以用 DEFINER 子句指定另外的用户名
定义者和调用者安全上下文比较
试想:用户(调用者)执行某存储过程,且存储过程中读写某个表,存储过程的调用者没有这个表的读或写权限;存储过程的定义者拥有这个表的读或写权限。则定义者权限模式正常执行,调用者模式导致权限错误。
结论:定义者安全上下文有利于保证用户只能通过存储过程对数据进行有限的访问。
用 CREATE FUNCTION 语句创建函数
CREATE FUNCTION sp_name([func_parameter[,...]])
RETURNS type // 存储函数必须说明返回值类型
routine_body // 存储函数必须用 RETURN 返回值
func_parameter.param_name type // 参数均视为 IN 参数
调用函数的方式
不需要像存储过程那样使用 CALL 语句,直接使用存储函数名带参数的形式,存储函数的调用可以出现在很多地方。
example:
create function hello(name char(20))
returns char(50)
return concat('Hello,', name, '!');
mysql> select hello('MySQL');
+----------------+
| hello('MySQL') |
+----------------+
| Hello,MySQL! |
+----------------+
1 row in set (0.00 sec)
功能相同的存储过程和存储函数
例如:用存储函数的方式实现前面实现过的存储过程。
delimiter $$
create procedure backup4(n int, out record_count int)
begin
insert into t_bak select * from t where id <= n;
select count(*) into record_count from t where id <= n;
delete from t where id <= n;
end $$
delimiter ;
将存储过程改写成存储程序如下:
delimiter $$
create function backupf(n int)
returns int
begin
declare record_count int;
insert into t_bak select * from t where id <= n;
select count(*) into record_count from t where id <= n;
delete from t where id <= n;
return record_count;
end $$
delimiter ;
向调用程序传回数据的方式不同:
存储过程借助 OUT 参数和返回结果集的方式
存储函数使用 return 返回值的方式
调用方式不同:
存储函数比存储过程的人调用更为灵活
主体内允许的 SQL 不同:
存储过程中的主体中可以使用绝大多数 SQL
存储函数受较多的限制,例如不允许包含常规 SELECT 语句
存储例程特性的说明
characteristic:
| [NOT] DETERMINISTIC
| {NO SQL | CONTAINS SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
存储例程的确定性
[NOT] DETERMINISTIC,确定性,即给定同样的输入参数是否总是产生相同的结果。
例如 MySQL 内置函数 NOW() 就是不确定的,不同的时间会得到不同的输出,而 hello() 则是确定的,对应确定的参数有确定的结果。
默认选项是 NOT DETERMINISTIC。该选项会对优化产生影响。例如,执行 DETERMINISTIC 时 MySQL 可能使用缓冲等优化手段。
存储例程特性的数据访问特性
NO SQL | CONTAINS SQL | READS SQL DATA | MODIFIES SQL DATA,说明存储例程的数据访问特性,默认选项 CONTAINS SQL。
NO SQL 指示存储例程不含括 SQL 语句;
CONTAINS SQL 指示该存储例程不包含读或写数据的 SQL 语句;
READS SQL DATA 指示存储例程包括读数据的语句(例如 SELECT)但不包括写数据的语句;
MODIFIES SQL DATA 指示存储例程包含写数据的语句(例如 INSERT)。
查看存储例程的状态
example:
mysql> SHOW PROCEDURE STATUS LIKE 'backup'\G # \G表示竖排显示
*************************** 1. row ***************************
Db: db
Name: backup
Type: PROCEDURE
Definer: root@localhost
Modified: 2020-05-06 16:26:19
Created: 2020-05-06 16:26:19
Security_type: DEFINER
Comment:
character_set_client: gbk
collation_connection: gbk_chinese_ci
Database Collation: utf8_general_ci
1 row in set (0.00 sec)
查看存储例程的定义
mysql> SHOW CREATE PROCEDURE backup\G
*************************** 1. row ***************************
Procedure: backup
sql_mode: STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `backup`()
insert into t_bak select * from t
character_set_client: gbk
collation_connection: gbk_chinese_ci
Database Collation: utf8_general_ci
1 row in set (0.00 sec)
mysql> DROP PROCEDURE backup;
Query OK, 0 rows affected (0.13 sec)
mysql> DROP PROCEDURE backup;
ERROR 1305 (42000): PROCEDURE db.backup does not exist
mysql> DROP PROCEDURE IF EXISTS backup;
Query OK, 0 rows affected, 1 warning (0.00 sec)
删除并创新创建存储程序,需要先 DROP 后 CREATE
mysql> drop procedure if exists backup;
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> create procedure backup()
-> insert into t_bak select * from t;
Query OK, 0 rows affected (0.00 sec)
characteristic:
COMMENT ‘string’
| LANGUAGE SQL
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
alter procedure backup
-> comment '备份表t到t_bak'
-> modifies sql data;
Query OK, 0 rows affected (0.05 sec)
用 GRANT 语句授权用户执行存储例程
用户执行存储例程需要存储例程对象上的 EXECUTE 权限
GRANT EXECUTE ON [{ PROCEDURE | FUNCTION}]
{ *.* | db_name.* | db_name.routine_name } TO user
example:
GRANT EXECUTE ON course.* TO 'user1';
GRANT EXECUTE ON PROCEDURE course.backup TO 'user2';
存储程序中对结果集每行记录依次处理,需要使用游标(CURSOR)。
游标的作用
游标的使用
example:计算学生平均绩点
把学生所选的每门课程的百分制成绩折算成绩点(例如按五级制 A/B/C/D/F 分别折算 4/3/2/1/0),将课程学分和课程绩点相乘,得到课程绩点,除以总的课程学生即平均绩点。
delimiter $$
create function gpa(stu_id char(12))
returns decimal(3, 2)
begin
declare score, credit, points, total_credit, total_points int default 0;
declare done int default FALSE;
declare cursor_choose_course cursor for
select choose.score, course.credit from choose
join course on choose.Course_id = course.Course_id
where choose.Student_id = stu_id and choose.score IS NOT NULL;
declare continue handler for not found set done = TRUE;
open cursor_choose_course;
loop_cursor: loop
fetch cursor_choose_course into score, credit;
if done then leave loop_cursor;
end if;
set total_credit = total_credit + credit;
if score >= 90 then set points = 4;
elseif score >= 80 then set points = 3;
elseif score >= 70 then set points = 2;
elseif score >= 60 then set points = 1;
else set points = 0;
end if;
set total_points = total_points + points * credit;
end loop;
close cursor_choose_course;
return if (total_credit > 0, total_points/total_credit, 0);
end $$
delimiter ;
mysql> select gpa('M20177001');
+------------------+
| gpa('M20177001') |
+------------------+
| 4.00 |
+------------------+
1 row in set (0.41 sec)
MySQL 默认在每一条 SQL 语句执行后都自动提交(事务),也允许在存储过程中使用显式地事务控制。
事务控制原则
常用的事务控制语句
事务经典案例:银行账户转账
对事务的要求
delimiter $$
create procedure transfer(account_from int, account_to int, amount int, OUT status int)
modifies sql data
begin
declare account_from_balance int;
declare exit handler for sqlexception begin rollback;
set status = -1;
end;
start transaction;
update bank_account set Balance = Balance + amount where Account_Id = account_to;
select balance into account_from_balance from bank_account where Account_Id = account_from;
if account_from_balance < amount then
rollback;
set status = -1;
else
update bank_account set Balance = Balance - amount where Account_Id = account_from;
commit;
set status = 0;
end if;
end $$
delimiter ;
初始情况
+------------+---------+
| Account_Id | Balance |
+------------+---------+
| 1 | 500 |
| 2 | 1000 |
+------------+---------+
案例运行:一次成功的转账业务,账户1向账户2转账100元。
mysql> set @status = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> source E:\sql\test.sql # 用脚本执行,脚本中内容就是上面的代码
Query OK, 0 rows affected (0.00 sec)
mysql> call transfer(1, 2, 100, @status);
Query OK, 0 rows affected (0.10 sec)
mysql> select @status;
+---------+
| @status |
+---------+
| 0 |
+---------+
1 row in set (0.00 sec)
mysql> select * from bank_account;
+------------+---------+
| Account_Id | Balance |
+------------+---------+
| 1 | 400 |
| 2 | 1100 |
+------------+---------+
2 rows in set (0.00 sec)
案例运行:一次失败的转账业务,账户1向账户2转账1000元。
mysql> SET @status = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> CALL transfer(1, 2, 1000, @status);
Query OK, 0 rows affected (0.11 sec)
mysql> SELECT @status;
+---------+
| @status |
+---------+
| -1 |
+---------+
1 row in set (0.00 sec)
mysql> select * from bank_account;
+------------+---------+
| Account_Id | Balance |
+------------+---------+
| 1 | 500 |
| 2 | 1000 |
+------------+---------+
2 rows in set (0.00 sec)
触发器是数据库中的命名对象,与一个表关联,并且在该表上的 INSERT,UPDATE,DELETE 等更改操作前/后被触发。
触发器的典型应用
让表上的增删改操作接受一定的强制性“规则”
使用 CREATE TRIGGER 语句创建触发
CREATE TRIGGER trigger_name // 创建触发器
{ BEFORE | AFTER } { INSERT | UPDATE | DELETE } // 触发的时机和事件
ON tbl_name FOR EACH ROW // 触发器关联的表
trigger_body // 触发器主体定义
example:
触发器示例1:将无效成绩“舍入”到有效成绩
delimiter $$
create trigger valid_score_before_update_choose
before update
on choose for each row
begin
if New.score < 0 then set New.score = 0;
elseif New.score > 100 then set New.score = 100;
end if;
end $$
delimiter ;
mysql> update choose set score = 102 where Student_id = 'M20177006';
Query OK, 1 row affected (0.11 sec)
Rows matched: 1 Changed: 1 Warnings: 0
查看触发器列表
使用 DROP TRIGGER 语句删除触发器
example:
mysql> drop trigger if exists valid_score_before_update_choose;
Query OK, 0 rows affected (0.00 sec)
注意:
事件是指在 MySQL 事件调度器的调度下,在特定的时刻所执行的任务,因此也称为调度事件。
(一)事件调度器配置
事件也是一种存储程序,也有和其它存储程序相似的属性:
(二)调度的时间和周期
CREATE EVENT event_name // 创建事件
ON SCHEDULE // 调度事件的时机
{ AT time_spec // 一次性事件的时刻
| EVERY interval [STARTS time_spec] [ENDS time_spec] } // 重复事件的周期和始终
[ ON COMPLETION [NOT] PRESERVE ] // 完成后是否保留
[ ENABLE | DISABLE ] // 创建时启用还是禁用
DO event_body; // 事件要执行的
example:
示例 1:定义一个一次性事件,在一分钟后备份表。
CREATE EVENT event_backup
ON SCHEDULE
AT CURRENT_TIMESTAMP + INTERVAL 1 MINUTE
DO INSERT INTO t_bak SELECT * FROM t;
说明:
示例 2:定义一个重复性事件,在每天 1 点定时 备份表。
CREATE EVENT daily_backup
ON SCHEDULE
EVERY 1 DAY STARTS CURRENT_DATE + INTERVAL 1 HOUR
DO INSERT INTO t_bak SELECT * FROM t;
说明:
使用 ALTER EVENT 语句
alter event daily_backup
on schedule
every 1 week starts current_date + interval 2 hour
rename to weekly_backup;
使用 DROP EVENT 语句删除事件
example:
DROP EVENT IF EXISTS weekly_backup;