使用動態SQL處理table_name作為輸入參數的存儲過程(MySQL)


關於mysql如何創建和使用存儲過程,參考筆記《MySQL存儲過程和函數創建》以及官網:https://dev.mysql.com/doc/refman/5.7/en/create-procedure.html

本篇主要示例使用了輸入參數的存儲過程,並解決使用表名作為輸入參數的問題,因為之前遇到過需要使用表名作為參數的存儲過程,很難處理。

問題描述:

假設我們有TEST1-TEST12共12個相同結構的車輛里程表,我們想要對這12個表進行去重,那么邏輯上比較簡單的辦法是寫12個存儲過程處理或者寫一個存儲過程每執行一次改一次表名並重新編譯,但是這樣都太麻煩了。

接下來很容易的就會想到是否可以使用表名作為輸入參數,這樣每次執行給定表名即可。

因此初始的存儲過程代碼如下:

DELIMITER //
DROP PROCEDURE IF EXISTS Del_Dupilicate;
CREATE DEFINER=`root`@`localhost` PROCEDURE `Del_Dupilicate`(in table_name varchar(64))
BEGIN
DECLARE v_min_id,v_group_count INT;
DECLARE v_get_on_time,v_get_off_time DATETIME;
DECLARE v_car_no VARCHAR(255);
DECLARE done INT DEFAULT FALSE;
DECLARE my_cur CURSOR FOR SELECT get_on_time,get_off_time,car_no,min(id),count(1) AS count FROM table_name GROUP BY get_on_time,get_off_time,car_no HAVING count>1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN my_cur;
myloop: LOOP
FETCH my_cur INTO v_get_on_time,v_get_off_time,v_car_no,v_min_id,v_group_count;
IF done THEN
LEAVE myloop;
END IF;
DELETE FROM table_name WHERE get_on_time=v_get_on_time AND get_off_time=v_get_off_time AND car_no=v_car_no AND id>v_min_id;
COMMIT;
END LOOP;
CLOSE my_cur;
END;
//
DELIMITER ;

上述存儲過程可以正常編譯,但是執行卻一定會報table not exist的錯誤,因為mysql會錯誤的把輸入變量table_name當做真正的數據庫表名,這顯然是錯誤的。

那么如何在SQL中引用變量呢?一個可行的辦法是使用動態SQL,把變量拼入SQL語句中然后執行動態SQL。

所以根據官網(https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html)提供的語法,對於上述procedure中的delete語句可以改寫成如下格式:

set @del_sql=concat('DELETE FROM ',table_name,' WHERE get_on_time=',v_get_on_time,' AND get_off_time=',v_get_off_time,' AND car_no=',v_car_no,' AND id>',v_min_id)
PREPARE stmt FROM @del_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
//注意prepare目前只能在存儲過程中使用,函數和觸發器都不適用。

Ps:需要注意的是官網在示例prepare的語法時使用了?作為占位符,但是經試驗?不能作為表名的占位符(實際上官網只示例了?可以作為整數字面量的占位符,我猜測凡是數據庫對象用?作為占位符都會報錯),想要將表名變量整合入SQL中只能使用concat函數,concat的函數的輸入支持local variables、user defined variables和input variables。

好,delete語句處理完畢,但是對於cursor中的select語句呢?官網明確說明游標中不能使用動態SQL,也就是不能使用prepare語句,那只能換一種思路了。

游標的作用是什么呢?是獲取一個結果集以便進行遍歷,那么可否使用臨時表代替游標來存儲結果集,這樣可以使用動態SQL創建臨時表(mysql的臨時表是session級別的,不同會話可以使用相同名稱的臨時表,會話釋放時臨時表自動刪除):

set @tmp_table_name=concat(table_name,'_tmp');
set @cur_sql=concat('create temporary table ',@tmp_table_name,' as select get_on_time,get_off_time,car_no,min(id) as min_id,count(1) AS count FROM ',table_name,' GROUP BY get_on_time,get_off_time,car_no HAVING count>1');
PREPARE stmt FROM @cur_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
然后整個存儲過程的邏輯就可以更改了,因為我們把中間結果集存入了臨時表,那就無需遍歷cursor了,同時連declare的local variables也省了(因為這些本地變量是用於遍歷游標時存儲列值的),只需要delete ... join即可,因此最終的存儲過程修改為:
CREATE DEFINER=`root`@`localhost` PROCEDURE `Del_Dupilicate`(in table_name varchar(64))
BEGIN

set @tmp_table_name=concat(table_name,'_tmp');

set @cur_sql=concat('create temporary table ',@tmp_table_name,' as select get_on_time,get_off_time,car_no,min(id) as min_id,count(1) AS count FROM ',table_name,' GROUP BY get_on_time,get_off_time,car_no HAVING count>1');
PREPARE stmt FROM @cur_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

set @del_sql=concat('delete a from ',table_name,' a join ',@tmp_table_name,' b on a.get_on_time=b.get_on_time and a.get_off_time=b.get_off_time and a.car_no=b.car_no and a.id != b.min_id');
PREPARE stmt FROM @del_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

set @drop_tmp_sql=concat('drop temporary table ',@tmp_table_name);
PREPARE stmt FROM @drop_tmp_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

END
 調用:
call Del_Dupilicate('TEST1');
 上述存儲過程經過了實測,可以正常的刪除重復數據。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM