1. 程式人生 > 實用技巧 >12. 儲存過程和函式

12. 儲存過程和函式

MySQL官方文件有儲存例程(Store routines)一說,它的兩種型別就是儲存過程和函式。

12.1 什麼是儲存過程和函式

儲存過程和函式是事先經過編譯並存儲在資料庫中的一段SQL語句的集合。可以把一些複雜的資料處理邏輯封裝在儲存過程和函式,這樣就減輕了後端人員編寫SQL語句的負擔,提高了資料處理的效率。

儲存過程和函式的區別:

  • 函式必須有返回值,而儲存過程沒有。
  • 儲存過程的引數可以使用IN、OUT、INOUT型別,函式的引數只能是IN型別。省略則預設引數型別為IN。

12.2 儲存過程和函式的相關操作

對儲存過程和函式操作時,需要使用者有相應的許可權。建立儲存過程或者函式需要 CREATE ROUTINE 許可權,修改或者刪除儲存過程或者函式需要 ALTER ROUTINE 許可權,執行儲存過程或者函式需要 EXECUTE 許可權。

通常begin-end用於定義一組語句塊,在各大資料庫中的客戶端工具中可直接呼叫,但在mysql中不可用。

begin-end、流程控制語句、區域性變數只能用於函式、儲存過程內部、遊標、觸發器的定義內部。

12.2.1 儲存過程或函式建立

CREATE
    [DEFINER = user]
    PROCEDURE sp_name ([proc_parameter[,...]])
    [characteristic ...] routine_body

CREATE
    [DEFINER = 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

characteristic: {
    COMMENT 'string'
  | LANGUAGE SQL
  | [NOT] DETERMINISTIC
  | { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
  | SQL SECURITY { DEFINER | INVOKER }
}

routine_body:
    Valid SQL routine statement

呼叫儲存過程和函式

# 儲存過程
call proc_name(...);
# 函式:直接在表示式中引用即可,和呼叫SQL內部函式一樣
func_name();

下面的示例顯示了一個簡單的儲存過程,該儲存過程給定了國家(地區)程式碼,計算了出現在國家表中相同國家程式碼的城市數。使用IN引數傳遞國家/地區程式碼,並使用OUT引數返回城市計數:

CREATE TABLE "country" (
  "country_code" varchar(11) DEFAULT NULL,
  "city" varchar(11) DEFAULT NULL
);

# 修改定界符,防止mysql在遇到“;”被解釋為結束符, code是mysql保留的關鍵字
delimiter //;
CREATE PROCEDURE citycount(in ccode varchar(11), out cities varchar(11))
begin
select count(*) into cities from country
where country_code=ccode;
end
//
# 將結束定界符復原
delimiter ;
# 呼叫儲存過程,這裡的@cities相當於一個全域性變數,在執行完畢之後,可以通過
# select @cities;檢視結果
call citycount('CN', @cities);

定界符因需選擇,比如下面儲存函式就沒有使用,因為就只有單條語句。

mysql> create function hello(s char(20))
    -> returns char(50) deterministic
    -> return concat('hello, ',s,'!');

mysql> select hello('world');
+----------------+
| hello('world') |
+----------------+
| hello, world!  |
+----------------+

關於characteristic特徵值的說明這裡不再記錄,詳情可以看書或者官方文件。

12.2.2 刪除儲存過程或者函式

DROP {PROCEDURE | FUNCTION} [IF EXISTS] sp_name;

12.2.3 檢視儲存過程或者函式

show create {PROCEDURE | FUNCTION} sp_name;

show {PROCEDURE | FUNCTION} status like \G;

# ...具體就是結果不同
# show create 查詢的是 某結構的 定義
# show status 查詢的是 某結構的 狀態

12.2.4 變數的宣告及使用

MySQL變數的分類

MySQL變數分為:區域性變數、使用者變數、會話變數和全域性變數。全域性變數和會話變數又稱為系統變數

(1)區域性變數

一般用於sql語句塊,比如store_routines的begin/end。作用域僅限於該語句塊,生命週期隨語句塊而存活。

一般用法:declare宣告,set或者select ...into 賦值

(2)使用者變數

@var_name形式的變數為使用者變數,使用者變數的生命週期與本次連線(session)有關,當本次會話結束,連線中所有定義的使用者變數都會消失,且各個連線中的使用者變數互不可見。

一般用法:set @var_name=value; set @var_name:=value;或者 select @var_name:=value;select @num:=欄位名 from 表名 where ……

使用者變數不需要宣告,賦初值的過程即宣告和定義,一起做了。宣告及定義然後再賦值

(3)會話變數和全域性變數

session和globall。

1、變數的定義

格式:DECLARE var_name[,...] type [DEFAULT value]

通過 DECLARE 可以定義一個區域性變數,該變數的作用範圍只能在 BEGIN…END 塊中,可以用在巢狀的塊中。變數的定義必須寫在複合語句的開頭,並且在任何其他語句的前面。可以一次宣告多個相同型別的變數。如果需要,可以使用 DEFAULT 賦預設值。

2、變數的賦值

變數可以直接賦值,或者通過查詢賦值

格式:

# set賦值可以是常量或者表示式
set var_name = expr [,var name = expr]...

# 查詢賦值,要求查詢返回的結果必須只有一行
select col_name[,...] into var_name[,...] table_expr;

declare 宣告變數,set或者select...into為變數賦值。

# set方式賦值
delimiter //
create function addSum(a int,b int)
returns int
begin
declare sum int;
set sum = a + b;
return sum;
end
//
# 查詢賦值
delimiter //
create function addSum(a int,b int)
returns int
begin
declare sum int;
select a+b into sum;
return sum;
end
//

12.2.5 定義條件和處理

條件的定義和處理可以用來定義在處理過程中遇到問題時相應的處理步驟。

1、條件的定義

DECLARE condition_name CONDITION FOR condition_value

condition_value: {
    mysql_error_code
  | SQLSTATE [VALUE] sqlstate_value
}

# mysql_error_code: An integer literal indicating a MySQL error code.
# SQLSTATE [VALUE] sqlstate_value: A 5-character string literal indicating an SQLSTATE value.

2、條件的處理

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
}

當呼叫儲存過程執行其中的SQL時發生內部錯誤時,如果使用了條件處理,那麼就會觸發條件處理的action,它有三個值分別代表:

  • 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 程式碼的速記。

向staff表中插入重複主鍵的記錄,如果沒有異常處理,x=3就不會執行,我們希望異常發生後,繼續往後執行,這時就需要異常處理。

CREATE DEFINER="stronger"@"%" PROCEDURE "staff_insert"()
BEGIN
	declare continue handler for sqlstate '23000' set @x2=1;
	set @x = 1;
	insert into staff values(2, 1, 'Qing','Q');
	set @x = 2;
	insert into staff values(1, 1, 'Long','L');
	set @x = 3;
END

12.2.6 游標的使用

在儲存過程和函式中可以使用游標對結果集進行迴圈的處理。游標的使用包括游標的宣告、OPEN、FETCH 和 CLOSE。

  • 宣告游標

    declare cursor_name cursor for select statement;
    
  • 開啟游標

    open cursor_name;
    
  • fetch游標,迴圈遍歷

    fetch cursor_name into var_name [, var_name]...
    

    作用:將遍歷的每條記錄賦值到var_name

  • 關閉游標

    close cursor_name;
    

例:統計賬單金額大於100的賬單數量,判斷迴圈結束的條件是捕獲NOT FOUND的條件,當FETCH游標找不到下一條記錄的時候,就會關閉游標然後退出過程。

drop PROCEDURE payment_proc;
delimiter //
create procedure payment_proc()
begin
	# 一定不要定義和fetch捕獲的列同名的變數比如declare amount int;
	declare i_amount int;
	declare amount_cur cursor for select amount from payment;

	declare exit handler for not found close amount_cur;
	
	set @count = 0;
	# 開啟游標
	open amount_cur;
	REPEAT
		# 使用游標
		fetch amount_cur into i_amount;
			if i_amount>100 then
				set @count = @count + 1;# 注意賦值必須加set
			end if;
	UNTIL 0 END REPEAT;
	
	# 關閉游標
	close amount_cur;

end
//

call payment_proc();
select @count;

【注】

  • navicate中定義儲存過程一定要通過delimiter //改變定界符,不然會報錯。
  • fetch遊標將引數賦值給之前定義的變數,需要注意定義的變數不要和遊標中捕獲的引數同名。
  • 變數、條件、處理程式、游標都是通過 DECLARE 定義的,它們之間是有先後順序的要求的。變數和條件必須在最前面宣告,然後才能是游標的宣告,最後才可以是處理程式的宣告。

12.2.7 流程控制

1、IF語句

語法:

if condition then statement_list
	[elseif condition then statement_list]...s
	[else statement_list]
endif;

2、CASE語句

CASE 實現比 IF 更復雜一些的條件構造,具體語法如下:

case case_value
	when when_value then statement_list
	[when when_value then statement_list]...
	[else statement_list]
end case
or:
case 
	when search_condition then statement_list
	[when search_condition then statement_list]...
	[else statement_list]
end case;

3、LEAVE和ITERATE

(1)LEAVE

用於退出具有給定標籤的流控制構造。如果標籤用於最外面儲存的程式塊,則LEAVE退出程式。

LEAVE可以在BEGIN ... END或迴圈構造(LOOP,REPEAT,WHILE)中使用。

#格式
LEAVE label;

(2)ITERATE

ITERATE 語句必須用在迴圈中,作用是跳過當前迴圈的剩下的語句,直接進入下一輪迴圈。

#格式
ITERATE label;

# 迴圈向表中插入值,如果是偶數跳過當前迴圈,奇數則插入
create procedure test()
begin
	declare i int default 10;
	ins: loop
		set i = i + 1;
		if i > 20 then
			leave ins;
		elseif mod(i,2)=0 then
			iterate ins;
		else 
			insert into t values(i);
		end if;
		#set i = i + 1;剛開始把這句放後面了,導致死迴圈
	end loop;
end
//

4、迴圈

MySQL中迴圈的定義有whilerepeatloop

(1)while

#格式
[begin_label:] WHILE search_condition DO
    statement_list
END WHILE [end_label]

delimiter //
# 儲存過程括號一定不要忘記加
create procedure test()
begin
	declare i int default 1;
	while i <= 10 do
		insert into t values(i);
		set i = i + 1;
	end while;
end
//
delimiter ;
call test();

(2)repeat

# 格式
[begin_label:] REPEAT
    statement_list
UNTIL search_condition # 注意它的意思,是直到滿足什麼條件才跳出,有點像do...while
END REPEAT [end_label]

create procedure test()
begin
	declare i int default 1;
	repeat
		insert into t values(i);
		set i = i + 1;
	until i > 10 end repeat;
end

(3)loop

通常loop配合leave,迴圈的終止需要leave,或者如果在儲存函式中也可以通過return終止迴圈。

# 格式
[begin_label:] LOOP
    statement_list
END LOOP [end_label]

create procedure test()
begin
	declare i int default 1;
	lp: loop
		if i > 10 then
			leave lp;
		end if;
		insert into t values(i);
		set i = i + 1;
	end loop;
end
//