1 什么是存储过程和函数
存储过程和函数是事先经过编译并存储在数据库中的⼀段 SQL 语句的集合,调⽤存储过程和函数可以简化应⽤开发⼈员的很多⼯作,减少数据在数据库和应⽤服务器 之间的传输,对于提⾼数据处理的效率是有好处的。
存储过程和函数的区别在于函数必须有返回值,⽽存储过程没有,存储过程的参 数可以使⽤IN、OUT、INOUT类型,⽽函数的参数只能是IN类型的。如果有函数从其 他类型的数据库迁移到MySQL,那么就可能因此需要将函数改造成存储过程。
2.1 创建、修改存储过程或者函数
CREATE PROCEDURE sp_name ([proc_parameter[,. .]]) [characteristic . .] routine_body
CREATE 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 characteristic:
LANGUAGE SQL
| [NOT] DETERMINISTIC
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
| COMMENT 'string' routine_body:
Valid SQL procedure statement or statements
ALTER {PROCEDURE | FUNCTION} sp_name [characteristic . .] characteristic:
{ CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
| COMMENT 'string'
调⽤过程的语法如下:
CALL sp_name([parameter[,. .]]) MySQL的存储过程和函数中允许包含DDL语句,也允许在存储过程中执⾏提交
(Commit,即确认之前的修改)或者回滚(Rollback,即放弃之前的修改),但是存 储过程和函数中不允许执⾏LOAD DATA INFILE语句。此外,存储过程和函数中可以 调⽤其他的过程或者函数。
下⾯创建了⼀个新的过程film_in_stock:
mysql> DELIMITER $$ mysql>
mysql> CREATE PROCEDURE film_in_stock(IN p_film_id INT, IN p_store_id INT,
OUT p_film_count INT)
-> READS SQL DATA
-> BEGIN
-> SELECT inventory_id
-> FROM inventory
-> WHERE film_id = p_film_id
-> AND store_id = p_store_id
-> AND inventory_in_stock(inventory_id);
->
-> SELECT FOUND_ROWS() INTO p_film_count;
-> END $$
Query OK, 0 rows affected (0.00 sec)
上⾯是在使⽤的样例数据库中创建的⼀个过程,该过程⽤来检查film_id和store_id 对应的inventory是否满⾜要求,并且返回满⾜要求的inventory_id以及满⾜要求的记录 数。
通常我们在执⾏创建过程和函数之前,都会通过“DELIMITER $$”命令将语句的 结束符从“;”修改成其他符号,这⾥使⽤的是“$$”,这样在过程和函数中的“;”就不会被 MySQL解释成语句的结束⽽提⽰错误。在存储过程或者函数创建完毕,通 过“DELIMITER ;”命令再将结束符改回成“;”。
可以看到在这个过程中调⽤了函数 inventory_in_stock(),并且这个过程有两个输
⼊参数和⼀个输出参数。下⾯可以通过调⽤这个过程来看看返回的结果。 如果需要检查 film_id=2 store_id=2对应的 inventory的情况,则⾸先⼿⼯执⾏过程
中的SQL语句,以查看执⾏的效果:
SELECT inventory_id
-> FROM inventory
-> WHERE film_id = 2
-> AND store_id = 2
-> AND inventory_in_stock(inventory_id);
+--------------+
| inventory_id |
+--------------+
| 10 |
| 11 |
+--------------+
2rows in set (0.00 sec)
满⾜条件的记录应该是两条,inventory_id分别是10和11。如果将这个查询封装在 存储过程中调⽤,那么调⽤过程的执⾏情况如下 CALL film_in_stock(2,2,@a);
+--------------+
| inventory_id |
+--------------+
| 10 |
| 11 |
+--------------+
2 rows in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec) mysql> select @a;
+------+
| @a |
+------+
| 2 |
+------+
1row in set (0.00 sec)
可以看到调⽤存储过程与直接执⾏ SQL 的效果是相同的,但是存储过程的好处在 于处理逻辑都封装在数据库端,调⽤者不需要了解中间的处理逻辑,⼀旦处理逻辑发
⽣变化,只需要修改存储过程即可,⽽对调⽤者的程序完全没有影响。 另外,和视图的创建语法稍有不同,存储过程和函数的CREATE语法不⽀持使⽤
CREATE OR REPLACE对存储过程和函数进⾏修改,如果需要对已有的存储过程或者 函数进⾏修改,需要执⾏ALTER语法。
⼀次只能删除⼀个存储过程或者函数,删除存储过程或者函数需要有该过程或者
函数的ALTER ROUTINE权限,具体语法如下:
DROP {PROCEDURE | FUNCTION} [IF EXISTS] sp_name
1.查看存储过程或者函数的状态
SHOW {PROCEDURE | FUNCTION} STATUS [LIKE 'pattern']
下⾯演⽰的是查看过程film_in_stock的信息:
mysql> show procedure status like 'film_in_stock'\G
2.查看存储过程或者函数的定义
SHOW CREATE {PROCEDURE | FUNCTION} sp_name
下⾯演⽰的是查看过程film_in_stock的定义:
mysql> show create procedure film_in_stock \G
2.4 变量的使⽤
1.变量的定义
通过DECLARE可以定义⼀个局部变量,该变量的作⽤范围只能在BEGIN…END 块中,可以⽤在嵌套的块中。变量的定义必须写在复合语句的开头,并且在任何其他 语句的前⾯。可以⼀次声明多个相同类型的变量。如果需要,可以使⽤DEFAULT赋 默认值。
定义⼀个变量的语法如下:
DECLARE var_name[,. .] type [DEFAULT value] 例如,定义⼀个DATE类型的变量,名称是last_month_start: DECLARE last_month_start DATE;
2.变量的赋值
变量可以直接赋值,或者通过查询赋值。直接赋值使⽤SET,可以赋常量或者赋 表达式,具体语法如下:
SET var_name = expr [, var_name = expr] . .
给刚才定义的变量last_month_start赋值,具体语法如下:
SET last_month_start = DATE_SUB(CURRENT_DATE(), INTERVAL 1 MONTH);
也可以通过查询将结果赋给变量,这要求查询返回的结果必须只有⼀⾏,具体语 法如下:
SELECT col_name[,. .] INTO var_name[,. .] table_expr
通过查询将结果赋值给变量v_payments:
CREATE FUNCTION get_customer_balance(p_customer_id INT, p_effective_date DATETIME)
RETURNS DECIMAL(5,2) DETERMINISTIC
READS SQL DATA BEGIN
…
DECLARE v_payments DECIMAL(5,2); #SUM OF PAYMENTS MADE
PREVIOUSLY
…
SELECT IFNULL(SUM(payment.amount),0) INTO v_payments FROM payment
WHERE payment.payment_date <= p_effective_date AND payment.customer_id = p_customer_id;
…
RETURN v_rentfees + v_overfees - v_payments; END $$
2.5 定义条件和处理
条件的定义和处理可以⽤来定义在处理过程中遇到问题时相应的处理步骤。
1.条件的定义
DECLARE condition_name CONDITION FOR condition_value condition_value:
SQLSTATE [VALUE] sqlstate_value
| mysql_error_code
2.条件的处理
DECLARE handler_type HANDLER FOR condition_value[,. .] sp_statement handler_type:
CONTINUE
| EXIT
| UNDO
condition_value:
SQLSTATE [VALUE] sqlstate_value
| condition_name
| SQLWARNING
| NOT FOUND
| SQLEXCEPTION
| mysql_error_code
下⾯将通过两个例⼦来说明:在向 actor 表中插⼊记录时,如果没有进⾏条件的处
理,那么在主键重的时候会抛出异常并退出,如果对条件进⾏了处理,那么就不会再
抛出异常。
(1)当没有进⾏条件处理时,执⾏结果如下:
mysql> select max(actor_id) from actor;
+---------------+
| max(actor_id) |
+---------------+
| 200 |
+---------------+
1 row in set (0.00 sec) mysql> delimiter $$ mysql>
mysql> CREATE PROCEDURE actor_insert ()
-> BEGIN
-> SET @x = 1;
-> INSERT INTO actor(actor_id,first_name,last_name) VALUES (201,'Test','201');
-> SET @x = 2;
-> INSERT INTO actor(actor_id,first_name,last_name) VALUES (1,'Test','1');
-> SET @x = 3;
-> END;
-> $$
Query OK, 0 rows affected (0.00 sec) mysql> delimiter ;
mysql> call actor_insert();
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY' mysql> select @x;
+------+
| @x |
+------+
| 2 |
+------+
1 row in set (0.00 sec)
从上⾯的例⼦可以看出,执⾏到插⼊actor_id=1的记录时,会主键重并退出,没有 执⾏到下⾯其他的语句。
(2)当对主键重的异常进⾏处理时,执⾏结果如下:
mysql> delimiter $$ mysql>
mysql> CREATE PROCEDURE actor_insert ()
-> BEGIN
-> DECLARE CONTINUE HANDLER FOR SQLSTATE '23000' SET @x2 = 1;
-> SET @x = 1;
-> INSERT INTO actor(actor_id,first_name,last_name) VALUES (201,'Test','201');
-> SET @x = 2;
-> INSERT INTO actor(actor_id,first_name,last_name) VALUES (1,'Test','1');
-> SET @x = 3;
-> END;
-> $$
Query OK, 0 rows affected (0.00 sec) mysql> delimiter ;
mysql> call actor_insert();
Query OK, 0 rows affected (0.06 sec) mysql> select @x,@x2;
+------+------+
| @x | @x2 |
+------+------+
| 3| 1|
+------+------+
1 row in set (0.00 sec)
调⽤条件处理的过程,再遇到主键重的错误时,会按照定义的处理⽅式进⾏处 理,由于例⼦中定义的是CONTINUE,所以会继续执⾏下⾯的语句。
handler_type现在还只⽀持CONTINUE和EXIT两种,CONTINUE表⽰继续执⾏下
⾯的语句,EXIT则表⽰执⾏终⽌,UNDO现在还不⽀持。
condition_value的值可以是通过DECLARE定义的condition_name,可以是
SQLSTATE的值或者mysql-error-code的值或者 SQLWARNING、NOT FOUND、 SQLEXCEPTION,这 3个值是3种定义好的错误类别,分别代表不同的含义。
SQLWARNING是对所有以01开头的SQLSTATE代码的速记。 NOT FOUND是对所有以 02开头的SQLSTATE代码的速记。
SQLEXCEPTION是对所有没有被SQLWARNING或NOT FOUND捕获的SQLSTATE代码的速记。
因此,上⾯的例⼦还可以写成以下⼏种⽅式:
--捕获mysql-error-code:
DECLARE CONTINUE HANDLER FOR 1062 SET @x2 = 1;
--事先定义condition_name:
DECLARE DuplicateKey CONDITION FOR SQLSTATE '23000'; DECLARE CONTINUE HANDLER FOR DuplicateKey SET @x2 = 1;
--捕获SQLEXCEPTION
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET @x2 = 1;
12.2.6 光标的使⽤
在存储过程和函数中,可以使⽤光标对结果集进⾏循环的处理。光标的使⽤包括
光标的声明、OPEN、FETCH和CLOSE,其语法分别如下。
声明光标:
DECLARE cursor_name CURSOR FOR select_statement
OPEN光标:
OPEN cursor_name
FETCH光标:
FETCH cursor_name INTO var_name [, var_name] . .
CLOSE光标:
CLOSE cursor_name
以下例⼦是⼀个简单的使⽤光标的过程,对 payment 表按照⾏进⾏循环的处理, 按照staff_id值的不同累加amount的值,判断循环结束的条件是捕获NOT FOUND的条 件,当FETCH光标找不到下⼀条记录的时候,就会关闭光标然后退出过程。
mysql> delimiter $$ mysql>
mysql> CREATE PROCEDURE payment_stat ()
-> BEGIN
-> DECLARE i_staff_id int;
-> DECLARE d_amount decimal(5,2);
-> DECLARE cur_payment cursor for select staff_id,amount from payment;
-> DECLARE EXIT HANDLER FOR NOT FOUND CLOSE cur_payment;
->
-> set @x1 = 0;
-> set @x2 = 0;
->
-> OPEN cur_payment;
->
-> REPEAT
-> FETCH cur_payment INTO i_staff_id, d_amount;
-> if i_staff_id = 2 then
-> set @x1 = @x1 + d_amount;
->else
-> set @x2 = @x2 + d_amount;
-> end if;
-> UNTIL 0 END REPEAT;
->
-> CLOSE cur_payment;
->
-> END;
-> $$
Query OK, 0 rows affected (0.00 sec) mysql> delimiter ;
mysql>
mysql> call payment_stat();
Query OK, 0 rows affected (0.11 sec) mysql> select @x1,@x2;
+----------+----------+
| @x1 | @x2 |
+----------+----------+
| 33927.04 | 33489.47 |
+----------+----------+
1 row in set (0.00 sec)
注意:变量、条件、处理程序、光标都是通过DECLARE定义的,它们之间是有先后顺序要求的。变量和条件 必须在最前⾯声明,然后才能是光标的声明,最后才可以是处理程序的声明。
12.2.7 流程控制
可以使⽤IF、CASE、LOOP、LEAVE、ITERATE、REPEAT及WHILE语句进⾏流
程的控制,下⾯将逐⼀进⾏说明。
1.IF语句 IF实现条件判断,满⾜不同的条件执⾏不同的语句列表,具体语法如下: IF search_condition THEN statement_list
[ELSEIF search_condition THEN statement_list] . . [ELSE statement_list]
END IF
上⼀⼩节中使⽤光标的例⼦中已经涉及了IF语句的使⽤,这⾥就不再举例说明 了。
2.CASE语句 CASE实现⽐IF更复杂⼀些的条件构造,具体语法如下: 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_staff_id = 2 then
set @x1 = @x1 + d_amount; else
set @x2 = @x2 + d_amount; end case;
或者:
case i_staff_id when 2 then
set @x1 = @x1 + d_amount; else
set @x2 = @x2 + d_amount; end case;
3.LOOP语句
LOOP实现简单的循环,退出循环的条件需要使⽤其他的语句定义,通常可以使
⽤LEAVE语句实现,具体语法如下:
[begin_label:] LOOP statement_list
END LOOP [end_label]
如果不在statement_list中增加退出循环的语句,那么LOOP语句可以⽤来实现简单 的死循环。
4.LEAVE语句
⽤来从标注的流程构造中退出,通常和BEGIN ... END或者循环⼀起使⽤。 下⾯是⼀个使⽤LOOP和LEAVE的简单例⼦,循环100次向actor表中插⼊记录,当
插⼊100条记录后,退出循环:
mysql> CREATE PROCEDURE actor_insert ()
-> BEGIN
-> set @x = 0;
-> ins: LOOP
-> set @x = @x + 1;
-> IF @x = 100 then
-> leave ins;
-> END IF;
-> INSERT INTO actor(first_name,last_name) VALUES ('Test','201');
-> END LOOP ins;
-> END;
-> $$
Query OK, 0 rows affected (0.00 sec) mysql> call actor_insert();
Query OK, 0 rows affected (0.01 sec)
mysql> select count(*) from actor where first_name='Test';
+----------+
| count(*) |
+----------+
| 100|
+----------+
1 row in set (0.00 sec)
5.ITERATE语句
ITERATE语句必须⽤在循环中,作⽤是跳过当前循环的剩下的语句,直接进⼊下
⼀轮循环。下⾯的例⼦使⽤了ITERATE语句,当@x变量是偶数的时候,不再执⾏循 环中剩下的语句,
⽽直接进⾏下⼀轮的循环:
mysql> CREATE PROCEDURE actor_insert ()
-> BEGIN
-> set @x = 0;
-> ins: LOOP
-> set @x = @x + 1;
-> IF @x = 10 then
-> leave ins;
-> ELSEIF mod(@x,2) = 0 then
-> ITERATE ins;
-> END IF;
-> INSERT INTO actor(actor_id,first_name,last_name) VALUES (@x+200, 'Test',@x);
-> END LOOP ins;
-> END;
-> $$
Query OK, 0 rows affected (0.00 sec) mysql> call actor_insert();
Query OK, 0 rows affected (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where first_name='Test';
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
| 201 | Test | 1 |
| 203 | Test | 3 |
| 205| Test| 5|
| 207| Test| 7|
| 209| Test| 9|
+----------+------------+-----------+
5 rows in set (0.00 sec)
6.REPEAT语句 有条件的循环控制语句,当满⾜条件的时候退出循环,具体语法如下: [begin_label:] REPEAT
statement_list
UNTIL search_condition END REPEAT [end_label]
在“12.2.6光标的使⽤”⼩节中的例⼦就使⽤了REPEAT语句来实现光标的循环获
得,下⾯节选的代码就是其中使⽤REPEAT语句的部分,详细的执⾏过程可以参照
12.2.6⼩节,这⾥不再赘述。
-> REPEAT
-> FETCH cur_payment INTO i_staff_id, d_amount;
-> if i_staff_id = 2 then
-> set @x1 = @x1 + d_amount;
-> else
-> set @x2 = @x2 + d_amount;
-> end if;
-> UNTIL 0 END REPEAT;
7.WHILE语句
WHILE 语句实现的也是有条件的循环控制语句,即当满⾜条件时执⾏循环的内 容,具体语法如下:
[begin_label:] WHILE search_condition DO statement_list
END WHILE [end_label] WHILE循环和REPEAT循环的区别在于:WHILE是满⾜条件才执⾏循环,
REPEAT是满⾜条件退出循环;WHILE在⾸次循环执⾏之前就判断条件,所以循环最 少执⾏0次,⽽REPEAT是在⾸次执⾏循环之后才判断条件,所以循环最少执⾏1次。
以下例⼦⽤来对⽐REPEAT和WHILE语句的功能:
mysql> delimiter $$
mysql> CREATE PROCEDURE loop_demo ()
-> BEGIN
-> set @x = 1 , @x1 = 1;
-> REPEAT
-> set @x = @x + 1;
-> until @x > 0 end repeat;
->
-> while @x1 < 0 do
-> set @x1 = @x1 + 1;
-> end while;
-> END;
-> $$
Query OK, 0 rows affected (0.00 sec) mysql> delimiter ;
mysql> call loop_demo();
Query OK, 0 rows affected (0.00 sec) mysql> select @x,@x1;
+------+------+
| @x | @x1 |
+------+------+
| 2 | 1 |
+------+------+
1 row in set (0.00 sec)
从判断的条件上看,初始值都是满⾜退出循环的条件的,但是REPEAT循环仍然 执⾏了⼀次以后才退出循环的,⽽WHILE循环则⼀次都没有执⾏。