參考:《MySQL必知必會》Ben Forta著,第23章 使用存儲過程,第24章 使用遊標
存儲過程
考慮以下的情形:
爲了處理訂單,需要覈對以保證庫存中有相應的物品。
如果庫存有物品,這些物品需要預定以便不將它們再賣給別的人,並且要減少可用的物品數量以反映正確的庫存量。
庫存中沒有的物品需要訂購,這需要與供應商進行某種交互。
關於哪些物品入庫(並且可以立即發貨)和哪些物品退訂,需要通知相應的客戶。
執行這些處理需要針對許多表的多條MySQL語句。此外,需要執行的具體語句及其次序也不是固定的,它們可能會(和將)根據哪些物品在庫存中哪些不在而變化。 => 可以創建存儲過程。
存儲過程簡單來說,就是爲以後的使用而保存的一條或多條MySQL語句的集合,可將其視爲批文件(作用不限於批處理)。
爲什麼要使用存儲過程
簡單、安全、高性能
1)通過把處理封裝在容易使用的單元中,簡化複雜的操作(正如前面例子所述)。
2)由於不要求反覆建立一系列處理步驟,這保證了數據的完整性。
=> 這一點的延伸就是防止錯誤,保證數據的一致性。
3)簡化對變動的管理。如果表名、列名或業務邏輯(或別的內容)有變化,只需要更改存儲過程的代碼。
=> 這一點的延伸就是安全性。通過存儲過程限制對基礎數據的訪問減少了數據錯誤的機會。
4)提高性能。因爲使用存儲過程比使用單獨的SQL語句要快。
使用存儲過程
MySQL稱存儲過程的執行爲調用,因此MySQL執行存儲過程的語句爲CALL。CALL接受存儲過程的名字以及需要傳遞給它的任意參數。存儲過程可以顯示結果,也可以不顯示結果。 => 有點像函數~
1.創建存儲過程
【例】一個返回產品平均價格的存儲過程
mysql> DELIMITER //
mysql> CREATE PROCEDURE productpricing()
-> BEGIN
-> SELECT AVG(prod_price) AS priceaverage FROM products;
-> END //
Query OK, 0 rows affected (0.00 sec)
mysql> DELIMITER ;
mysql>
注意:如果使用的是mysql命令行實用程序,
默認的MySQL語句分隔符爲";"。mysql命令行實用程序也使用";"作爲語句分隔符。
如果命令行實用程序要解釋存儲過程自身內的";"字符,會出現如下句法錯誤。
mysql> CREATE PROCEDURE productpricing()
-> BEGIN
-> SELECT AVG(prod_price) AS priceaverage FROM products;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 3
mysql>
解決辦法是臨時更改命令行實用程序的語句分隔符,除"\"符號外,任何字符都可以用作語句分隔符。
DELIMITER // => (注意有空格)告訴命令行實用程序使用//作爲新的語句結束分隔符
這樣,存儲過程體內的 ";" 仍然保持不動,並且正確地傳遞給數據庫引擎
最後,爲恢復爲原來的語句分隔符,可使用 "DELIMITER ;"
2.執行存儲過程
mysql> CALL productpricing();
+--------------+
| priceaverage |
+--------------+
| 16.133571 |
+--------------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
3.刪除存儲過程
mysql> DROP PROCEDURE productpricing;
Query OK, 0 rows affected (0.00 sec)
mysql>
注意:DROP只給出存儲過程名,沒有使用後面的括號
4.使用參數
IN 後的變量類似函數傳入的參數
OUT後的變量類似函數的返回值
【例】如下存儲過程接受3個參數:pl存儲產品最低價格,ph存儲產品最高價格,pa存儲產品平均價格。
mysql> CREATE PROCEDURE productpricing(
-> OUT pl DECIMAL(8,2),
-> OUT ph DECIMAL(8,2),
-> OUT pa DECIMAL(8,2)
-> )
-> BEGIN
-> SELECT Min(prod_price)
-> INTO pl
-> FROM products;
-> SELECT Max(prod_price)
-> INTO ph
-> FROM products;
-> SELECT Avg(prod_price)
-> INTO pa
-> FROM products;
-> END //
Query OK, 0 rows affected (0.00 sec)
mysql>
說明:每個參數必須具有指定的類型,這裏使用十進制值。
MySQL支持IN(傳遞給存儲過程)、OUT(從存儲過程傳出,如這裏所用)和INOUT(對存儲過程傳入和傳出)類型的參數。
存儲過程的代碼位於BEGIN和END語句內,如前所見,它們是一系列SELECT語句,用來檢索值,然後保存到相應的變量(通過指定INTO關鍵字)。
調用該存儲過程需要指定3個變量名,注意:MySQL變量以@開頭
mysql> CALL productpricing(@pricelow, @pricehigh, @priceaverage)//
Query OK, 1 row affected, 1 warning (0.00 sec)
mysql> SELECT @pricelow, @pricehigh, @priceaverage//
+-----------+------------+---------------+
| @pricelow | @pricehigh | @priceaverage |
+-----------+------------+---------------+
| 2.50 | 55.00 | 16.13 |
+-----------+------------+---------------+
1 row in set (0.00 sec)
【例】同時使用IN和OUT參數
mysql> CREATE PROCEDURE ordertotal(
-> IN onumber INT,
-> OUT ototal DECIMAL(8,2)
-> )
-> BEGIN
-> SELECT Sum(item_price*quantity) FROM orderitems WHERE order_num = onumber
-> INTO ototal;
-> END//
Query OK, 0 rows affected (0.00 sec)
mysql> CALL ordertotal(20005,@total)//
Query OK, 1 row affected (0.00 sec)
mysql> SELECT @total//
+--------+
| @total |
+--------+
| 149.87 |
+--------+
1 row in set (0.00 sec)
mysql>
mysql> DELIMITER ;
檢查存儲過程
爲顯示用來創建一個存儲過程的CREATE語句,使用SHOW CREATE PROCEDURE語句,
eg: SHOW CREATE PROCEDURE ordertotal;
爲了獲得包括何時、由誰創建等詳細信息的存儲過程列表,使用SHOW PROCEDURE STATUS 列出所有存儲過程。
可使用LIKE指定一個過濾模式限制輸出,
eg: SHOW PROCEDURE STATUS LIKE 'ordertotal';
mysql> SHOW CREATE PROCEDURE ordertotal\G;
*************************** 1. row ***************************
Procedure: ordertotal
sql_mode: STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `ordertotal`(
IN onumber INT,
OUT ototal DECIMAL(8,2)
)
BEGIN
SELECT Sum(item_price*quantity) FROM orderitems WHERE order_num = onumber
INTO ototal;
END
character_set_client: utf8
collation_connection: utf8_general_ci
Database Collation: latin1_swedish_ci
1 row in set (0.00 sec)
ERROR:
No query specified
mysql>
mysql> SHOW PROCEDURE STATUS LIKE 'ordertotal'\G;
*************************** 1. row ***************************
Db: supply
Name: ordertotal
Type: PROCEDURE
Definer: root@localhost
Modified: 2020-03-28 16:45:34
Created: 2020-03-28 16:45:34
Security_type: DEFINER
Comment:
character_set_client: utf8
collation_connection: utf8_general_ci
Database Collation: latin1_swedish_ci
1 row in set (0.00 sec)
ERROR:
No query specified
mysql>
建立智能存儲過程
考慮這個場景 -> 獲得訂單合計,且需要對合計增加營業稅,不過只針對某些顧客。那麼,你需要做下面幾件事情:
- 獲得合計;
- 把營業稅有條件地添加到合計;
- 返回合計(帶或不帶稅)。
【例】ordertotal.sql內容如下,包含存儲過程ordertotal,使用命令 ./mysql -uroot -p < ordertotal.sql 導入。
DELIMITER //
USE supply//
DROP PROCEDURE IF EXISTS ordertotal//
-- Name ordertotal
-- Parameters: onumber = order number
-- taxable = 0 if not taxable, 1 if taxable
-- ototal = order total variable
CREATE PROCEDURE ordertotal(
IN onumber INT,
IN taxable BOOLEAN,
OUT ototal DECIMAL(8,2)
) COMMENT 'Obtain order total, optionally adding tax'
BEGIN
-- Declare variable for total
DECLARE total DECIMAL(8,2);
-- Declare variable for tax percentage
DECLARE taxrate INT DEFAULT 6;
-- Get the order total
SELECT Sum(item_price*quantity)
FROM orderitems
WHERE order_num = onumber
INTO total;
-- Is this taxable?
IF taxable THEN
-- Yes, so add taxrate to the total
SELECT total+(total/100*taxrate) INTO total;
END IF;
-- Finally, save to out variable
SELECT total INTO ototal;
END//
DELIMITER ;
說明:
參數taxable是一個布爾值(如果要增加稅則爲真,否則爲假)。
COMMENT關鍵字 不是必需的,如果給出,將在SHOW PROCEDURE STATUS的結果中顯示。
在存儲過程體中,用DECLARE語句定義了兩個局部變量。DECLARE要求指定變量名和數據類型,它也支持可選的默認值(這個例子中的taxrate的默認被設置爲6)。
IF語句檢查taxable是否爲真,如果爲真,則用另一SELECT語句增加營業稅到局部變量total。
最後,用另一SELECT語句將total(它增加或許不增加營業稅)保存到ototal。
登錄數據庫驗證,訂單數爲20005的商品的總計金額 不含營業稅 vs 包含營業稅 =>
mysql> CALL ordertotal(20005,0,@total);SELECT @total;
Query OK, 1 row affected (0.00 sec)
+--------+
| @total |
+--------+
| 149.87 |
+--------+
1 row in set (0.00 sec)
mysql> CALL ordertotal(20005,1,@total);SELECT @total;
Query OK, 1 row affected (0.00 sec)
+--------+
| @total |
+--------+
| 158.86 |
+--------+
1 row in set (0.00 sec)
mysql>
遊標
遊標(cursor)是一個存儲在MySQL服務器上的數據庫查詢,它不是一條SELECT語句,而是被該語句檢索出來的結果集。
在存儲了遊標之後,應用程序可以根據需要滾動或瀏覽其中的數據。
MySQL遊標只能用於存儲過程(和函數)。
使用遊標
DECLARE 在能夠使用遊標前,必須聲明(定義)它。這個過程實際上沒有檢索數據,它只是定義要使用的SELECT語句。
OPEN 聲明後,必須打開遊標以供使用。這個過程用前面定義的SELECT語句把數據實際檢索出來。
FETCH 對於填有數據的遊標,根據需要取出(檢索)各行。
CLOSE 在結束遊標使用時,必須關閉遊標。
【例1】創建一個存儲過程,在其中聲明一個遊標,打開遊標,取一次數據,再關閉遊標
=> 從結果可以看出取了第一行的數據
mysql> DELIMITER //
mysql> DROP PROCEDURE IF EXISTS processorders//
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> CREATE PROCEDURE processorders(
-> OUT o INT
-> )
-> BEGIN
-> DECLARE ordernumbers CURSOR
-> FOR
-> SELECT order_num FROM orders;
->
-> OPEN ordernumbers;
-> FETCH ordernumbers INTO o;
-> CLOSE ordernumbers;
->
-> END//
Query OK, 0 rows affected (0.00 sec)
mysql> DELIMITER ;
mysql>
mysql> CALL processorders(@onum);
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @onum;
+-------+
| @onum |
+-------+
| 20005 |
+-------+
1 row in set (0.00 sec)
mysql>
【例2】創建一個存儲過程,在其中聲明一個遊標,打開遊標,取三次數據,再關閉遊標
=> 從結果可以看出從第一行開始,逐行取了三次數據
mysql> DELIMITER //
mysql> DROP PROCEDURE IF EXISTS processorders//
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE PROCEDURE processorders(
-> OUT o1 INT,
-> OUT o2 INT,
-> OUT o3 INT
-> )
-> BEGIN
-> DECLARE ordernumbers CURSOR
-> FOR
-> SELECT order_num FROM orders;
->
-> OPEN ordernumbers;
-> FETCH ordernumbers INTO o1;
-> FETCH ordernumbers INTO o2;
-> FETCH ordernumbers INTO o3;
-> CLOSE ordernumbers;
->
-> END//
Query OK, 0 rows affected (0.00 sec)
mysql> DELIMITER ;
mysql> CALL processorders(@onum1,@onum2,@onum3);
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @onum1,@onum2,@onum3;
+--------+--------+--------+
| @onum1 | @onum2 | @onum3 |
+--------+--------+--------+
| 20005 | 20009 | 20006 |
+--------+--------+--------+
1 row in set (0.00 sec)
mysql> select * from ordertotals;
+-----------+---------+
| order_num | total |
+-----------+---------+
| 20005 | 158.86 |
| 20009 | 40.78 |
| 20006 | 58.30 |
| 20007 | 1060.00 |
| 20008 | 132.50 |
| 20008 | 132.50 |
+-----------+---------+
6 rows in set (0.00 sec)
mysql>
【例3】processorders.sql內容如下,包含遊標ordernumbers,使用命令 ./mysql -uroot -p < processorders.sql 導入。
DELIMITER //
USE supply//
DROP PROCEDURE IF EXISTS processorders//
CREATE PROCEDURE processorders()
BEGIN
-- Declare local variables
DECLARE done BOOLEAN DEFAULT 0;
DECLARE o INT;
DECLARE t DECIMAL(8,2);
-- Declare the cursor
DECLARE ordernumbers CURSOR
FOR
SELECT order_num FROM orders;
-- Declare continue handler
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1;
-- Create a table to store the results
CREATE TABLE IF NOT EXISTS ordertotals
(order_num INT, total DECIMAL(8,2));
-- Open the cursor
OPEN ordernumbers;
-- Loop through all rows
REPEAT
-- Get order number
FETCH ordernumbers INTO o;
-- Get the total for this order
CALL ordertotal(o,1,t);
-- Insert order and total into ordertotals
INSERT INTO ordertotals(order_num,total)
VALUES(o,t);
-- End of loop
UNTIL done END REPEAT;
-- Close the cursor
CLOSE ordernumbers;
END//
DELIMITER ;
說明:
1.CONTINUE HANDLER 是在條件出現時被執行的代碼。這裏,它指出當SQLSTATE '02000’出現時,SET done=1。
SQLSTATE '02000’是一個未找到條件,當REPEAT由於沒有更多的行供循環而不能繼續時,出現這個條件。
2.FETCH取每個order_num,然後用CALL執行另一個存儲過程ordertotal來計算每個訂單的帶稅的合計(結果存儲到t)。
最後,用INSERT保存每個訂單的訂單號和合計。此存儲過程不返回數據,但會創建新表並填充數據。
比較調用存儲過程processorders前後,創建了新表ordertotals =>
mysql> SHOW TABLES;
+--------------------+
| Tables_in_supply |
+--------------------+
| customeremaillist |
| customers |
| orderitems |
| orderitemsexpanded |
| orders |
| productcustomers |
| productnotes |
| products |
| vendorlocations |
| vendors |
+--------------------+
10 rows in set (0.00 sec)
mysql> CALL processorders();
Query OK, 1 row affected (0.32 sec)
mysql> SHOW TABLES;
+--------------------+
| Tables_in_supply |
+--------------------+
| customeremaillist |
| customers |
| orderitems |
| orderitemsexpanded |
| orders |
| ordertotals |
| productcustomers |
| productnotes |
| products |
| vendorlocations |
| vendors |
+--------------------+
11 rows in set (0.00 sec)
查看錶ordertotals的內容 =>
mysql> select * from ordertotals;
+-----------+---------+
| order_num | total |
+-----------+---------+
| 20005 | 158.86 |
| 20009 | 40.78 |
| 20006 | 58.30 |
| 20007 | 1060.00 |
| 20008 | 132.50 |
| 20008 | 132.50 |
+-----------+---------+
6 rows in set (0.00 sec)
mysql>