MySQL 筆記7 存儲過程和遊標

參考:《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>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章