标准SQL是非过程化的查询语言,具有操作统一、面向集合、功能丰富、使用简单等多项优点。但和程序设计语言相比,高度非过程化的优点也造成了它的一个弱点:缺少流程控制能力,难以实现应用业务中的逻辑控制。SQL编程技术可以有效克服SQL语言实现复杂应用方面的不足,提高应用系统和数据库管理系统间的互操作性。

存储过程和函数

存储过程

存储过程是由过程化SQL语句书写的过程,这个过程经过编译和优化后存储在数据库服务器中,因此称它为存储过程,使用时只要调用即可。

优点:

  • 运行效率高;
  • 降低了客户端和服务器之间的通信量;

缺点:

  • 难以调试和扩展,更没有移植性;

创建存储过程

CREATE
    PROCEDURE proc_name ([proc_parameter[,...]])
    routine_body
    
proc_parameter:
    [ IN | OUT | INOUT ] param_name type
    
routine_body:
    Valid SQL routine statement

proc_name:存储过程名称,可以用过*db_name.*制定数据库。

proc_parameter:参数列表,没有参数时使用(),参数名称不区分大小写,必须指定类型。默认情况下,每个参数都是IN参数,若要以其他方式指定参数,请在参数名称之前使用关键字OUT或INOUT。对于每个OUT或INOUT参数,在调用过程的CALL语句中传递一个用户定义的变量,以便在过程返回时获得它的值。如果从另一个存储过程或函数内部调用该过程,还可以将它们的参数或局部变量作为OUT或INOUT参数传递。如果从触发器内部调用过程,也可以传递NEW.col_name作为OUT或INOUT参数。

routine_body:由一个有效的SQL例程语句组成。这可以是一个简单的语句,比如SELECT或INSERT,也可以是一个使用BEGIN和END编写的复合语句。复合语句可以包含声明、循环和其他控制结构语句。

例如:创建一个存储过程使其可以找出大于给定年龄的学生个数

delimiter //
CREATE PROCEDURE S_T.proc_test1(IN SageParam SMALLINT, OUT SNum INT)
BEGIN 
SELECT COUNT(*) INTO SNum FROM Student WHERE Sage > SageParam;
END//
delimiter ;

注意:当过程被定义时,使用mysql客户端分隔符命令delimiter更改语句分隔符;到//。这使得;是传递给服务器的过程主体中使用的分隔符,而不是由mysql本身解释。

执行存储过程

CALL sp_name([parameter[,...]])
CALL sp_name[()]

调用上面定义的存储过程:

CALL S_T.proc_test1(18, @Snums);
SELECT @Snums;

查看存储过程

查看存储过程状态

SHOW PROCEDURE STATUS [LIKE 'pattern' | WHERE expr]

查看某个存储过程定义

SHOW CREATE PROCEDURE proc_name

删除存储过程

DROP PROCEDURE [IF EXISTS] proc_name

存储函数

函数和存储过程类似,都是持久性存储模块。函数的定义和存储过程也类似,不同之处是函数必须指定返回的类型。MySQL允许例程包含DDL语句,比如CREATE和DROP。MySQL还允许存储过程(但不允许存储函数)包含SQL事务语句,如COMMIT。存储函数不能包含执行显式或隐式提交或回滚的语句。

创建存储函数

CREATE
    FUNCTION func_name ([func_parameter[,...]])
    RETURNS type
    routine_body
    
func_parameter:
    param_name type

RETURNS:指定函数的返回值类型,并且函数体必须包含RETURN value 语句。

使用函数改造过程例子:

delimiter //
CREATE FUNCTION S_T.func_test1(SageParam SMALLINT)
RETURNS INT DETERMINISTIC
BEGIN 
DECLARE SNum INT;
SELECT COUNT(*) INTO SNum FROM Student WHERE Sage > SageParam;
RETURN SNum;
END//
delimiter ;

执行存储函数

SELECT func_name

查看存储函数

查看存储函数状态

SHOW FUNCTION STATUS [LIKE 'pattern' | WHERE expr]

查看某个存储函数定义

SHOW CREATE FUNCTION func_name

删除存储函数

DROP FUNCTION [IF EXISTS] func_name

复合语句语法

BEGIN ... END

[begin_label:] BEGIN
    [statement_list]
END [end_label]

BEGIN ... END语法用于编写复合语句,可以出现在存储程序(存储过程和函数、触发器和事件)中。复合语句可以包含多个语句,这些语句由BEGIN和END关键字包围。statement_list表示一个或多个语句的列表,每个语句以分号(;)语句分隔符结束。statement_list本身是可选的,因此空的复合语句(BEGIN END)是合法的。

BEGIN ... END语句可以进行嵌套。

在所有存储程序中,解析器将BEGIN [WORK]作为一个BEGIN ... END块的开始,要在此上下文中开始事务,请使用START TRANSACTION。

Statement Labels

[begin_label:] BEGIN
    [statement_list]
END [end_label]

[begin_label:] LOOP
    statement_list
END LOOP [end_label]

[begin_label:] REPEAT
    statement_list
UNTIL search_condition
END REPEAT [end_label]

[begin_label:] WHILE search_condition DO
    statement_list
END WHILE [end_label]

允许在BEGIN…END块并用LOOP、REPEAT和WHILE语句。这些语句的标签使用遵循以下规则:

  • begin_label必须跟一个冒号;
  • begin_label可以不指定end_label。如果end_label存在,它必须与begin_label相同;
  • 同一嵌套层上的标签必须是不同的;
  • 标签最长可达16个字符;

DECLARE Statement

DECLARE语句用于定义程序的各种本地项:

  • Local variables
  • Conditions and handlers
  • Cursors

DECLARE仅允许在BEGIN...END复合语句中,并且必须在其开始处,在任何其他语句之前。

声明必须遵循一定的顺序。cursor声明必须出现在handler声明之前,variable和condition声明必须出现在cursor或handler声明之前。

存储程序中的变量

系统变量和用户定义的变量可以在存储程序中使用,就像它们可以在存储程序上下文之外使用一样。此外,存储程序可以使用DECLARE来定义局部变量。

声明局部变量:

DECLARE var_name [, var_name] ... type [DEFAULT value]

变量可以直接用set语句设置:

SET local_var_name = expr [, local_var_name = expr] ...
delimiter //
CREATE PROCEDURE test001() 
BEGIN
DECLARE var_age smallint DEFAULT 0;
SET var_age = 10;
SELECT var_age;
END //
delimiter ;

查询的结果可以使用SELECT ... INTO var_list或打开游标并使用 FETCH ... INTO var_list

delimiter //
CREATE PROCEDURE test002() 
BEGIN
DECLARE var_age smallint DEFAULT 0;
SELECT Sage INTO var_age FROM Student WHERE Sno = '201215121';
SELECT var_age;
END //
delimiter ;

流程控制语句

CASE Statement
CASE case_value
    WHEN when_value THEN statement_list
    [WHEN when_value THEN statement_list] ...
    [ELSE statement_list]
END CASE

case_value是一个表达式。将此值与每个WHEN子句中的when_value表达式进行比较,直到其中一个相等。当找到一个相等的when_value时,执行相应的THEN子句statement_list。如果when_value不相等,则执行ELSE子句statement_list(如果有的话)。

delimiter //
CREATE PROCEDURE test003(IN x INT) 
BEGIN
CASE x / 2
	WHEN 1 THEN
		SELECT 'A';
	ELSE
		SELECT 'B';
END CASE;
END //
delimiter ;

CASE
    WHEN search_condition THEN statement_list
    [WHEN search_condition THEN statement_list] ...
    [ELSE statement_list]
END CASE

每个WHEN子句search_condition表达式都被求值,直到其中一个为真,此时它对应的THEN子句statement_list就会执行。如果没有相等的search_condition,则执行ELSE子句statement_list(如果有的话)。

delimiter //
CREATE PROCEDURE test004(IN x INT) 
BEGIN
CASE 
	WHEN x > 0 THEN
		SELECT 'A';
	ELSE
		SELECT 'B';
END CASE;
END //
delimiter ;
IF Statement
IF search_condition THEN statement_list
    [ELSEIF search_condition THEN statement_list] ...
    [ELSE statement_list]
END IF
delimiter //
CREATE PROCEDURE test005(IN x INT) 
BEGIN
IF x = 1 THEN
	SELECT x * 10;
	ELSEIF x = 2 THEN
	SELECT x * 20;
ELSE
		SELECT x * 30;
END IF;
END //
delimiter ;
ITERATE Statement
ITERATE label

ITERATE只能出现在LOOP、REPEAT和WHILE语句中。ITERATE的意思是“再次开始循环”。

LEAVE Statement
LEAVE label

此语句用于退出具有给定标签的流控制构造。如果标签是最外层存储的程序块,则LEAVE退出程序。

LEAVE可以使用在BEGIN…END或循环结构(LOOP, REPEAT, WHILE)中。

LOOP Statement
[begin_label:] LOOP
    statement_list
END LOOP [end_label]

LOOP实现了一个简单的循环构造,允许重复执行语句列表。循环中的语句将重复执行,直到循环结束。通常,这是通过LEAVE语句完成的。在存储的函数中,还可以使用RETURN,它会完全退出函数。

delimiter //
CREATE PROCEDURE test006(p1 INT)
BEGIN
  label1: LOOP
    SET p1 = p1 + 1;
    IF p1 < 10 THEN
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
  SELECT p1;
END //
delimiter ;
REPEAT Statement
[begin_label:] REPEAT
    statement_list
UNTIL search_condition
END REPEAT [end_label]

REPEAT语句中的语句列表将被重复,直到search_condition表达式为真。因此,REPEAT总是至少进入循环一次。

RETURN Statement
RETURN expr

RETURN语句终止存储函数的执行,并将值expr返回给函数调用者。存储函数中必须至少有一个RETURN语句。

此语句不用于存储过程、触发器或事件。可以使用LEAVE语句退出这些类型的存储程序。

WHILE Statement
[begin_label:] WHILE search_condition DO
    statement_list
END WHILE [end_label]

只要search_condition表达式为真,WHILE语句中的语句列表就会重复。

Cursors

MySQL支持存储程序中的游标。其语法与嵌入式SQL相同。游标具有以下属性:

  • Asensitive:服务器可能会也可能不会复制其结果表;
  • Read only: 不能修改;
  • Nonscrollable:只能在一个方向上遍历,不能跳过行;

游标声明必须出现在handler声明之前,variable and condition声明之后。

Cursor DECLARE Statement
DECLARE cursor_name CURSOR FOR select_statement

该语句声明一个游标,并将其与一个SELECT语句关联,该SELECT语句检索游标要遍历的行。

Cursor OPEN Statement
OPEN cursor_name

打开生命的游标。

Cursor FETCH Statement
FETCH [[NEXT] FROM] cursor_name INTO var_name [, var_name] ...

该语句获取与指定游标(必须打开)关联的SELECT语句的下一行,并推进游标指针。如果存在一行,则获取的列存储在指定的变量中。SELECT语句检索的列数必须与FETCH语句中指定的输出变量数匹配。

如果没有更多的行可用,则出现一个No Data条件,SQLSTATE值为'02000'。要检测此条件,可以为其设置handler。

Cursor CLOSE Statement
CLOSE cursor_name

该语句关闭以前打开的游标,如果游标未打开,则发生错误。

如果未显式关闭,光标将在声明它的BEGIN ... END块的末尾关闭。

Condition Handling

在存储程序执行期间可能会出现需要特殊处理的情况,例如退出当前程序块或继续执行。可以为警告或异常等一般条件或特定条件(如特定错误代码)定义处理程序。特定条件可以指定名称,并在处理程序中以这种方式引用。

DECLARE ... CONDITION Statement
DECLARE condition_name CONDITION FOR condition_value

condition_value: {
    mysql_error_code
  | SQLSTATE [VALUE] sqlstate_value
}

DECLARE ... CONDITION语句声明一个命名错误条件,将名称与需要特定处理的条件相关联。该名称可在随后的DECLARE ... HANDLER statement中引用。

Condition声明必须出现在cursor声明或handler声明之前。

condition_value:指示要与条件名相关联的特定条件或条件类。

SIGNAL或RESIGNAL语句中引用的条件名称必须与SQLSTATE值关联,而不是与MySQL错误代码关联。

DECLARE ... HANDLER Statement
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
}

DECLARE ...HANDLER语句指定处理一个或多个条件的处理程序。如果出现这些条件之一,则执行指定的语句。语句可以是一个简单的语句,如SET var_name=value,也可以是使用BEGIN和END编写的复合语句。

handler声明必须出现在变量或condition声明之后。

handler_action值指示处理程序在执行处理程序语句后采取的动作:

  • CONTINUE:继续执行当前程序;
  • EXIT:声明处理程序的BEGIN...END复合语句的执行终止。即使条件发生在内部块中,也是如此。
  • UNDO:不支持;

condition_value:激活处理程序的特定条件或条件类。

例:

CREATE PROCEDURE curdemo()
BEGIN
  DECLARE done INT DEFAULT FALSE;
  DECLARE a CHAR(16);
  DECLARE b, c INT;
  DECLARE cur1 CURSOR FOR SELECT id,data FROM test.t1;
  DECLARE cur2 CURSOR FOR SELECT i FROM test.t2;
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

  OPEN cur1;
  OPEN cur2;

  read_loop: LOOP
    FETCH cur1 INTO a, b;
    FETCH cur2 INTO c;
    IF done THEN
      LEAVE read_loop;
    END IF;
    IF b < c THEN
      INSERT INTO test.t3 VALUES (a,b);
    ELSE
      INSERT INTO test.t3 VALUES (a,c);
    END IF;
  END LOOP;

  CLOSE cur1;
  CLOSE cur2;
END;

更多请查看 https://dev.mysql.com/doc/refman/8.0/en/condition-handling.html



参考文献:
数据库系统概论-第5版[王珊,萨师煊]
https://dev.mysql.com/doc/refman/8.0/en/