存储过程是在大型数据库系统中,一组为了完成特定功能的SQL语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字和参数(如果该存储过程有参数)来执行它。存储过程是数据库中的一个重要对象,任何一个设计良好的数据库应用程序都应该用到存储过程。
优点:
重复使用:存储过程可以重复使用,可以减少开发人员的工作量。
提高效率:存储过程在第一次使用的时候会编译,一次编译后以后不用再次编译,提高了效率。
减少网络流量:存储过程位于服务器上,调用的时候只需要传递存储过程的名称以及参数就可以了,因此降低了网络传输的数据量。
安全性:参数化的存储过程可以防止SQL注入,而且可以将Grant、Deny以及Revoke权限应用于存储过程
在生产环境下,可以通过直接修改存储过程的方式修改业务逻辑(或bug),而不用重启服务器。
缺点:
调试麻烦
移植性差
维护性差
CREATE DEFINER=`root`@`%` PROCEDURE `getName`(IN `uid` int,OUT my_uname VARCHAR(255)) BEGIN #Routine body goes here... select admin_name into my_uname from admin_user where id=uid; select my_uname; END
存储函数为一组存储程序,带函数名、参数,且返回一个结果集。存储函数和存储过程的结构类似,但必须有一个return 子句来返回结果。
-- 需要声明返回值类型,默认int CREATE DEFINER=`root`@`%` FUNCTION `queryChildrenAreaInfo`(areaId INT) RETURNS varchar(4000) CHARSET utf8 BEGIN DECLARE sTemp VARCHAR(4000); DECLARE sTempChd VARCHAR(4000); SET sTemp='$'; SET sTempChd = CAST(areaId AS CHAR); WHILE sTempChd IS NOT NULL DO SET sTemp= CONCAT(sTemp,',',sTempChd); SELECT GROUP_CONCAT(id) INTO sTempChd FROM personal_dept WHERE FIND_IN_SET(dept_parentId,sTempChd)>0; END WHILE; RETURN sTemp; END
MySQL创建存储函数时报错:> 1418 - This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you might want to use the less safe log_bin_trust_function_creators variable)
原因分析:
因为CREATE PROCEDURE, CREATE FUNCTION, ALTER PROCEDURE,ALTER FUNCTION,CALL, DROP PROCEDURE, DROP FUNCTION等语句都会被写进二进制日志,然后在从服务器上执行。但是,一个执行更新的不确定子程序(存储过程、函数、触发器)是不可重复的,在从服务器上执行(相对与主服务器是重复执行)可能会造成恢复的数据与原始数据不同,从服务器不同于主服务器的情况。
为了解决这个问题,MySQL强制要求:
在主服务器上,除非子程序被声明为确定性的或者不更改数据,否则创建或者替换子程序将被拒绝。这意味着当创建一个子程序的时候,必须要么声明它是确定性的,要么它不改变数据。
解决办法也有两种:
第一种是在创建子程序(存储过程、函数、触发器)时,声明为DETERMINISTIC或NO SQL与READS SQL DATA中的一个,
create function plusefunction(num1 int,num2 int) returns int DETERMINISTIC begin return num1 + num2; END
第二种是信任子程序的创建者,禁止创建、修改子程序时对SUPER权限的要求,设置log_bin_trust_routine_creators全局系统变量为1。设置方法有三种:
在客户端上执行SET GLOBAL log_bin_trust_function_creators = 1;
MySQL启动时,加上–log-bin-trust-function-creators选贤,参数设置为1
在MySQL配置文件my.ini或my.cnf中的[mysqld]段上加log-bin-trust-function-creators=1
3.1.1 返回值上的不同
存储函数将向调用者返回一个且仅有一个结果值,使用return关键字。
存储过程将返回一个或多个结果集(函数做不到这一点),或者只是来实现某种效果或动作而无需返回结果。
3.1.2 调用方式的不同
存储函数嵌入在sql中使用的,可以在select中调用,就像内置函数一样,比如cos()、sin()。
存储过程通过 CALL 语句调用。
3.1.3 参数的不同
存储函数的参数类型类似于IN参数
存储过程的参数类型有三种:IN参数、OUT参数、INOUT参数
in:数据只是从外部传入内部使用(值传递),可以是数值也可以是变量
out:只允许过程内部使用(不用外部数据),给外部使用的(引用传递:外部的数据会被先清空才会进入到内部),只能是变量
inout:外部可以在内部使用,内部修改的也可以给外部使用,典型的引用 传递,只能传递变量。
3.1.4 内容
存储过程是用户定义的一系列sql语句的集合,设计特定表或其它对象的任务,用户可以调用存储过程。
而函数通常是数据库已定义的方法,它接收参数并返回某种类型的值并且不涉及特定用户表。
存储过程的功能更加强大,包括能够执行对表的操作(比如创建表,删除表等)和事务 操作,这些功能是存储函数不具备的。
过程和函数语法基本一致
-- 官方参考网址 https://dev.mysql.com/doc/refman/5.6/en/sql-statements.html https://dev.mysql.com/doc/refman/5.6/en/sql-compound-statements.html
-- [DEFINER = user]:DEFINER用于指明存储过程是由哪个用户定义的,默认存储过程的定义者是存储过程,跟存储过程的使用权限无关 -- PROCEDURE:/prəˈsiːdʒə(r)/ -- proc_parameter参数部分:type类型可以是MySQL支持的所有类型,可以如下书写:[ IN | OUT | INOUT ] param_name type CREATE [DEFINER = user] PROCEDURE sp_name ([proc_parameter[,...]]) [characteristic ...] routine_body -- routine_body(程序体)部分,可以书写合法的SQL语句 BEGIN ... END
简单演示:
-- 声明结束符。因为MySQL默认使用‘;’作为结束符,而在存储过程中,会使用‘;’作为一段语句的结束,导致‘;’使用冲突 CREATE PROCEDURE hello_procedure(in age int) BEGIN SELECT 'hello procedure'; SELECT age; END call hello_procedure(); -- 调用存储过程
类比一下java中的局部变量和成员变量的声明和使用
用户自定义,在begin/end块中有效。DECLARE必须紧跟在BEGIN之后声明
语法: 声明变量:declare var_name type [default var_value]; 举例:(1)declare nickname varchar(32); (2)declare emp_no int default 0; -- /dɪˈkleə(r)/
-- set赋值 create procedure sp_var01() begin declare nickname varchar(32) default 'unkown'; -- default赋默认值 set nickname = 'ZS'; -- set nickname := 'SF'; select nickname; end
-- into赋值 create procedure sp_var_into() begin declare emp_name varchar(32) default 'unkown' ; declare emp_no int default 0; select e.empno,e.ename into emp_no,emp_name from emp e where e.empno = 7839; select emp_no,emp_name; end
用户自定义,当前会话(连接)有效。类比java的成员变量
语法: set @var_name = 'ZS' (不需要提前声明,使用即声明)
-- 赋值 create procedure sp_var02() begin set @nickname = 'ZS'; -- set nickname := 'SF'; end call sp_var02() select @nickname --可以看到结果
由系统提供,当前会话(连接)有效
语法: @@session.var_name
show session variables; -- 查看会话变量 select @@session.unique_checks; -- 查看某会话变量 set @@session.unique_checks = 0; --修改会话变量
由系统提供,整个mysql服务器有效
语法: @@global.var_name
举例
-- 查看全局变量中变量名有char的记录 show global variables like '%char%'; -- 查看全局变量character_set_client的值 select @@global.character_set_client;
-- 语法 in | out | inout param_name type
举例
-- IN类型,传入 create procedure sp_param01(in age int) begin set @user_age = age; end -- 测试 call sp_param01(10) $$ select @user_age$$
-- OUT类型,输出 -- 需求:输出传入的地址字符串对应的部门编号。 -- OUT类型,输出 -- 需求:输出传入的地址字符串对应的部门编号。 create procedure sp_param02(in loc varchar(64),out dept_no int(11)) begin select d.deptno into dept_no from dept d where d.loc = loc; end -- 测试 call sp_param01('DALLAS');
-- INOUT类型,传入并输出 create procedure sp_param03(inout name varchar(64)) begin set name = concat('hello' ,name); select name; end -- 测试 call sp_param03('小明');
官网说明 https://dev.mysql.com/doc/refman/5.6/en/flow-control-statements.html
if
-- 语法 IF search_condition THEN statement_list [ELSEIF search_condition THEN statement_list] ... [ELSE statement_list] END IF
举例:
-- 前置知识点:timestampdiff(unit,exp1,exp2) 取差值exp2-exp1差值,单位是unit select timestampdiff(year,e.hiredate,now()) from emp e where e.empno = '7499';
-- 需求:入职年限<=38是新手 >38并且<=40老员工 >40元老 delimiter $$ create procedure sp_hire_if() begin declare result varchar(32); if timestampdiff(year,'2001-01-01',now()) > 40 then set result = '元老'; elseif timestampdiff(year,'2001-01-01',now()) > 38 then set result = '老员工'; else set result = '新手'; end if; select result; end$$ delimiter ;
case
此语法是不仅可以用在存储过程,查询语句也可以用!
-- 语法一(类比java的switch): CASE case_value WHEN when_value THEN statement_list [WHEN when_value THEN statement_list] ... [ELSE statement_list] END CASE -- 语法二: CASE WHEN search_condition THEN statement_list [WHEN search_condition THEN statement_list] ... [ELSE statement_list] END CASE
举例:
-- 需求:入职年限年龄<=38是新手 >38并 <=40老员工 >40元老 delimiter $$ create procedure sp_hire_case() begin declare result varchar(32); declare message varchar(64); case when timestampdiff(year,'2001-01-01',now()) > 40 then set result = '元老'; set message = '老爷爷'; when timestampdiff(year,'2001-01-01',now()) > 38 then set result = '老员工'; set message = '油腻中年人'; else set result = '新手'; set message = '萌新'; end case; select result, message; end$$ delimiter ;
loop
(死循环)loop是死循环,需要手动退出循环,使用leave
(退出循环)。可以把leave看成我们java中的break
-- 语法 luːp [begin_label:] LOOP ...SQL等逻辑 END LOOP [end_label]
--需求:循环打印1到10 -- leave控制循环的退出 delimiter $$ create procedure sp_flow_loop() begin declare c_index int default 1; declare result_str varchar(256) default '1'; cnt:loop if c_index >= 10 then leave cnt; end if; set c_index = c_index + 1; set result_str = concat(result_str,',',c_index); end loop cnt; select result_str; end$$ -- iterate + leave控制循环 delimiter $$ create procedure sp_flow_loop02() begin declare c_index int default 1; declare result_str varchar(256) default '1'; cnt:loop set c_index = c_index + 1; set result_str = concat(result_str,',',c_index); if c_index < 10 then iterate cnt; end if; -- 下面这句话能否执行到?什么时候执行到? 当c_index < 10为false时执行 leave cnt; end loop cnt; select result_str; end$$
repeat
(do...while)/rɪˈpiːt/
[begin_label:] REPEAT ...SQL等逻辑 UNTIL search_condition -- 直到满足条件,退出循环 END REPEAT [end_label]
-- 需求:循环打印1到10 delimiter $$ create procedure sp_flow_repeat() begin declare c_index int default 1; -- 收集结果字符串 declare result_str varchar(256) default '1'; count_lab:repeat set c_index = c_index + 1; set result_str = concat(result_str,',',c_index); until c_index >= 10 end repeat count_lab; select result_str; end$$
while
[begin_label:] WHILE search_condition DO ...SQL等逻辑 END WHILE [end_label]
-- 需求:循环打印1到10 delimiter $$ create procedure sp_flow_while() begin declare c_index int default 1; -- 收集结果字符串 declare result_str varchar(256) default '1'; while c_index < 10 do set c_index = c_index + 1; set result_str = concat(result_str,',',c_index); end while; select result_str; end$$
leave
类比java的breake
-- 退出 LEAVE can be used within BEGIN ... END or loop constructs (LOOP, REPEAT, WHILE). LEAVE label
iterate
类比java的continue
-- 开启下一次循环 ITERATE can appear only within LOOP, REPEAT, and WHILE statements ITERATE label
用游标得到某一个结果集,逐行处理数据。
语法
在语法中,变量声明、游标声明、handler声明是必须按照先后顺序书写的,否则创建存储过程出错。
-- 声明语法 /ˈkɜːsə(r)/ DECLARE cursor_name CURSOR FOR select_statement -- 打开语法 OPEN cursor_name -- 取值语法 FETCH cursor_name INTO var_name [, var_name] ... -- 关闭语法 CLOSE cursor_name
游标示例
CREATE PROCEDURE cursor_example_multi() BEGIN -- 定义游标 DECLARE cur1 CURSOR FOR SELECT id, name FROM users WHERE age > 30; -- 定义变量 DECLARE done INT DEFAULT FALSE; DECLARE id_value INT; DECLARE name_value VARCHAR(50); -- 打开游标 OPEN cur1; -- 处理游标结果集 /fetʃ/ read_loop_1: LOOP FETCH cur1 INTO id_value, name_value; IF done THEN LEAVE read_loop_1; END IF; -- 处理当前行数据 SELECT CONCAT('cur1:', id_value, ':', name_value); END LOOP; -- 关闭游标1 CLOSE cur1; END
游标 + 句柄 示例
游标的句柄(handler)不是必须的,可以根据需要选择是否使用它。如果在游标声明时没有定义句柄,则MySQL会自动为该游标生成一个隐含的句柄,可以使用FETCH语句来遍历结果集。
-- 需求:按照部门名称查询员工,通过select查看员工的编号、姓名、薪资。(注意,此处仅仅演示游标用法) create procedure sp_create_table02(in dept_name varchar(32)) begin -- 定义局部变量 declare e_no int; declare e_name varchar(32); declare e_sal decimal(7,2); -- 句柄值 declare lp_flag boolean default true; -- 声明游标 declare emp_cursor cursor for select e.empno,e.ename,e.sal from emp e,dept d where e.deptno = d.deptno and d.dname = dept_name; -- handler,定义句柄,当数据未发现时将标记位设置为false declare continue handler for NOT FOUND set lp_flag = false; -- 打开游标 open emp_cursor; -- 循环获取游标值 emp_loop:loop -- e_no,e_name,e_sal 分别对应上 empno,ename,sal fetch emp_cursor into e_no,e_name,e_sal; -- 判断标志位,true表示有数据,false表示没有数据,结束循环。 if lp_flag then select e_no,e_name,e_sal; else leave emp_loop; end if; end loop emp_loop; -- 关闭游标 close emp_cursor; end call sp_create_table02('RESEARCH');
DECLARE handler_action HANDLER FOR condition_value [, condition_value] ... statement handler_action: { CONTINUE | EXIT | UNDO } condition_value: { mysql_error_code | SQLSTATE [VALUE] sqlstate_value | condition_name | SQLWARNING | NOT FOUND | SQLEXCEPTION } CONTINUE: Execution of the current program continues. EXIT: Execution terminates for the BEGIN ... END compound statement in which the handler is declared. This is true even if the condition occurs in an inner block. SQLWARNING: Shorthand for the class of SQLSTATE values that begin with '01'. NOT FOUND: Shorthand for the class of SQLSTATE values that begin with '02'. SQLEXCEPTION: Shorthand for the class of SQLSTATE values that do not begin with '00', '01', or '02'.
-- 各种写法: DECLARE exit HANDLER FOR SQLSTATE '42S01' set @res_table = 'EXISTS'; DECLARE continue HANDLER FOR 1050 set @res_table = 'EXISTS'; DECLARE continue HANDLER FOR not found set @res_table = 'EXISTS';
——大家注意,存储过程的业务过程在java代码中一般也可以实现,我们下面的需求是为了练习存储过程
为某部门(需指定)的人员涨薪100;如果是公司总裁,则不涨薪。
delimiter // create procedure high_sal(in dept_name varchar(32)) begin declare e_no int; declare e_name varchar(32); declare e_sal decimal(7,2); declare lp_flag boolean default true; declare emp_cursor cursor for select e.empno,e.ename,e.sal from emp e,dept d where e.deptno = d.deptno and d.dname = dept_name; -- handler 句柄 declare continue handler for NOT FOUND set lp_flag = false; open emp_cursor; emp_loop:loop fetch emp_cursor into e_no,e_name,e_sal; if lp_flag then if e_name = 'king' then iterate emp_loop; else update emp e set e.sal = e.sal + 100 where e.empno = e_no; end if; else leave emp_loop; end if; end loop emp_loop; set @end_falg = 'exit_flag'; close emp_cursor; end call high_sal('ACCOUNTING');
创建下个月的每天对应的表comp_2020_04_01、comp_2020_04_02、... (模拟)需求描述: 我们需要用某个表记录很多数据,比如记录某某用户的搜索、购买行为(注意,此处是假设用数据库保存),当每天记录较多时,如果把所有数据都记录到一张表中太庞大,需要分表,我们的要求是,每天一张表,存当天的统计数据,就要求提前生产这些表——每月月底创建下一个月每天的表!
-- 知识点 预处理 prepare语句from后使用局部变量会报错 -- https://dev.mysql.com/doc/refman/5.6/en/sql-prepared-statements.html PREPARE stmt_name FROM preparable_stmt EXECUTE stmt_name [USING @var_name [, @var_name] ...] {DEALLOCATE | DROP} PREPARE stmt_name -- 知识点 时间的处理 -- EXTRACT(unit FROM date)截取时间的指定位置值 -- DATE_ADD(date,INTERVAL expr unit) 日期运算 -- LAST_DAY(date) 获取日期的最后一天 -- YEAR(date) 返回日期中的年 -- MONTH(date) 返回日期的月 -- DAYOFMONTH(date) 返回日
-- 思路:循环构建表名 comp_2020_05_01 到 comp_2020_05_31;并执行create语句。 create procedure sp_create_table() begin declare next_year int; declare next_month int; declare next_month_day int; declare next_month_str char(2); declare next_month_day_str char(2); -- 处理每天的表名 declare table_name_str char(10); declare t_index int default 1; -- declare create_table_sql varchar(200); -- 获取下个月的年份 set next_year = year(date_add(now(),INTERVAL 1 month)); -- 获取下个月是几月 set next_month = month(date_add(now(),INTERVAL 1 month)); -- 下个月最后一天是几号 set next_month_day = dayofmonth(LAST_DAY(date_add(now(),INTERVAL 1 month))); if next_month < 10 then set next_month_str = concat('0',next_month); else set next_month_str = concat('',next_month); end if; while t_index <= next_month_day do if (t_index < 10) then set next_month_day_str = concat('0',t_index); else set next_month_day_str = concat('',t_index); end if; -- 2020_05_01 set table_name_str = concat(next_year,'_',next_month_str,'_',next_month_day_str); -- 拼接create sql语句 set @create_table_sql = concat( 'create table comp_', table_name_str, '(`grade` INT(11) NULL,`losal` INT(11) NULL,`hisal` INT(11) NULL) COLLATE=\'utf8_general_ci\' ENGINE=InnoDB'); -- FROM后面不能使用局部变量! prepare create_table_stmt FROM @create_table_sql; execute create_table_stmt; DEALLOCATE prepare create_table_stmt; set t_index = t_index + 1; end while; end call sp_create_table()
“为用户添加购物积分,并更新到用户的总积分表中”等需要对多张表进行CRUD操作的业务。 而且内部可以使用事务命令。
characteristic: COMMENT 'string' | LANGUAGE SQL | [NOT] DETERMINISTIC | { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA } | SQL SECURITY { DEFINER | INVOKER }
其中,SQL SECURITY的含义如下:
MySQL存储过程是通过指定SQL SECURITY子句指定执行存储过程的实际用户; 如果SQL SECURITY子句指定为DEFINER(定义者),存储过程将使用存储过程的DEFINER执行存储过程,验证调用存储 过程的用户是否具有存储过程的execute权限和DEFINER用户是否具有存储过程引用的相关对象(存储过程中的表等对象)的权限; 如果SQL SECURITY子句指定为INVOKER(调用者),那么MySQL将使用当前调用存储过程的用户执行此过程,并验证用户是否具有存储过程的execute权限和存储过程引用的相关对象的权限; 如果不显示的指定SQL SECURITY子句,MySQL默认将以DEFINER执行存储过程。
-- 如有死循环处理,可以通过下面的命令查看并结束 show processlist; kill id;
https://dev.mysql.com/doc/refman/5.6/en/control-flow-functions.html
select case when timestampdiff(year,e.hiredate,now()) <= 38 then '新手' when timestampdiff(year,e.hiredate,now()) <= 40 then '老员工' else '元老' end hir_loc, e.* from emp e;
create temporary table 表名( 字段名 类型 [约束], name varchar(20) )Engine=InnoDB default charset utf8; -- 需求:按照部门名称查询员工,通过select查看员工的编号、姓名、薪资。(注意,此处仅仅演示游标用法) delimiter $$ create procedure sp_create_table02(in dept_name varchar(32)) begin declare emp_no int; declare emp_name varchar(32); declare emp_sal decimal(7,2); declare exit_flag int default 0; declare emp_cursor cursor for select e.empno,e.ename,e.sal from emp e inner join dept d on e.deptno = d.deptno where d.dname = dept_name; declare continue handler for not found set exit_flag = 1; -- 创建临时表收集数据 CREATE temporary TABLE `temp_table_emp` ( `empno` INT(11) NOT NULL COMMENT '员工编号', `ename` VARCHAR(32) NULL COMMENT '员工姓名' COLLATE 'utf8_general_ci', `sal` DECIMAL(7,2) NOT NULL DEFAULT '0.00' COMMENT '薪资', PRIMARY KEY (`empno`) USING BTREE ) COLLATE='utf8_general_ci' ENGINE=InnoDB; open emp_cursor; c_loop:loop fetch emp_cursor into emp_no,emp_name,emp_sal; if exit_flag != 1 then insert into temp_table_emp values(emp_no,emp_name,emp_sal); else leave c_loop; end if; end loop c_loop; select * from temp_table_emp; select @sex_res; -- 仅仅是看一下会不会执行到 close emp_cursor; end$$ call sp_create_table02('RESEARCH');