什么是存储过程和函数
存储过程和函数是事先经过编译并存储在数据库中的一段SQL语句的集合,调用存储过程和函数可以简化应用开发人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。
存储过程和函数的区别在于函数必须有返回值,而存储过程没有,存储过程的参数可以使用IN、OUT、INOUT类型,而函数的参数只能是IN类型的。如果有函数从其他类型的数据库迁移到MySQL,那么就可能因此需要将函数改造成存储过程。
为什么使用存储过程和函数
在完成一个逻辑操作时,有时会执行多条SQL语句,此外这些SQL语句的执行顺序也不是固定的,它会根据条件的变化而变化。在执行过程中,这些需要根据前面SQL语句的执行结果有选择的执行后面的SQL语句。为了解决该问题,MySQL软件提供了数据库对象存储过程和函数。
存储过程和函数的优点
<1> 存储过程和函数允许标准组件式编程提高了SQL语句的重用性、共享性和可移植性。
<2> 存储过程和函数能够实现较快的执行速度,减少网络流量。
<3> 存储过程和函数可以被作为一种安全机制来利用
存储过程和函数的缺点
<1> 存储过程和函数的编写比单句SQL语句复杂,需要用户具有更高的技能和更丰富的经验。
<2> 在编写存储过程和函数时,需要创建这些数据库对象的权限。
存储过程和函数的相关操作
创建、修改存储过程或者函数
创建、修改存储过程或者函数的语法如下:
CREATE
[DEFINER = { user | CURRENT_USER }]
PROCEDURE sp_name ([proc_parameter[,...]])
[characteristic ...] routine_body
CREATE
[DEFINER = { user | CURRENT_USER }]
FUNCTION sp_name ([func_parameter[,...]])
RETURNS type
[characteristic ...] routine_body
proc_parameter:[IN|OUT|INOUT]param_name type
func_parameter:param_name type
type:Any valid MySQL data type(支持MySQL中的所有数据类型)
调用语法:
CALL sp_name([parameter[,…]])
MySQL的存储过程或者函数中允许包含DDL语句,也允许在存储过程中执行提交(Commit,即确认之前的修改)或者回滚(Rollback,即放弃之前的修改),但是存储过程或者函数中不允许执行LOAD DATA INFILE语句。此外,存储过程和函数中可以调用其他的过程或者函数。
示例1:在员工姓名模糊查询时,可能需要匹配不同的查询条件,有时需要查询名字中带有“李”的,有时需要查询以“梅”结尾的,此时可以创建一个存储过程根据不同的传入参数,返回不同的结果:
mysql>DELIMITER $$
->CREATE PROCEDURE emp_in_hire(IN emp_name_reg VARCHAR(10),OUT emp_count INT)
->READS SQL DATA
->BEGIN
->SELECT id FROM emp WHERE ename REGEXP emp_name_reg;
->SELECT FOUND_ROWS() INTO emp_count;
->END $$
mysql>DELIMITER ;
mysql>CALL emp_in_hire ('^李',@emp_count);
+----+
| id |
+----+
| 2 |
+----+
1 row in set (0.00 sec)
Query OK, 1 row affected (0.00 sec)
mysql>SELECT @emp_count;
+------------+
| @emp_count |
+------------+
| 1 |
+------------+
1 row in set (0.00 sec)
示例中可以根据传入的第一个参数指定员工姓名匹配的正则表达式,第二个参数用来接收存储过程的输出参数。达到了一段SQL多次使用的目的,不用在业务逻辑中多次编写SQL语句。
但是在进行上面的例子中可以这样尝试一下,将IN类型的参数emp_name_reg 的类型定义为VARCHAR,也就是不指定长度,这样的情况在MySQL5.6版本中会创建失败。但是INT类型的参数就不需要指定长度。使用VARCHAR指定传入参数的类型时,没有指定长度,创建时总是提示检查语法。
还有需要注意的是,在参数命名时,存储过程的参数名不要跟字段名一样。将会导致参数的值将不会是传入的值,而是变成每条记录中对应字段的值。这样的后果,是非常严重的。比如执行单个删除操作:
DELIMITER $$
CREATE PROCEDURE delete_one_emp(IN ename VARCHAR(10))
BEGIN
DELETE FROM EMP WHERE ename=ename;
END $$
调用上面的存储过程时,传入’李四’,意图是删除姓名为李四的员工信息,但是执行后发现,整张表的数据被清空了,即便是字段名是大写,参数名是小写也会如此,因为MySQL对大小写不敏感。
示例2:创建函数将指定员工的工资增加指定的值,然后返回工资的最大值。
DELIMITER $$
CREATE FUNCTION add_salary(raise_count int(5),e_name_reg varchar(10))
RETURNS int(10)
COMMENT ’涨工资’
BEGIN
UPDATE EMP SET SALARY = SALARY+raise_count WHERE ENAME RLIKE e_name_reg;
RETURN (SELECT max(SALARY) FROM EMP);
END $$
DELIMITER;
SELECT add_salary(5000,'^李');
存储过程和函数的CREATE语法不支持使用CREATEOR REPLACE对存储过程和函数进行修改,如果需要对已有的存储过程或者函数进行修改需要执行ALTER语法。
在存储过程和函数的创建语法中有characteristic特性,下面对该特性值的部分进行说明。
<1> LANGUAGE SQL:说明下面过程的BODY是使用SQL语言编写,这条是系统默认的,为以后除SQL外的其他语言支持的存储过程而准备。
<2> [NOT]DETERMINISTIC:DETERMINISTIC确定的,即每次输入一样输出也一样的程序,NOT DETERMINISTIC非确定的,默认的是非确定的。当前,这个特征值还没有被优化程序使用。
<3> {CONTAINS SQL|NOT SQL|READS SQL DATA|MODIFIES SQL DATA}:这些特征值提供子程序使用数据的内在信息,这些特征值目前只是提供给服务器,并没有根据这些特征值来约束过程实际使用数据的情况。CATAINS SQL表示子程序不包含读或写的语句。NOT SQL表示子程序不包含SQL语句。READS SQL DATA表示子程序包含读数据的语句,但是不包含写数据的语句。MODIFIES SQL DATA表示子程序包含写数据的语句。如果这些特征值没有明确给定,默认使用的值CONTAINS SQL。
<4> SQL SECURITY{DEFINER|INVOKER}:可以用来指定子程序该用创建子程序者的许可来执行,还是使用调用者的许可来执行。默认是DEFINER。
<5> COMMENT :存储过程或者函数的注释信息。
删除存储过程或者函数
一次只能删除一个存储过程或者函数,删除存储过程或者函数需要有该过程或者函数的ALTER ROUTINE权限。语法如下:
DROP{PROCEDURE|FUNCTION} [IF EXISTS] sp_name
查看存储过程或者函数
查看存储过程或者函数的状态
SHOW {PROCEDURE|FUNCTION} STATUS [LIKE ‘pattern’]
查看存储过程或者函数的定义
SHOW CREATE {PROCEDURE|FUNCTION} sp_name
通过查看information_schema.Routines了解存储过程和函数的信息
除了以上两种方法,还可以查看系统表来了解存储过程和函数的相关信息,通过查看information_schema.Routines就可以获得存储过程和函数的名称、类型、语法、创建人等信息。
变量的使用
存储过程和函数中可以使用变量。
变量的定义
通过DECLARE可以定义一个局部变量,该变量的作用范围只能在BEGIN…END块中,可以用在嵌套的块中。变量的定义必须写在复合语句的开头,并且在任何其他语句的前面。可以一次声明多个相同类型的变量。如果需要,可以使用DEFAULT赋默认值。
定义一个变量的语法如下:
DECLAREvar_name[,…] type [DEFAULT value]
例如,定义一个DATE类型的变量,名称是last_month_start:
DECLARElast_month_start DATE;
变量的赋值
变量可以直接赋值,或者通过查询赋值。直接赋值使用SET,可以赋常量或者赋表达式,具体语法如下:
SET var_name =expr[,var_name=expr]…
给刚才定义的变量last_month_start赋值,具体语法如下:
SETlast_month_start = DATE_SUB(CURRENT_DATE(),INTERVAL 1 MONTH);
也可以通过查询结果将结果赋给变量,这要求查询返回的结果必须只有一行,具体语法如下:
SELECTcol_name[,…] INTO var_name[,…] table_expr
示例3:通过查询将结果赋值给变量max_salary。
DELIMITER $$
CREATE FUNCTIONget_max_salary(param_deptno INT)
RETURNS INT(7)
DETERMINISTIC
READS SQL DATA
BEGIN
DECLAREv_max_salary INT(7);
SELECT IFNULL(MAX(SALARY),0)INTO v_max_salary
FROM EMP
WHEREDEPTNO=param_deptno;
RETURNv_max_salary;
END$$
DELIMITER ;
存储过程和函数中的异常及处理
在看到的资料书中都将本小节命名为“条件的定义和处理”。虽然在帮助文档或是语法中使用CONDITION,可以翻译为“条件”,但是,感觉“状态”更为贴切一些。“条件”很容易和“条件语句”之类的混淆。
状态的定义
本小节将直接翻译帮助文档,翻译不正确或不地道的地方还请见谅。
http://dev.mysql.com/doc/refman/5.6/en/declare-condition.html
DECLARE condition_name CONDITION FOR condition_value
condition_value:
mysql_error_code
|SQLSTATE [VALUE] sqlstate_value
DECLARE ...CONDITION语句为指定的状态定义一个名称,该名称所代表的状态可以在DECLARE ... HANDLER中进行处理。
状态的声明必须在游标(cursor)或是处理程序(handler)之前。
condition_value表示与状态名称(condition_name)关联的特定条件或条件类型。它可以是以下几种类型:
<1> mysql_error_code: 表示MySQL状态码的整数。
不要使用状态码0,0代表正常状态而不是异常状态,MySQL状态码列表参考:
Section B.3, “ServerError Codes and Messages”
<2> SQLSTATE [VALUE] sqlstate_value:五个字符的字符串,表示SQL状态。
不要使用以“00”开头的状态值,以“00”开头的状态值代表正常状态而不是异常状态。参考同上。
声明状态时,若使用伪装错误信息(SIGNAL或RESIGNAL)时必须使用状态值而不能使用状态码(此规定的原因是伪装信息的定义中不支持为其指定状态码)。
为状态声明一个名称可以使代码更加清晰(如果不看列表,很难知道状态码1146是什么意思,在上面的连接中能够看到解释,1146 ;ER_NO_SUCH_TABLE ;table ‘%s.%s’ does’t exist,表不存在),假如,处理程序试图删除一张不存在的表,只有当你知道1051代表的是“unknown table”时你才明白错误的原因。
通过为状态码指定一个名称,才使得处理程序的目的更加明了。
状态的处理定义
DECLARE handler_action HANDLER FORcondition_value[,…] sp_statement
handler_ action:
CONTINUE|EXIT|UNDO
condition_value:
SQLSTATE[VALUE] sqlstate_value
|condition_name
|SQLWARNING
|NOT FOUND
|SQLEXCEPTION
|mysql_error_code
示例4:通过不指定与指定状态处理程序的不同处理熟悉状态处理程序的使用
<1> 向emp(id,ename,deptno)表中连续插入两条记录(1,’张三’,1),(1,’李四’,1)没有处理程序时,id重复的执行结果如下:
DELIMITER $$
CREATE PROCEDURE insert_emp()
BEGIN
SET @x=1;
INSERT INTO emp(id,ename,deptno) VALUES(1,'张三',1);
SET @X=2;
INSERT INTO emp(id,ename,deptno) VALUES(1,'李四',1);
SET @X=3;
END $$
DELIMITER ;
CALL insert_emp();
ERROR 1062 (23000): Duplicate entry '1' forkey 'PRIMARY'
SELECT @X;
->2
从最后的执行结果可以看出,执行到插入emp.ename=李四的记录时,会主键重复并退出,并没有执行到下面的语句。
<1> 向emp(id,ename,deptno)表中连续插入两条记录(1,’张三’,1),(1,’李四’,1)使用处理程序时,id重复的执行结果如下,我们在处理程序加上输出:
DELIMITER $$
CREATE PROCEDURE insert_emp()
BEGIN
DECLAREduplicate_entry CONDITION FOR SQLSTATE '23000';
DECLARECONTINUE HANDLER FOR duplicate_entry
BEGIN
SELECT'遇到异常继续执行';
END;
SET@x=1;
INSERTINTO emp(id,ename,deptno) VALUES(1,'张三',1);
SET@X=2;
INSERTINTO emp(id,ename,deptno) VALUES(1,'李四',1);
SET@X=3;
END
<3> 使用光标
FETCH cursor_name INTO var_name[,var_name]…
<4> 关闭光标
CLOSE cursor_name
示例5:对emp表按照行进行循环的处理,按照deptno的不同累加salary的值,判断循环结束的条件是NOT FOUND的状态,当FETCH光标找不到下一条记录的时候,就会关闭光标然后退出程序。
DELIMITER $$
CREATE PROCEDURE dept_salary_sum()
BEGIN
DECLAREi_dept_no int;
DECLAREi_salary int(5);
DECLAREcur_emp CURSOR FOR SELECT deptno,salary FROM emp;
DECLAREEXIT HANDLER FOR NOT FOUND CLOSE cur_emp;
SET@salary_sum_dept1=0;
SET@salary_sum_dept2=0;
OPENcur_emp;
REPEAT
FETCHcur_emp INTO i_dept_no,i_salary;
IFi_dept_no=1 THEN
SET@salary_sum_dept1=@salary_sum_dept1+i_salary;
ELSE
SET@salary_sum_dept2=@salary_sum_dept2+i_salary;
ENDIF;
UNTIL0 END REPEAT;
CLOSEcur_emp;
END $$
DELIMITER ;
CALL dept_salary_sum();
SELECT @salary_sum_dept1,@salary_sum_dept2;
->50000 27000
注意:变量,状态,处理程序,光标都是通过DECLARE定义的,他们之间是有先后顺序要求的。变量和状态必须在最前面声明,然后才能是光标的声明,最后才可以是处理程序的声明。
流程控制
可以使用IF、CASE、LOOP、LEAVE、ITERATE、REPEAT及WHILE语句进行流程的控制。详细说明及示例参考对应版本的官网。5.6版本:
http://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
CASE语句
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
上面光标的例子中,IF语句也可以使用CASE语句来完成:
CASE
WHEN i_dept_no =1 THEN
SET@salary_sum_dept1=@salary_sum_dept1+i_salary;
ELSE
SET@salary_sum_dept2=@salary_sum_dept2+i_salary;
END CASE;
或者:
CASE i_dept_no
WHEN 1 THEN
SET@salary_sum_dept1=@salary_sum_dept1+i_salary;
ELSE
SET @salary_sum_dept2=@salary_sum_dept2+i_salary;
END CASE;
LOOP语句
[begin_label:] LOOP
statement_list
END LOOP [end_label]
LOOP实现简单的循环,退出循环的条件需要使用其它的语句定义,通常可以使用LEAVE语句实现。如果不在statement_list中增加退出循环的语句,那么LOOP语句可以用来实现简单的死循环。
示例6:简单LOOP循环
CREATE PROCEDURE doiterate(p1 INT)
BEGIN
label1: LOOP
SETp1 = p1 + 1;
IFp1 < 10 THEN
ITERATE label1;
ENDIF;
LEAVE label1;
ENDLOOP label1;
SET@x = p1;
END;
LEAVE语句
LEAVE label
用来从标注的流程构造中退出,通常和BEGIN…END或者循环一起使用。如上面的例子。
ITERATE语句
ITERATE label
ITERATE语句必须用在循环中,作用是跳过当前循环的剩下的语句,直接进入下一轮循环。如上面的例子。
REPEAT语句
[begin_label:] REPEAT
statement_list
UNTIL search_condition
END REPEAT [end_label]
有条件的循环控制语句,当满足条件的时候退出循环。在示例5中使用REPEAT语句来实现光标的循环获得。
示例7:REPEAT的使用
mysql> delimiter //
mysql> CREATE PROCEDURE dorepeat(p1 INT)
-> BEGIN
-> SET @x = 0;
-> REPEAT
-> SET @x = @x + 1;
-> UNTIL @x > p1 END REPEAT;
-> END
-> //
Query OK, 0 rows affected (0.00 sec)
mysql> CALL dorepeat(1000)//
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @x//
+------+
| @x |
+------+
| 1001 |
+------+
1 row in set (0.00 sec)
WHILE语句
[begin_label:] WHILE search_condition DO
statement_list
END WHILE [end_label]
有条件的循环,与REPEAT的区别在于:WHILE是满足条件才执行循环,REPEAT是满足条件退出循环;WHILE在首次循环执行之前就潘丹条件,所以最少执行0次,而REPEAT是在首次执行循环之后才判断条件,所以循环最少执行1次。
示例8:WHILE的使用
REATE PROCEDURE dowhile()
BEGIN
DECLARE v1 INT DEFAULT 5;
WHILEv1 > 0 DO
...
SETv1 = v1 - 1;
ENDWHILE;
END;
事件调度器
事件调度器是MySQL5.1后新增的功能,可以将数据库按自定义的时间周期触发某种操作,可以理解为定时任务,类似Linux系统下的定时任务crontab
如:
CREATE EVENT myevent
ONSCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR
DO
UPDATE myschema.mytable SET mycol = mycol + 1;
其中:
<1> 事件名称在CREATE EVENT关键字之后指定;
<2> 通过ON SCHEDULE子句指定事件在何时执行及执行频次;
<3> 通过DO子句指定要执行的具体操作或事件。
上面的示例中创建一个创建后立即执行的调度事件,执行频次为1小时,执行内容为更新表数据。
事件调度器默认是关闭的,需要打开:
SET GLOBAL event_scheduler = 1;
如果事件调度器不再使用,可以禁用(disable)或删除(drop):
ALTER EVENT myevent disable;
DROP EVENT myevent;