存儲過程和函數
概覽
存儲過程和函數是事先編譯並存儲在數據庫中的一段SQL語句的集合。5.0版本開始支持。
存儲過程:無返回值,參數可使用IN、OUT和INOUT類型;
函數:必須有返回值,參數只能是IN類型。
優點
1、存儲過程和函數可重複使用,調用存儲過程和函數可簡化開發人員的工作量;
2、調用存儲過程和函數只需要傳遞存儲過程/函數名稱和參數即可,能夠減少數據在數據庫和應用服務器之間的傳輸;
3、安全性高,可設定用戶的使用權限。
主要操作
本筆記記錄的例子都是使用官方提供的demo庫sakila來進行練習的。
創建
創建存儲過程語法如下:
create
[definer = {user | current_user}]
procedure `sp_name` ([proc_parameter[, ...]])
[characteristic ...] `routine_body`;
創建函數語法與存儲過程類似,只是多了返回值類型的定義:
create
[definer = {user | current_user}]
function `func_name` ([func_parameter[, ...]])
returns type
[characteristic ...] `routine_body`;
definer:可選,指明存儲過程/函數創建用戶。
characteristic:特徵值,可選取值如下
language sql 系統默認,說明過程/函數body是以MySQL編寫。
deterministic/not deterministic 系統默認爲not deterministic。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,則會調用失敗;若該值爲definer,則可正常調用。舉個例子:
先創建一個簡單的存儲過程,功能是向actor表中信息數據:
delimiter $$
create procedure actor_insert(p_first_name varchar(45), p_last_name varchar(45))
modifies sql data
sql security definer
begin
insert into actor(first_name, last_name)
values (p_first_name, p_last_name);
end $$
Query OK, 0 rows affected (0.00 sec)
delimiter ;
使用當前用戶測試一下存儲過程actor_insert:
call actor_insert('JIM', 'Carey');
Query OK, 1 row affected (0.01 sec)
插入成功。現在我們再創建一個對actor表沒有insert權限的用戶test,來調用這個存儲過程,因爲當前設置了sql security definer,因此調用能夠成功:
call sakila.actor_insert('xx', 'xx');
Query OK, 1 row affected (0.00 sec)
接下來修改存儲過程(alter procedure/function用於修改過程/函數的一些特徵值,如果要修改routine_body則需要刪除重新創建):
alter procedure actor_insert
sql security invoker;
Query OK, 0 rows affected (0.00 sec)
此時再調用一下:
call sakila.actor_insert('xx', 'xx');
ERROR 1142 (42000): INSERT command denied to user 'test'@'localhost' for table 'actor'
由於調用者test沒有actor表的insert權限,因此調用過程失敗。
查看
1、查看存儲過程/函數的狀態:show { procedure | function } status [like 'pattern'];
例子:查看actor_insert表狀態
show procedure status like 'actor_insert'\G;
*************************** 1. row ***************************
Db: sakila
Name: actor_insert
Type: PROCEDURE
Definer: root@localhost
Modified: 2020-02-09 22:46:32
Created: 2020-02-09 17:53:05
Security_type: INVOKER
Comment:
character_set_client: utf8
collation_connection: utf8_general_ci
Database Collation: latin1_swedish_ci
1 row in set (0.00 sec)
2、查看存儲過程/函數的定義:show create { procedure | function } `sp_name`;
例子:查看actor_list表定義語句
show create procedure sakila.actor_insert\G;
*************************** 1. row ***************************
Procedure: actor_insert
sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `actor_insert`(p_first_name varchar(45), p_last_name varchar(45))
MODIFIES SQL DATA
SQL SECURITY INVOKER
begin
insert into actor(first_name, last_name)
values (p_first_name, p_last_name);
end
character_set_client: utf8
collation_connection: utf8_general_ci
Database Collation: latin1_swedish_ci
1 row in set (0.00 sec)
3、所有的存儲過程/函數,都存放在information_schema庫的routines表中,可以通過routines表查詢存儲過程/函數信息:
select * from information_schema.routines where routine_name='actor_insert'\G;
*************************** 1. row ***************************
SPECIFIC_NAME: actor_insert
ROUTINE_CATALOG: def
ROUTINE_SCHEMA: sakila
ROUTINE_NAME: actor_insert
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
insert into actor(first_name, last_name)
values (p_first_name, p_last_name);
end
EXTERNAL_NAME: NULL
EXTERNAL_LANGUAGE: NULL
PARAMETER_STYLE: SQL
IS_DETERMINISTIC: NO
SQL_DATA_ACCESS: MODIFIES SQL DATA
SQL_PATH: NULL
SECURITY_TYPE: INVOKER
CREATED: 2020-02-09 17:53:05
LAST_ALTERED: 2020-02-09 22:46:32
SQL_MODE: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
ROUTINE_COMMENT:
DEFINER: root@localhost
CHARACTER_SET_CLIENT: utf8
COLLATION_CONNECTION: utf8_general_ci
DATABASE_COLLATION: latin1_swedish_ci
1 row in set (0.01 sec)
刪除
刪除存儲過程/函數語法如下:
drop { procedure | function } [if exists] `sp_name`;
需要注意,刪除存儲過程/函數的用戶需要擁有該存儲過程/函數alter routine權限,且一次只能刪除一個存儲過程/函數。
變量
變量定義
定義變量的語句如下:
declare var_name[, ...] var_type [default_value]
一次可定義多個相同類型的變量。變量的作用範圍限於begin...end塊中,且要位於塊中所有語句之前。
變量賦值
1、直接賦值,使用set,可以賦常量或表達式:
set `var_name`=expr [, `var_name` expr] ...;
2、通過查詢賦值(查詢的返回結果必須只有一行):
select `col_name`[, ...] into var_name[, ...] table_expr;
變量及其賦值可以參考sakila庫的inventory_in_stock函數:
BEGIN
DECLARE v_rentals INT;
DECLARE v_out INT;
SELECT COUNT(*) INTO v_rentals
FROM rental
WHERE inventory_id = p_inventory_id;
...
SELECT COUNT(rental_id) INTO v_out
FROM inventory LEFT JOIN rental USING(inventory_id)
WHERE inventory.inventory_id = p_inventory_id
AND rental.return_date IS NULL;
...
END
可以看到這個函數中,首先定義了變量v_rentals和v_out,因爲它們數據類型相同,這一句也可以改寫爲:
declare v_rentals, v_out int;
後面的語句都是通過查詢結果對這兩個變量賦值。從出租表rental中統計出庫存id與參數p_inventory_id相等的記錄數量即指定貨物出租的次數,並將其賦值給v_rentals;從庫存表inventory以及出租表rental中統計指定貨物出租且未歸還的次數,並賦值給v_out。
條件
條件的定義和處理,用於定義在語句執行過程中遇到問題時的處理方式,相當於Java中使用try...catch捕獲、處理異常。條件的定義及處理語法如下:
-- 相當於爲condition_value起別名爲condition_name
declare condition_name condition for condition_value;
-- handler_type即對於條件發生後的處理方式,取值有以下兩種:
-- 1、continue:跳過條件發生的語句,繼續執行下一條
-- 2、exit:整個程序終止執行
declare handler_type handler
for condition_value[, ...]
statement;
condition_value的取值有以下幾種:
1、MySQL錯誤碼
2、SQLState值
3、通過declare定義的condition_name
4、SQLWARNING,即01開頭的SQLState值,表示警告
5、NOT FOUND,即02開頭的SQLState值,表示無數據
6、SQLEXCEPTION,表示所有沒有被SQLWARNING和NOT FOUND捕獲的SQLState值
流程控制
if語句
還是以上文中出現過的sakila中的函數inventory_in_stock爲例:
BEGIN
DECLARE v_rentals INT;
DECLARE v_out INT;
...
IF v_rentals = 0 THEN
RETURN TRUE;
END IF;
...
IF v_out > 0 THEN
RETURN FALSE;
ELSE
RETURN TRUE;
END IF;
END
可以看到,if語句實現條件判斷,與一般的編程語言類似,以if...end if包裹內容塊,語法可以概括爲:
if search_condition then statement_list
[elseif search_condition then statement_list] ...
[else statement_list]
end if
case語句
case語句也用於實現條件判斷,語法如下:
case case_value
when when_value then statement_list
[when when_value then statement_list] ...
end case
嘗試用case語句來改寫上文if語句中的示例代碼:
case v_rentals
when 0 then return true;
end case;
case v_out
when 0 the return true;
else return false;
end case;
在實際使用中,case更多被用於較爲複雜的條件判斷中,在有很多條件取值的情況下,case語句比if語句更爲清晰。
loop & leave語句
loop語句可以實現簡單的循環,退出循環則可以使用leave語句配合,語法如下:
[loop_label]:loop
statement_list
leave [loop_label]
end loop [loop_label]
做個簡單的練習,還是使用sakila庫,構造一個存儲過程,連續向actor表中插入10條數據:
USE `sakila`;
DROP procedure IF EXISTS `actor_insert`;
DELIMITER $$
USE `sakila`$$
CREATE PROCEDURE `actor_insert` ()
modifies sql data
BEGIN
set @i = 0;
ins: loop
if @i = 10 then
leave ins;
end if;
insert into actor(`first_name`, `last_name`) values ('test_loop', @i);
set @i = @i + 1;
end loop ins;
END$$
DELIMITER ;
最後可以看看過程中定義的@i的值是否爲10,即可判斷是否循環了10次:
select @i;
+------+
| @i |
+------+
| 10 |
+------+
1 row in set (0.01 sec)
iterate語句
iterate用於循環中跳過當前循環剩下的語句、直接進入下一輪循環。重新定義一下上文中的過程actor_insert,使@i爲偶數時插入數據,否則不插入,@i最小爲0、最大爲10:
USE `sakila`;
DROP procedure IF EXISTS `actor_insert`;
DELIMITER $$
USE `sakila`$$
CREATE DEFINER=`root`@`%` PROCEDURE `actor_insert`()
MODIFIES SQL DATA
BEGIN
set @i = 0;
ins: loop
set @i = @i + 1;
if @i > 10 then
leave ins;
end if;
if mod(@i, 2) = 0 then
iterate ins;
else
insert into actor(`first_name`, `last_name`) values ('test_iterate', @i);
end if;
end loop ins;
END$$
DELIMITER ;
repeat語句
repeat語句用於實現有條件控制的循環,當滿足條件時即可退出循環。語法如下:
[begin_label:] repeat
statement_list
util search_condition
end repeat [end_label];
我們還是以上文的過程actor_insert爲例,將其改爲用repeat語句控制循環:
USE `sakila`;
DROP procedure IF EXISTS `actor_insert`;
DELIMITER $$
USE `sakila`$$
CREATE DEFINER=`root`@`%` PROCEDURE `actor_insert`()
MODIFIES SQL DATA
BEGIN
set @i = 0;
ins: repeat
insert into actor(`first_name`, `last_name`) values ('test_repeat', @i);
set @i = @i + 1;
until @i = 10
end repeat ins;
END$$
DELIMITER ;
while語句
while語句也是實現有條件控制的循環。與repeat語句相比,while語句相當於編程語言中的while...do循環,而repeat相當於while...do循環。語法如下:
[begin_label:] while search_condition do
statement_list
end while [end_label];
我們還是以上文的過程actor_insert爲例,將其改爲用while語句控制循環:
USE `sakila`;
DROP procedure IF EXISTS `actor_insert`;
DELIMITER $$
USE `sakila`$$
CREATE DEFINER=`root`@`%` PROCEDURE `actor_insert`()
MODIFIES SQL DATA
BEGIN
set @i = 0;
ins: while @i < 10 do
insert into actor(`first_name`, `last_name`) values ('test_while', @i);
set @i = @i + 1;
end while ins;
END$$
DELIMITER ;
光標
光標,用於存儲過程/函數中,對結果集進行循環處理。
通過一個例子理解光標的使用。寫一個存儲過程,查詢payment表中的記錄,根據不同的staff_id來對各個員工的銷售額amount進行累加:
USE `sakila`;
DROP procedure IF EXISTS `payment_amount_count`;
DELIMITER $$
USE `sakila`$$
CREATE PROCEDURE `payment_amount_count` ()
reads sql data
BEGIN
declare i_staff_id int;
declare d_staff_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 @staff1 = 1;
set @staff2 = 2;
open cur_payment;
repeat
fetch cur_payment into i_staff_id, d_staff_amount;
case i_staff_id
when 1 then set @staff1 = @staff1 + d_staff_amount;
when 2 then set @staff2 = @staff2 + d_staff_amount;
end case;
until 0
end repeat;
close cur_payment;
END$$
DELIMITER ;
select @staff1, @staff2;
+----------+----------+
| @staff1 | @staff2 |
+----------+----------+
| 33490.47 | 33924.05 |
+----------+----------+
1 row in set (0.00 sec)
上述存儲過程中,運用了很多上文記錄的內容,如變量的定義與賦值、條件處理、if語句、case語句和repeat語句。另一方面,可以通過該存儲過程,總結出光標的使用過程:
1、declare cur_name cursor for select_statement:聲明光標;
2、open cur_name:打開光標;
3、fetch cur_name into var_name[, ...]:移動光標;
4、close cur_name:關閉光標。
事件調度器
使用事件調度器,可以使數據庫安自定義的時間週期觸發某種操作,可以理解爲時間觸發器。
事件調度器適用於定期收集統計信息、定期清理歷史數據、定期數據庫檢查,但是在繁忙且要求性能的數據庫服務器上要慎重部署和啓用,同時,開啓和關閉事件調度器需要超級用戶權限。
創建事件調度器的語法如下:
create event `event_name`
on schedule <schedule>
do
<event_body>;
舉個例子,創建一個test表,每隔兩秒向表中插入一條數據,一分鐘後停止插入:
首先創建test表:
create table test(
id int primary key auto_increment comment '主鍵',
last_update datetime not null comment '最後更新時間'
) charset utf8mb4;
接下來創建一個事件調度器,定時執行指定任務:
create event test_insert_event
on schedule
every 2 second ends current_timestamp + interval 1 minute
do
insert into test.test(last_update) value(now());
爲了任務調度器能夠執行,我們還需要開啓調度器:
set global event_scheduler=1;
此時就可以看看test表中是否有數據增長了。
另外,我們可以通過show events;語句來查看當前事件調度器信息:
show events\G;
*************************** 1. row ***************************
Db: test
Name: test_insert_event
Definer: root@localhost
Time zone: SYSTEM
Type: RECURRING
Execute at: NULL
Interval value: 2
Interval field: SECOND
Starts: 2020-02-12 00:42:46
Ends: 2020-02-12 00:44:46
Status: ENABLED
Originator: 6
character_set_client: utf8
collation_connection: utf8_general_ci
Database Collation: utf8mb4_general_ci
1 row in set (0.00 sec)
當任務調度器執行結束,該event會自動銷燬。如果想要提前結束事件,則可以通過以下語句:
-- 刪除
drop event event_name;
-- 或者,禁用
alter event event_name disable;
觸發器
觸發器是與表有關的數據庫對象,我們可以爲某張表定義觸發器,當滿足觸發器定義的條件時執行觸發器中定義的語句集合。相當於編程語言中的事件監聽與回調,而這裏的監聽對象是一張表。
查看觸發器
官方示例庫sakila中已經定義了數個觸發器,我們可以先查看一下這些觸發器。
查看數據庫中所有觸發器
show triggers\G;
這條語句可以用於查看數據庫中所有的觸發器信息,但不能查詢指定的觸發器。
查看指定觸發器
類似於視圖、存儲過程/函數,information_schema庫中也存放了所有的觸發器信息,具體是在triggers表中,可以通過指定觸發器名稱查詢該表,來查詢指定的觸發器。如查詢觸發器ins_film:
select * from information_schema.triggers where trigger_name='ins_film'\G;
查詢結果如下:
*************************** 1. row ***************************
TRIGGER_CATALOG: def
TRIGGER_SCHEMA: sakila
TRIGGER_NAME: ins_film
EVENT_MANIPULATION: INSERT
EVENT_OBJECT_CATALOG: def
EVENT_OBJECT_SCHEMA: sakila
EVENT_OBJECT_TABLE: film
ACTION_ORDER: 1
ACTION_CONDITION: NULL
ACTION_STATEMENT: BEGIN
INSERT INTO film_text (film_id, title, description)
VALUES (new.film_id, new.title, new.description);
END
ACTION_ORIENTATION: ROW
ACTION_TIMING: AFTER
ACTION_REFERENCE_OLD_TABLE: NULL
ACTION_REFERENCE_NEW_TABLE: NULL
ACTION_REFERENCE_OLD_ROW: OLD
ACTION_REFERENCE_NEW_ROW: NEW
CREATED: 2020-02-03 23:36:41.96
SQL_MODE: STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,TRADITIONAL,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
DEFINER: root@localhost
CHARACTER_SET_CLIENT: utf8mb4
COLLATION_CONNECTION: utf8mb4_general_ci
DATABASE_COLLATION: latin1_swedish_ci
1 row in set (0.01 sec)
創建觸發器
我們可以根據上文的ins_film這個觸發器,來總結創建觸發器的語法。
首先,觸發器需要一個作用對象,這個對象必須是一張表,即ins_film中的event_object_table: film;然後,還需要一個觸發的條件,即ins_film中的event_manipulation: insert;接着還要有觸發的時機,是在觸發條件之前呢還是之後,即ins_film中的action_timing: after;最後就是觸發器的SQL語句集了,可以看到ins_film的行爲是在有新的數據插入film表之後,將相同的數據也插入到film_text表。在SQL語句中,目標行爲新產生的記錄對象使用new表示,原先的記錄對象用old表示。還需要注意,MySQL觸發器僅支持行觸發。
可以總結出創建觸發器的語法如下:
create trigger trigger_name
trigger_timing
trigger_event
on table_name
for each row
trigger_body;
trigger_timing 即觸發的時機,取值有before和after,表示在目標行爲之前執行SQL語句集還是之後;
trigger_event 即觸發行爲,取值有insert、update和delete。
作爲練習,創建觸發器test_insert_customer,當有新的記錄插入customer表時,將新紀錄的first_name、last_name和create_date列值寫入customer_added表中。
先創建customer_added表:
create table customer_added(
id int primary key auto_increment comment '主鍵',
first_name varchar(45) not null comment '名',
last_name varchar(45) not null comment '姓',
create_time datetime
) charset utf8mb4;
再創建觸發器test_insert_customer:
delimiter $$
create trigger test_insert_customer
after insert
on customer
for each rows
begin
insert into customer_added(first_name, last_name, create_time)
values(new.first_name, new.last_name, new.create_date);
end; $$
delimiter ;
最後驗證一下,向customer表中插入一條記錄:
insert into customer(`store_id`, `first_name`, `last_name`, `address_id`, `active`) values (1, 'Tom', 'Smith', 602, 1);
select * from customer where first_name='Tom' and last_name='Smith';
+-------------+----------+------------+-----------+-------+------------+--------+---------------------+---------------------+
| customer_id | store_id | first_name | last_name | email | address_id | active | create_date | last_update |
+-------------+----------+------------+-----------+-------+------------+--------+---------------------+---------------------+
| 602 | 1 | Tom | Smith | NULL | 602 | 1 | 2020-02-12 15:47:58 | 2020-02-12 15:47:58 |
+-------------+----------+------------+-----------+-------+------------+--------+---------------------+---------------------+
1 row in set (0.00 sec)
檢查一下customer_added表:
select * from customer_added;
+----+------------+-----------+---------------------+
| id | first_name | last_name | create_time |
+----+------------+-----------+---------------------+
| 1 | Tom | Smith | 2020-02-12 15:47:58 |
+----+------------+-----------+---------------------+
1 row in set (0.00 sec)
數據正確地插入了customer_added表,因此我們創建的觸發器無誤。
刪除觸發器
使用drop trigger一次可以刪除一個觸發器,未指定庫名的情況下默認是當前庫:
drop trigger [schema_name.]trigger_name;
例如刪除上文創建的觸發器test_insert_customer:
drop trigger sakila.test_insert_customer;
Query OK, 0 rows affected (0.00 sec)