一、初識存儲過程
1、什么是存儲過程
存儲過程是在大型數據庫系統中一組為了完成特定功能的SQL語句集,存儲在數據庫中。存儲過程經過第一次編譯后,再次調用不需要編譯,用戶可以通過指定的存儲過程名和給出一些存儲過程定義的參數來使用它。一般用的較少,和腳本有類似之處。
Java,Python,PHP等應用程序可以調用存儲過程。自MySQL 5.0版本以來,存儲過程,存儲函數,觸發器和事件這些功能才被添加到MySQL數據庫引擎
2、為什么要用存儲過程
程序分兩種,一種是基于web,一種是基于桌面,他們都和數據庫進行交互來完成數據的存取工作。假設現在有一種應用程序包含了這兩種,現在要修改其中的一個查詢sql語句,那么我們可能要同時修改他們中對應的查詢sql語句,當我們的應用程序很龐大很復雜的時候問題就出現這,不易維護!另外把sql查詢語句放在我們的web程序或桌面中很容易遭到sql注入的破壞。而存儲過程正好可以幫我們解決這些問題。
3、存儲過程優缺點
優點
- 增強SQL語言的功能和靈活性:存儲過程可以用控制語句編寫,有很強的靈活性,可以完成復雜的判斷和較復雜的運算。
- 標準組件式編程:存儲過程被創建后,可以在程序中被多次調用,而不必重新編寫該存儲過程的SQL語句。而且數據庫專業人員可以隨時對存儲過程進行修改,對應用程序源代碼毫無影響。
- 較快的執行速度:如果某一操作包含大量的Transaction-SQL代碼或分別被多次執行,那么存儲過程要比批處理的執行速度快很多。因為存儲過程是預編譯的。在首次運行一個存儲過程時查詢,優化器對其進行分析優化,并且給出最終被存儲在系統表中的執行計劃。而批處理的Transaction-SQL語句在每次運行時都要進行編譯和優化,速度相對要慢一些。
- 減少網絡流量:針對同一個數據庫對象的操作(如查詢、修改),如果這一操作所涉及的Transaction-SQL語句被組織進存儲過程,那么當在客戶計算機上調用該存儲過程時,網絡中傳送的只是該調用語句,從而大大減少網絡流量并降低了網絡負載。
- 作為一種安全機制來充分利用:通過對執行某一存儲過程的權限進行限制,能夠實現對相應的數據的訪問權限的限制,避免了非授權用戶對數據的訪問,保證了數據的安全。
缺點
- 可移植性差
- 對于簡單的SQL語句,存儲過程沒什么優勢,不一定會減少網絡傳輸
- 如果只有一個用戶使用數據庫,那么存儲過程對安全也沒什么影響
- 團隊開發時需要先統一標準,否則后期維護成本大
- 在大并發量訪問的情況下,不宜寫過多涉及運算的存儲過程
- 業務邏輯復雜時,特別是涉及到對很大的表進行操作的時候,不如在前端先簡化業務邏輯
- 如果使用大量存儲過程,那么使用這些存儲過程的每個連接的內存使用量將會大大增加。此外,如果您在存儲過程中過度使用大量邏輯操作,則CPU使用率也會增加,因為數據庫服務器的設計不當于邏輯運算。
- 存儲過程的構造使得開發具有復雜業務邏輯的存儲過程變得更加困難。
- 很難調試存儲過程。只有少數數據庫管理系統允許您調試存儲過程。不幸的是,MySQL不提供調試存儲過程的功能。
- 開發和維護存儲過程并不容易。開發和維護存儲過程通常需要一個不是所有應用程序開發人員擁有的專業技能。這可能會導致應用程序開發和維護階段的問題。
存儲過程能不用盡量不用。原則是:業務邏輯不要封裝在數據庫里面(數據庫去進行邏輯判斷業務)。把業務邏輯要交給應用程序處理。這樣可以減少數據庫資源消耗。人員也難以招聘,因為既懂存儲過程,又懂業務的人少。使用困難。大量業務邏輯封裝在存儲過程中,造成后面根本就不能動了。動a影響b。以后業務邏輯很難剝離出來。增加以后維護困難
4、存儲過程和函數
相同點
- 存儲過程和函數都是為了可重復執行操作數據庫的 sql 語句的集合
- 存儲過程和函數都是一次編譯,后續執行
不同點
- 標識符不同,函數是 function,過程是 procedure
- 函數中有返回值,過程沒有返回值
- 函數中不能使用 select 語句,而過程可以使用
- 函數最后可以通過 select 語句使用,過程通過 call 語句使用
二、存儲過程的創建
創建存儲過程
注:創建相同名字的存儲過程不會成功,即不能覆蓋一個已經存在的存儲過程。可以先刪除然后再創建。
CREATE[DEFINER = { user | CURRENT_USER }]PROCEDURE sp_name ([proc_parameter[,...]])[characteristic ...] routine_bodyproc_parameter:[ IN | OUT | INOUT ] param_name typecharacteristic: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[begin_label:] BEGIN[statement_list]……END [end_label]
MYSQL 存儲過程中的關鍵語法
注:如果在命令行模式下進行存儲過程創建,需要修改語句結束符,避免沖突。使用工具可以不修改語句結束符,示例如下
DELIMITER $$
或
DELIMITER //
聲明存儲過程:
CREATE PROCEDURE demo_in_parameter(IN p_in int)
存儲過程開始和結束符號:
BEGIN .... END
變量賦值:
SET @p_in=1
變量定義:
DECLARE l_int int unsigned default 4000000;
下面是存儲過程的例子,刪除給定球員參加的所有比賽:
mysql> delimiter $$ # 將語句的結束符號從分號;臨時改為兩個$$(可以是自定義)
mysql> CREATE PROCEDURE delete_matches(IN p_playerno INTEGER)-> BEGIN-> DELETE FROM MATCHES->???WHERE playerno = p_playerno;-> END$$
Query OK, 0 rows affected (0.01 sec)
mysql> delimiter; # 將語句的結束符號恢復為分號
解析:默認情況下,存儲過程和默認數據庫相關聯,如果想指定存儲過程創建在某個特定的數據庫下,那么在過程名前面加數據庫名做前綴。 在定義過程時,使用 DELIMITER $$ 命令將語句的結束符號從分號 ; 臨時改為兩個 $$,使得過程體中使用的分號被直接傳遞到服務器,而不會被客戶端(如mysql)解釋。
存儲過程體
存儲過程體包含了在過程調用時必須執行的語句,例如:dml、ddl語句,if-then-else和while-do語句、聲明變量的declare語句等
過程體格式:以begin開始,以end結束(可嵌套)
BEGINBEGINBEGINstatements;ENDEND
END
注意:每個嵌套塊及其中的每條語句,必須以分號結束,表示過程體結束的begin-end塊(又叫做復合語句compound statement),則不需要分號。
如果過程體中只有一條指令,則可以省略 begin 和 end,存儲過程體中的每條sql語句的結尾要求必須加分號。
為語句塊貼標簽
[begin_label:] BEGIN[statement_list]
END [end_label]
例如:
label1: BEGINlabel2: BEGINlabel3: BEGINstatements;END label3 ;END label2;
END label1
標簽有兩個作用:
- 增強代碼的可讀性
- 在某些語句(例如:leave和iterate語句),需要用到標簽
捕獲異常
declare continue handler for SQLEXCEPTION set e=1;
三、存儲過程的參數
MySQL存儲過程的參數用在存儲過程的定義,共有三種參數類型,IN,OUT,INOUT,形式如:
CREATE PROCEDURE 存儲過程名([[IN |OUT |INOUT ] 參數名 數據類形...])IN 輸入參數:表示調用者向過程傳入值(傳入值可以是字面量或變量)OUT 輸出參數:表示過程向調用者傳出值(可以返回多個值)(傳出值只能是變量)INOUT 輸入輸出參數:既表示調用者向過程傳入值,又表示過程向調用者傳出值(值只能是變量)
1、in 輸入參數
mysql> delimiter $$
mysql> create procedure in_param(in p_in int)-> begin-> select p_in;-> set p_in=2;->???select P_in;-> end$$
mysql> delimiter ;
mysql> set @p_in=1; # 用戶變量命名最好加@
mysql> call in_param(@p_in);
+------+
| p_in |
+------+
|??? 1 |
+------+
+------+
| P_in |
+------+
|??? 2 |
+------+mysql> select @p_in;
+-------+
| @p_in |
+-------+
|???? 1 |
+-------+
# 以上可以看出,p_in 在存儲過程中被修改,但并不影響 @p_id 的值,因為前者為局部變量、后者為全局變量。
2、out輸出參數
mysql> delimiter //
mysql> create procedure out_param(out p_out int)->?? begin->???? select p_out;->???? set p_out=2;->???? select p_out;->?? end-> //
mysql> delimiter ;
mysql> set @p_out=1;
mysql> call out_param(@p_out);
+-------+
| p_out |
+-------+
|? NULL |
+-------+
# 因為out是向調用者輸出參數,不接收輸入的參數,所以存儲過程里的p_out為null+-------+
| p_out |
+-------+
|???? 2 |
+-------+mysql> select @p_out;
+--------+
| @p_out |
+--------+
|????? 2 |
+--------+
# 調用了out_param存儲過程,輸出參數,改變了p_out變量的值
3、inout輸入參數
mysql> delimiter $$
mysql> create procedure inout_param(inout p_inout int)->?? begin->???? select p_inout;->???? set p_inout=2;->???? select p_inout;->?? end-> $$
mysql> delimiter ;
mysql> set @p_inout=1;
mysql> call inout_param(@p_inout);
+---------+
| p_inout |
+---------+
|?????? 1 |
+---------+
+---------+
| p_inout |
+---------+
|?????? 2 |
+---------+
mysql> select @p_inout;
+----------+
| @p_inout |
+----------+
|??????? 2 |
+----------+
# 調用了inout_param存儲過程,接受了輸入的參數,也輸出參數,改變了變量
注意:
1、如果過程沒有參數,也必須在過程名后面寫上小括號 例:
CREATE PROCEDURE sp_name ([proc_parameter[,...]]) ……
2、確保參數的名字不等于列的名字,否則在過程體中,參數名被當做列名來處理
四、變量
1. 變量定義
局部變量聲明一定要放在存儲過程體的開始:
DECLARE variable_name [,variable_name...] datatype [DEFAULT value];
其中,datatype 為 MySQL 的數據類型,如: int, float, date,varchar(length),例如:
DECLARE l_int int unsigned default 4000000;?
DECLARE l_numeric number(8,2) DEFAULT 9.95;?
DECLARE l_date date DEFAULT '1999-12-31';?
DECLARE l_datetime datetime DEFAULT '1999-12-31 23:59:59';?
DECLARE l_varchar varchar(255) DEFAULT 'This will not be padded';
2. 變量賦值
SET 變量名 = 表達式值 [,variable_name = expression ...]
3.使用SELECT …INTO語句為變量賦值
在MySQL存儲過程中,可以使用SELECT …INTO語句對變量進行賦值,該語句在數據庫中進行查詢,并將得到的結果賦值給變量。SELECT …INTO語句的語法格式如下:
SELECT col_name[,...] INTO var_name[,...] table_exprcol_name:要從數據庫中查詢的列字段名;var_name:變量名,列字段名按照在列清單和變量清單中的位置對應,將查詢得到的值賦給對應位置的變量;table_expr:SELECT語句中的其余部分,包括可選的FROM子句和WHERE子句。
需要注意的是,在使用SELECT …INTO語句時,變量名不能和數據表中的字段名相同,否則會出錯。范例語句:
create procedure getMsg?()?
Begindeclare v_title varchar(30);?declare v_content varchar(100);?select title,content into v_title,v_content from news where artId=333;?
End?
將變量值返回給調用者
在存儲過程中定義的變量,經過一系列的處理之后,結果值可能需要返回給存儲過程調用者。那么如何返回呢?方便的做法是使用SELECT語句將變量作為結果集返回,因此,在上面一段代碼的基礎上,加上一句:
create procedure getMsg?()?
Begindeclare v_title varchar(30);?declare v_content varchar(100);?select title,content into v_title,v_content from news where artId=333;?select v_title,v_content;?
End
4. 用戶變量
在MySQL客戶端使用用戶變量
mysql > SELECT 'Hello World' into @x;?
mysql > SELECT @x;?
+-------------+?
|?? @x??????? |?
+-------------+?
| Hello World |?
+-------------+?
mysql > SET @y='Goodbye Cruel World';?
mysql > SELECT @y;?
+---------------------+?
|???? @y????????????? |?
+---------------------+?
| Goodbye Cruel World |?
+---------------------+?mysql > SET @z=1+2+3;?
mysql > SELECT @z;?
+------+?
| @z?? |?
+------+?
|? 6?? |?
+------+
在存儲過程中使用用戶變量
mysql > CREATE PROCEDURE GreetWorld( ) SELECT CONCAT(@greeting,' World');?
mysql > SET @greeting='Hello';?
mysql > CALL GreetWorld( );?
+----------------------------+?
| CONCAT(@greeting,' World') |?
+----------------------------+?
|? Hello World?????????????? |?
+----------------------------+
在存儲過程間傳遞全局范圍的用戶變量
mysql> CREATE PROCEDURE p1()?SET @last_procedure='p1';?
mysql> CREATE PROCEDURE p2() SELECT CONCAT('Last procedure was ',@last_procedure);?
mysql> CALL p1( );?
mysql> CALL p2( );?
+-----------------------------------------------+?
| CONCAT('Last procedure was ',@last_proc?????? |?
+-----------------------------------------------+?
| Last procedure was p1???????????????????????? |?
+-----------------------------------------------+
注意:用戶變量名一般以@開頭,濫用用戶變量會導致程序難以理解及管理
五、注釋
MySQL 存儲過程可使用兩種風格的注釋
- 兩個橫桿--:該風格一般用于單行注釋。
- c 風格: 一般用于多行注釋。
六、MySQL存儲過程的調用
用call和你過程名以及一個括號,括號里面根據需要,加入參數,參數包括輸入參數、輸出參數、輸入輸出參數。調用存儲過程示例:
call sp_name[(傳參)];
七、MySQL存儲過程的查詢
我們像知道一個數據庫下面有那些表,我們一般采用 showtables; 進行查看。那么我們要查看某個數據庫下面的存儲過程,是否也可以采用呢?答案是,我們可以查看某個數據庫下面的存儲過程,但是是另一種方式。我們可以用以下語句進行查詢:
# 查看所有的存儲過程
select name from mysql.proc where type='PROCEDURE';
# 當然也可以指定數據庫名來縮小范圍
select name from mysql.proc where type='PROCEDURE' and db='數據庫名';#
select routine_name from information_schema.routines where routine_schema='數據庫名';# 顯示數據庫中所有存儲的存儲過程基本信息,包括所屬數據庫,存儲過程名稱,創建時間等
show procedure status [where db='數據庫名'];
如果我們想知道,某個存儲過程的詳細,那我們又該怎么做呢?是不是也可以像操作表一樣用describe 表名進行查看呢?答案是:我們可以查看存儲過程的詳細,但是需要用另一種方法:
SHOW CREATE PROCEDURE 數據庫.存儲過程名;
就可以查看當前存儲過程的詳細。
八、MySQL存儲過程的修改
ALTER PROCEDURE
修改存儲過程只能修改那些選項(這里不講解那些具體選項,想了解的可以自行百度),并不能修改傳入傳出參數或者sql語句
更改用 CREATE PROCEDURE 建立的預先指定的存儲過程,其不會影響相關存儲過程或存儲功能。
九、MySQL存儲過程的刪除
刪除一個存儲過程比較簡單,和刪除表一樣:
語法:drop procedure 存儲過程名
#示例DROP PROCEDURE p1;#錯誤演示,不支持批量刪除DROP PROCEDURE p2,p3;
十、MySQL存儲過程的控制語句
(1). 變量作用域
內部的變量在其作用域范圍內享有更高的優先權,當執行到 end 變量時,內部變量消失,此時已經在其作用域外,變量不再可見了,應為在存儲過程外再也不能找到這個申明的變量,但是你可以通過 out 參數或者將其值指派給會話變量來保存其值。
(2). 條件語句
1. if-then-else 語句
if 條件 then語句;
else語句;
end if;if 條件 then語句;
elseif 條件 then語句;
.....
else語句;
end if;
示例:?
mysql > DELIMITER //?
mysql > CREATE PROCEDURE proc2(IN parameter int)?-> begin-> declare var int;?-> set var=parameter+1;?-> if var=0 then-> insert into t values(17);?-> end if;?-> if parameter=0 then-> update t set s1=s1+1;?-> else-> update t set s1=s1+2;?-> end if;?-> end;?-> //?
mysql > DELIMITER ;
2. case語句:
case [變量名]when [值] then[執行內容]when [值] then[執行內容]...
else[執行內容]
end case;
示例:?
mysql > DELIMITER //?
mysql > CREATE PROCEDURE proc3 (in parameter int)?-> begin-> declare var int;?-> set var=parameter+1;?-> case var?-> when 0 then??-> insert into t values(17);?-> when 1 then??-> insert into t values(18);?-> else??-> insert into t values(19);?-> end case;?-> end;?-> //?
mysql > DELIMITER ;
casewhen var=0 theninsert into t values(30);when var>0 thenwhen var<0 thenelse
end case
(3). 循環語句
1. while ···· end while
while語句,先判斷后運行
while 條件 do--循環體
endwhile
示例:
mysql > DELIMITER //?
mysql > CREATE PROCEDURE proc4()?-> begin-> declare var int;?-> set var=0;?-> while var<6 do?-> insert into t values(var);?-> set var=var+1;?-> end while;?-> end;?-> //?
mysql > DELIMITER ;
2. repeat···· end repea
它在執行操作后檢查結果,而 while 則是執行前進行檢查。
repeat--循環體
until 循環條件?
end repeat;
示例:
mysql > DELIMITER //?
mysql > CREATE PROCEDURE proc5 ()?-> begin??-> declare v int;?-> set v=0;?-> repeat?-> insert into t values(v);?-> set v=v+1;?-> until v>=5?-> end repeat;?-> end;?-> //?
mysql > DELIMITER ;
3. loop ·····endloop
loop 循環不需要初始條件,這點和 while 循環相似,同時和 repeat 循環一樣不需要結束條件, leave 語句的意義是離開循環。
loop語句,運行直到遇到leave
[標簽名]:loop[執行內容]leave [標簽名][執行內容]
end loop;
示例
mysql > DELIMITER //?
mysql > CREATE PROCEDURE proc6 ()?-> begin-> declare v int;?-> set v=0;?-> LOOP_LABLE:loop?-> insert into t values(v);?-> set v=v+1;?-> if v >=5 then-> leave LOOP_LABLE;?-> end if;?-> end loop;?-> end;?-> //?mysql > DELIMITER ;
4. LABLES 標號:
標號可以用在 begin repeat while 或者 loop 語句前,語句標號只能在合法的語句前面使用。可以跳出循環,使運行指令達到復合語句的最后一步。
(4). ITERATE迭代,相當于continue,LEAVE 結束,相當于break
ITERATE 通過引用復合語句的標號,來從新開始復合語句:
LEAVE 結束循環
mysql > DELIMITER //?
mysql > CREATE PROCEDURE proc10 ()?-> begin-> declare v int;?-> set v=0;?-> LOOP_LABLE1:loop?-> if v=3 then??-> set v=v+1;?-> ITERATE LOOP_LABLE1;?# 進行下一次循環-> end if;?-> insert into t values(v);?-> set v=v+1;?-> if v>=5 then-> leave LOOP_LABLE1;?# 結束循環-> end if;?-> end loop;?-> end;?-> //?
mysql > DELIMITER ;
十一、mysql 在存儲過程中輸出日志信息
1、直接用select 打印輸出
SELECT 'Comment';
2、用concat連接變量輸出
declare myvar INT default 0;
SET myvar = 5;
SELECT concat('myvar is ', myvar);
輸出: myvar is 5
3、額外創建一個有一列文本列的表,然后往里面塞信息
declare myvar INT default 0;
SET myvar = 5;
insert into tmptable select concat('myvar is ', myvar);
將上面的sql語句封裝成一個存儲過程log,以后要用的話就直接調用下面的語句就可以了CALL log(concat('the value is', myvar));
直接輸出到一個文本里面select "penguin" as log into outfile '/tmp/result.txt';
這個命令會有嚴格的限制,只能將輸出文本放在本地,然后給予其創建和寫的權限
一旦輸出了一個文本,無法重寫,這樣可以阻止惡意執行sql注入
十一、DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE?
在MySQL的存儲過程中經常會看到這句話:DECLARE CONTINUE HANDLER FOR NOT FOUND。
它的含義是:若沒有數據返回,程序繼續,并將變量 done 設為TRUE ,這種情況是出現在select XX into XXX from tablename的時候發生的。
我們在使用儲存過程中使用游標遍歷數據的時候的基本寫法如下:
create procedure proc_test() sql security invoker
begindeclare p_id varchar(32);declare done tinyint default false;declare c_cur cursor for select id from user;declare continue handler for not found set done = true;open c_cur;fetch c_cur into p_id;while !done do... #程序邏輯fetch c_cur into p_id;end while;close c_cur;
end;
正常情況這么寫是沒問題的,可是如果你在while里面的要是有select語句的話就有問題了。如果說你的處理邏輯是這樣的:
while !done doselect * from user_role r where r.user_id = p_id;fetch c_cur into p_id;
end while;
那么當你的select * from user_role r where r.user_id = p_id;找不到數據的時候,declare continue handler for not found set done = true;這句就會執行,有done = true,所以循環體會提前跳出。通過測試得出,declare continue handler for not found set done = true 是對全局的select有效的,只要有一條select語句返回空,那么就是觸發該語句。
解決方法就是確保while里面的select永遠不會返回空
select * from user_role r where r.user_id = p_id;#改成下面這樣
select col1, col2, ... from
(select col1, col2, ... from user_role r where r.user_id = p_id
union all
select '' col1, '' col2, ...) t
這樣的話就可以保證select肯定不是空集合。
?
?