如果說前面講的視圖讓你對SQL語言開始有了一些新的理解,那麼這次講的存儲過程和函數就會讓你覺得SQL語言跟其它的編程語言真的很接近,因爲它也像別的語言一樣去封裝函數、定義變量、流程及條件控制、異常捕獲等等。MySQL從5.0版本開始支持存儲過程和函數。
一、什麼是存儲過程和函數
簡單的可以理解成其它語言中封裝的函數一樣,可以調用這個函數來達到某種功能。但也有一些不同,這裏的存儲過程和函數是經過編譯並存儲在數據庫中的一段SQL語句的集合,它可以減少開發人員的工作,將數據處理放在數據庫服務器上減少了與客戶端之間的數據傳輸,但這樣做也會佔用服務器的CPU,給數據庫服務器造成壓力,所以在存儲過程和函數裏面儘量不要涉及大量運算,儘量將他們分攤到應用服務器上去。
存儲過程沒有返回值而函數必須有返回值,存儲過程的參數可以是IN、OUT、INOUT類型,而函數的參數只能是IN類型。
二、存儲過程和函數的相關操作
執行相關操作的時候要確認是否有對應權限,比如創建存儲過程和函數需要有CREATE ROUTINE權限,修改或刪除需要ALTER ROUTINE 權限,執行需要EXECUTE權限。
2.1 創建、修改存儲過程和函數
相關語法如下:
創建存儲過程的語法:
CREATE PROCEDURE sp_name ([ [IN|OUT|INOUT] param_name type [, ...] ])
[ LANGUAGE SQL|[NOT] DETERMINISTIC|{CONTAINS SQL|NO SQL|READS SQL DATA|MODIFIES SQL DATA} | SQL SECURITY {DEFINER|INVOKER}|COMMENT 'string' ]
BEGIN 相應的SQL語句 END
創建函數的語法:
CREATE FUNCTION sp_name ([ [IN|OUT|INOUT] param_name type [, ...] ])
RETURNS type
[ LANGUAGE SQL|[NOT] DETERMINISTIC|{CONTAINS SQL|NO SQL|READS SQL DATA|MODIFIES SQL DATA} | SQL SECURITY {DEFINER|INVOKER}|COMMENT 'string' ]
BEGIN 相應的SQL語句 END
修改存儲過程和函數的語法:
ALTER {PROCEDURE|FUNCTION} sp_name [{CONTAINS SQL|NO SQL|READS SQL DATA|MODIFIES SQL DATA} | SQL SECURITY {DEFINER|INVOKER}|COMMENT 'string']
調用過程語法:
CALL sp_name([parameter[,...]])
對語法裏面的字段解釋如下:
- LANGUAGE SQL:說明下面的語句體裏面是使用SQL語言寫的(默認),雖然MySQL現在只支持SQL,但將來可能會支持別的語言;
- [NOT] DETERMINISTIC:非確定的,每次一樣的輸入不一定有同樣的輸出(默認);
- {CONTAINS SQL|NO SQL|READS SQL DATA|MODIFIES SQL DATA}:CONTAINS SQL(默認)表示子程序不包含讀或寫數據的語句;NO SQL表示子程序不包含SQL語句;READS SQL DATA表示子程序包含讀數據的語句但不包含寫;MODIFIES SQL DATA表示子程序包含寫數據的語句;
- SQL SECURITY {DEFINER|INVOKER}:指定子程序的執行許可,DEFINER創建子程序者的許可(默認);INVOKER調用者的許可;
- COMMENT 'string':存儲過程或者函數的註釋信息。
- type:MySQL裏面支持的數據類型。
注意:MySQL存儲過程和函數中允許包含DDL語句,也允許在存儲過程中執行提交或回滾,但存儲過程和函數中不允許執行LOAD DATA INFILE語句;此外,存儲過程和函數中還可以調用其它的過程和函數。
請看下面創建一個存儲過程的例子:
1. 選擇一個數據庫創建一張目標表並插入數據:
mysql> create table inventory (inventory_id int,film_id int,store_id int);
Query OK, 0 rows affected (0.02 sec)
mysql> insert into inventory values(1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5);
Query OK, 5 rows affected (0.01 sec)
Records: 5 Duplicates: 0 Warnings: 0
2. 創建一個存儲過程
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;
-> select found_rows() into p_film_count;
-> end $$
Query OK, 0 rows affected (0.00 sec)
mysql>
mysql> delimiter ;
3. 不使用存儲過程時我們這樣查找
mysql> select inventory_id from inventory where film_id=2 and store_id = 2 ;
+--------------+
| inventory_id |
+--------------+
| 2 |
+--------------+
1 row in set (0.00 sec)
4. 使用存儲過程時:輸入兩個參數,並得到一個輸出結果@a
mysql> call film_in_stock(2,2,@a);
+--------------+
| inventory_id |
+--------------+
| 2 |
+--------------+
1 row in set (0.00 sec)
mysql> select @a;
+------+
| @a |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
可以看到調用存儲過程與直接執行SQL語句的效果是一樣的,但存儲過程的好處在於邏輯都封裝在數據庫端,調用者只需要瞭解作用不需要知道邏輯,修改與使用都很方便。
下面再舉個例子說明一下SQL SECURITY特徵值的不同:
1. 使用root用戶創建兩個存儲過程,一個是definer,一個是invoker
mysql> delimiter $$
mysql> create procedure film_in_stock_definer(IN p_film_id int,IN p_store_id int,OUT p_film_count int)
-> sql security definer
-> begin
-> select inventory_id from inventory where film_id=p_film_id and store_id=p_store_id;
-> select found_rows() into p_film_count;
-> end $$
Query OK, 0 rows affected (0.00 sec)
mysql> create procedure film_in_stock_invoker(IN p_film_id int,IN p_store_id int,OUT p_film_count int)
-> sql security invoker
-> begin
-> select inventory_id from inventory where film_id=p_film_id and store_id=p_store_id;
-> select found_rows() into p_film_count;
-> end $$
Query OK, 0 rows affected (0.00 sec)
mysql> delimiter ;
2. 創建一個新用戶只賦予它可以執行存儲過程的權限,沒有其它例如查詢inventory表的權限
mysql> grant execute on test1.* to 'lisa'@'localhost' identified by '123';
Query OK, 0 rows affected, 1 warning (0.00 sec)
3. 使用新用戶登陸MySQL,嘗試直接查詢inventory表會出錯
mysql> select count(*) from inventory;
ERROR 1142 (42000): SELECT command denied to user 'lisa'@'localhost' for table 'inventory'
4. lisa用戶來調用兩個存儲過程
mysql> call film_in_stock_definer(2,2,@a);
+--------------+
| inventory_id |
+--------------+
| 2 |
+--------------+
1 row in set (0.00 sec)
Query OK, 1 row affected (0.01 sec)
mysql> call film_in_stock_invoker(2,2,@a);
ERROR 1142 (42000): SELECT command denied to user 'lisa'@'localhost' for table 'inventory'
從上例可以看出,雖然兩個存儲過程都是root用戶創建的,但是一個使用的是創建者本身的權限(也就是root),另一個使用的是調用者的權限(這裏是lisa),由於用戶lisa的權限不能查看inventory表,因而執行film_in_stock_invoker存儲過程會出錯。
2.2 刪除存儲過程或函數
一次只能刪除一個存儲過程或函數,注意要有對應的權限;相關語法如下:
DROP { PROCEDURE | FUNCTION } [ IF EXISTS ] sp_name
mysql> drop procedure film_in_stock;
Query OK, 0 rows affected (0.00 sec)
2.3 查看存儲過程或者函數
1. 查看狀態
SHOW { PROCEDURE | FUNCTION } STATUS [LIKE 'pattern’]
mysql> show procedure status like 'film_in_stock' \G 不需要選擇數據庫就可以查看
*************************** 1. row ***************************
Db: test1
Name: film_in_stock
Type: PROCEDURE
Definer: root@localhost
Modified: 2018-12-21 11:27:14
Created: 2018-12-21 11:27:14
Security_type: DEFINER
Comment:
character_set_client: gbk
collation_connection: gbk_chinese_ci
Database Collation: utf8_general_ci
1 row in set (0.01 sec)
2. 查看定義
SHOW CREATE { PROCEDURE | FUNCTION } sp_name
mysql> show create procedure film_in_stock \G 必須選擇對應的數據庫下才能查看
*************************** 1. row ***************************
Procedure: film_in_stock
sql_mode: STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
Create Procedure: CREATE DEFINER=`root`@`localhost` 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;
select found_rows() into p_film_count;
end
character_set_client: gbk
collation_connection: gbk_chinese_ci
Database Collation: utf8_general_ci
1 row in set (0.00 sec)
3. 通過information_schema.Routines查看相關信息
mysql> select * from routines where routine_name = 'film_in_stock' \G
*************************** 1. row ***************************
SPECIFIC_NAME: film_in_stock
ROUTINE_CATALOG: def
ROUTINE_SCHEMA: test1
ROUTINE_NAME: film_in_stock
ROUTINE_TYPE: PROCEDURE
DATA_TYPE:
CHARACTER_MAXIMUM_LENGTH: NULL
CHARACTER_OCTET_LENGTH: NULL
NUMERIC_PRECISION: NULL
NUMERIC_SCALE: NULL
DATETIME_PRECISION: NULL
CHARACTER_SET_NAME: NULL
COLLATION_NAME: NULL
DTD_IDENTIFIER: NULL
ROUTINE_BODY: SQL
ROUTINE_DEFINITION: begin
select inventory_id from inventory where film_id=p_film_id and store_id=p_store_id;
select found_rows() into p_film_count;
end
EXTERNAL_NAME: NULL
EXTERNAL_LANGUAGE: NULL
PARAMETER_STYLE: SQL
IS_DETERMINISTIC: NO
SQL_DATA_ACCESS: READS SQL DATA
SQL_PATH: NULL
SECURITY_TYPE: DEFINER
CREATED: 2018-12-21 11:27:14
LAST_ALTERED: 2018-12-21 11:27:14
SQL_MODE: STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
ROUTINE_COMMENT:
DEFINER: root@localhost
CHARACTER_SET_CLIENT: gbk
COLLATION_CONNECTION: gbk_chinese_ci
DATABASE_COLLATION: utf8_general_ci
1 row in set (0.00 sec)
2.4 變量的使用
存儲過程和函數中可以使用變量,變量名不區分大小寫。
1. 變量的定義
定義的語法如下:
DECLARE var_name[,...] type [DEFAULT value]
注意用DECLARE定義的是局部變量,該變量的作用範圍只能在BEGIN....END塊中,需寫在其它語句的前面,可以一次聲明多個相同類型的變量,也可爲它設置默認值。
2. 變量的賦值
變量可以用SET直接賦值也可以通過查詢語句賦值(要求查詢返回的結果只能有一行),可以賦常量也可以賦表達式,語法如下:
SET var_name = expr [, var_name = expr] ....
SELECT col_name[, ....] INTO var_name [,....] table_expr
2.5 定義條件和處理
這裏的條件定義和處理可以理解爲其它語言中的錯誤、異常捕獲與處理,我可以把系統處理某個語句發生異常錯誤時的一些狀態信息定義爲爲某個名字,再提前把出現這種錯誤的解決方法定義好,這樣存儲過程和函數在執行的過程中不會因爲某一些錯誤而中斷。
條件的定義:
DECLARE condition_name CONDITION FOR SQLSTATE [VALUE] sqlstate_value|mysql_error_code
條件的處理:
DECLARE {CONTINUE|EXIT|UNDO} HANDLER FOR SQLSTATE [VALUE] sqlstate_value|condition_name|SQLWARNING|NOT FOUND|SQLEXCEPTION|mysql_error_code [,...] sp_statement
- {CONTINUE|EXIT|UNDO}:處理錯誤的方式,CONTINUE表示繼續執行,EXIT表示終止執行,UNDO暫時還不支持;
- SQLSTATE [VALUE] sqlstate_value|condition_name|SQLWARNING|NOT FOUND|SQLEXCEPTION|mysql_error_code:錯誤捕獲的方式,可以是SQL語句自身定義的狀態值,也可以是我們自己定義的condition_name,亦或是mysql定義的錯誤碼,還可以是對sql狀態預定義的SQLWARNING(以01開頭的SQLSTATE代碼速記)、NOT FOUND(以02開頭的SQLSTATE代碼速記)、SQLEXCEPTION(其它沒有被包括的SQLSTATE代碼速記);
下面舉個例子說明一下出現異常時的處理:
1. 我先創建一個actor表並定義actor_id爲主鍵,插入四條數據
mysql> create table actor (actor_id int not null primary key,first_name char(10),last_name char(10));
Query OK, 0 rows affected (0.01 sec)
mysql> insert into actor values(1,'a','a'),(2,'b','b'),(3,'c','c'),(4,'d','d');
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
2. 創建存儲過程
mysql> delimiter $$
mysql>
mysql> create procedure actor_insert()
-> begin
-> set @x = 1;
-> insert into actor values(5,'e','e');
-> set @x = 2;
-> insert into actor values(1,'a','a');
-> set @x = 3;
-> end $$
Query OK, 0 rows affected (0.00 sec)
mysql> delimiter ;
3. 調用存儲過程
mysql> call actor_insert(); 調用報錯,錯誤是主鍵重複,而且通過@x的值爲2可以知道SET @x=3並沒有執行,因此一旦出錯,存儲過程即停止執行
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'
mysql> select @x;
+------+
| @x |
+------+
| 2 |
+------+
1 row in set (0.00 sec)
4. 對主鍵重複這個錯誤進行異常處理
mysql> delimiter $$
mysql> create procedure actor_insert1()
-> begin
-> declare continue handler for sqlstate '23000' set @x2 =1; sqlstate '23000'是前面出錯時提示的錯誤代碼
-> set @x = 1;
-> insert into actor values(5,'e','e'); 這條數據仍然能插入到表中說明前面的存儲過程雖然執行了這條語句但是由於出錯並沒有提交,所以現在還能重新執行
-> set @x = 2;
-> insert into actor values(1,'a','a');
-> set @x = 3;
-> end $$
Query OK, 0 rows affected (0.00 sec)
mysql> delimiter ;
mysql> call actor_insert1();
Query OK, 0 rows affected (0.00 sec)
mysql> select @x,@x2; 這裏得到@x=3說明上面出錯的地方被忽略過了,@x=1說明錯誤處理語句正常執行了
+------+------+
| @x | @x2 |
+------+------+
| 3 | 1 |
+------+------+
1 row in set (0.00 sec)
這裏再提一點,上面用到的錯誤處理代碼sqlstate '23000' 是根據出錯時的提示得到的,不然我也不知道,當然還可以有其它的寫法,如:
--捕獲 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;
2.6 光標的使用
在存儲過程和函數中,可以使用光標對結果集進行循環處理,光標的使用包括四步,其語法分別如下:
1. 聲明光標
DECLARE cursor_name CURSOR FOR select_statement
2. 打開光標
OPEN cursor_name
3. 獲取光標
FETCH cursor_name INTO var_name [,var_name]...
4. 關閉光標
CLOSE cursor_name
下面舉個例子:
這個例子可能會有一些錯誤,但這裏主要是讓大家瞭解光標的使用,暫時就不要糾結這個錯誤問題
mysql> delimiter $$
mysql>
mysql> create procedure actor_stat()
-> begin
-> declare i_actor_id int;
-> declare c_first_name char(10);
-> declare cur_actor cursor for select actor_id,first_name from actor;
-> declare exit handler for not found close cur_actor;
->
-> set @x1=0;
-> set @x2=0;
->
-> open cur_actor;
-> repeat
-> fetch cur_actor into i_actor_id,c_first_name;
-> if i_actor_id =2 then
-> set @x1 = @x1+c_first_name;
-> else
-> set @x2 = @x2+c_first_name
-> end if;
-> until 0 end repeat;
->
-> close cur_actor;
->
-> end $$
mysql> delimiter ;
注意:變量、條件、光標、錯誤處理都是通過DECLARE定義的,他們有先後順序要求,注意按照當前列出的順序寫。
2.7 流程控制
流程控制一共有7中,下面一一介紹。
1. IF語句
IF 語句實現條件判斷執行不同的語句列表
IF condition then statement_list
[ELSEIF condition then statement_list]....
[ELSE statement_list]
END IF
2. CASE 語句
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
WHEN i_actor_id =2 THEN set @x1 = @x1+c_first_name;
ELSE set @x2 = @x2+c_first_name;
END CASE
或
CASE i_actor_id
WHEN 2 THEN set @x1 = @x1+c_first_name;
ELSE set @x2 = @x2+c_first_name;
END CASE
3. LOOP 語句
實現簡單循環,退出的條件需要額外定義,通常使用LEAVE實現,如果沒有退出條件那麼就是死循環
[bengin_label:]LOOP
statement_list
END LOOP [end_label]
4. LEAVE 語句
從標註的流程構造中退出,通常和begin..end或者循環一起使用
例如:
begin
set @x=0;
ins:LOOP
set @x = @x + 1;
if @x = 100 then
leave ins;
END if;
insert into ...
END LOOP ins;
end
5. ITERATE 語句
ITERATE必須用在循環中,作用是跳過當前循環剩下的語句直接開始下一次循環,跟其它語言中的continue一樣
例如:
begin
set @x=0;
ins:LOOP
set @x = @x + 1;
if @x = 100 then
leave ins;
elseif mod(@x,2) = 0 then
iterate ins;
END if;
insert into ...
END LOOP ins;
end
6. REPEAT 語句
有條件的循環控制語句,當滿足條件時退出循環
[begin_label:]REPEAT
statement_list
UNTIL search_condition
END REPEAT [end_label]
7. WHILE 語句
實現有條件的循環控制
[begin_label:] WHILE search_condition DO
statement_list
END WHILE [end_label]
WHILE循環與REPEAT循環的區別就像別的語言中while 與 do while的區別;
2.8 事件調度器
事件調度器可以指定數據庫按照一定時間週期觸發某種操作,可以理解爲時間觸發器,類似於Linux系統下的任務調度器,在MySQL5.1版本後纔有該功能。
創建一個簡單的事件調度器
CREATE EVENT myevent
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR
DO UPDATE myschema.mytable SET mycol = mycol + 1;
說明:
myevent 事件調度器名稱
ON SCHEDULE 字句指定事件在合適執行及執行頻次
DO子句指定要具體執行的操作或事件
下面通過一個完整的實例來展示事件調度器的使用:
1. 創建表及事件調度器,每5秒向表中插入一條數據
mysql> create table test(id1 varchar(10),create_time datetime);
Query OK, 0 rows affected (0.02 sec)
mysql> create event test_event_1
-> on schedule every 5 second
-> do insert into test1.test(id1,create_time) values('test',now());
Query OK, 0 rows affected (0.00 sec)
2. 查看調度器狀態
mysql> show events \G
*************************** 1. row ***************************
Db: test1
Name: test_ecent_1
Definer: root@localhost
Time zone: SYSTEM
Type: RECURRING
Execute at: NULL
Interval value: 5
Interval field: SECOND
Starts: 2018-12-21 17:33:32
Ends: NULL
Status: ENABLED
Originator: 1
character_set_client: gbk
collation_connection: gbk_chinese_ci
Database Collation: utf8_general_ci
1 row in set (0.00 sec)
3. 隔幾秒後查看錶test,發現並沒有數據插入(一開始我這裏是插入語句寫錯了所以沒有數據插進去)
mysql> select * from test;
Empty set (0.00 sec)
4. 查看事件調度器的狀態,發現默認是關閉的
mysql> show variables like '%scheduler%';
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| event_scheduler | OFF |
+-----------------+-------+
1 row in set, 1 warning (0.00 sec)
5. 通過以下命令打開調度器,同時查看後臺進程
mysql> set global event_scheduler =1;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like '%scheduler%';
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| event_scheduler | ON |
+-----------------+-------+
1 row in set, 1 warning (0.00 sec)
mysql> show processlist \G
*************************** 1. row ***************************
Id: 6
User: root
Host: localhost:6650
db: test1
Command: Query
Time: 0
State: starting
Info: show processlist
*************************** 2. row ***************************
Id: 7
User: event_scheduler 一個新的事件調度器進程
Host: localhost
db: NULL
Command: Daemon
Time: 2
State: Waiting for next activation
Info: NULL
2 rows in set (0.00 sec)
7. 隔幾秒後再次查看test表
mysql> select * from test;
+------+---------------------+
| id1 | create_time |
+------+---------------------+
| test | 2018-12-21 17:44:02 |
| test | 2018-12-21 17:44:07 |
| test | 2018-12-21 17:44:13 |
| test | 2018-12-21 17:44:17 |
+------+---------------------+
4 rows in set (0.00 sec)
8. 爲防止表一直插入變得很大,創建一個新的調度器每個1分鐘清空一次test表
mysql> create event trunc_test on schedule every 1 minute
-> do truncate table test;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test; 最開始的那些記錄已經被清除掉了,這類觸發器非常適合定時清空臨時表或者日誌表
+------+---------------------+
| id1 | create_time |
+------+---------------------+
| test | 2018-12-21 17:45:57 |
| test | 2018-12-21 17:46:02 |
| test | 2018-12-21 17:46:07 |
| test | 2018-12-21 17:46:12 |
| test | 2018-12-21 17:46:17 |
| test | 2018-12-21 17:46:22 |
| test | 2018-12-21 17:46:27 |
| test | 2018-12-21 17:46:32 |
| test | 2018-12-21 17:46:37 |
| test | 2018-12-21 17:46:42 |
| test | 2018-12-21 17:46:47 |
+------+---------------------+
11 rows in set (0.00 sec)
9. 如果調度器不再使用可以禁用或刪除
mysql> alter event test_event_1 disable;
Query OK, 0 rows affected (0.00 sec)
mysql> drop event test_event_1;
Query OK, 0 rows affected (0.00 sec)
關於事件調度器還有很多其它的選項,比如開始結束的事件,指定執行次數等等,用到的時候可以查查相應資料。
- 事件調度器的優勢:MySQL的事件調度器部署在數據庫內部,安全,不會讓一般人誤操作;數據庫遷移時不需要額外遷移定時任務,因爲它已經包含了調度事件的遷移。
- 適用場景:適用於定期收集統計信息、定期清理歷史數據、定期數據庫檢查(如自動監控和恢復slave失敗進程);
- 注意事項:在繁忙且性能要求高的數據庫上要慎用事件調度器;過於複雜的事件處理儘量用程序實現而不是用事件調度器;開啓和關閉事件調度器需要具有超級用戶權限。