通常對 Stored Procedure 的印象,就是將指令寫死 (Hard Coded) 在程式中,較少採取動態的方式來產生。本篇所謂的動態 SQL,就是在開發 Stored Procedure 時,在程式中利用變數組成 SQL 的指令字串;通常可用兩種方法來執行,即 sp_executesql 和 exec。本文將針對 sp_executesql 進行介紹,希望能對各位有所助益。
再提醒一點,SQL 動態指令可發揮強大的功能,它實現了靈活的開發方式;但凡事有利便有弊,它也可能成為資安的漏洞,讓駭客有可趁之機 (如:SQL Injection)。所以當您用這種方式進行開發時,請特別注意使用時機與防護機制。
NOTE 以下範例旨在協助您了解所介紹的內容,於實際開發時,請注意 SQL Injection 的問題。
首先,建立測試資料 (加入 5 筆測試資料於 DETAIL_RECORD Table 中)。
Create Table DETAIL_RECORD( Record_Date datetime, Expense_Name nvarchar(50), Expense_Amount int, Memo nvarchar(500) Constraint PK_HIS_DETAIL_RECORD Primary Key (Record_Date)); Insert Into DETAIL_RECORD (Record_Date, Expense_Name, Expense_Amount, Memo) Values ('2012/01/01 08:00:00', '花費 1', 10000, '說明 1'); Insert Into DETAIL_RECORD (Record_Date, Expense_Name, Expense_Amount, Memo) Values ('2012/01/02 09:00:00', '花費 2', 20000, '說明 2'); Insert Into DETAIL_RECORD (Record_Date, Expense_Name, Expense_Amount, Memo) Values ('2012/01/02 10:00:00', '花費 3', 30000, '說明 3'); Insert Into DETAIL_RECORD (Record_Date, Expense_Name, Expense_Amount, Memo) Values ('2012/01/02 20:00:00', '花費 4', 40000, '說明 4'); Insert Into DETAIL_RECORD (Record_Date, Expense_Name, Expense_Amount, Memo) Values ('2012/01/05 21:00:00', '花費 5', 50000, '說明 5');
再建立測試 Stored Procedure (找出該期間的第一筆花費明細)
if object_id ('usp_First_Expense') is not null drop proc usp_First_Expense go Create Proc usp_First_Expense @prm_start_datetime datetime, @prm_end_datetime datetime, @rtn_record_date datetime output, @rtn_expense_name nvarchar(50) output, @rtn_expense_amount int output as set nocount on select top 1 @rtn_record_date = Record_Date, @rtn_expense_name = Expense_Name, @rtn_expense_amount = Expense_Amount from DETAIL_RECORD where Record_Date >= @prm_Start_Datetime and Record_Date < @prm_End_Datetime order by Record_Date asc set nocount off return
以下開始解說...
新增資料:利用變數值組成 INSERT 指令字串,再以 sp_executesql 執行。
declare @i_name nvarchar(50), @i_amount int, @i_memo nvarchar(500), @sql nvarchar(2000) set @i_name = '花費 6' set @i_amount = 990 set @i_memo = '說明 6' set @sql = 'insert into DETAIL_RECORD (Record_Date, Expense_Name, Expense_Amount, Memo) ' + 'values(' + '''' + cast(getdate() as varchar(20)) + '''' + ', ' + + '''' + @i_name + '''' + ', ' + + cast(@i_amount as varchar(10)) + ', ' + + '''' + @i_memo + '''' + '); ' exec sp_executesql @sql -- 執行結果 select * from DETAIL_RECORD -- 組成的動態查詢字串 select @sql as Your_SQL_String
修改資料:利用變數值組成 UPDATE 指令字串,再以 sp_executesql 執行。
[範例] 將 2012/01/05 20:00:00 ~ 23:59:59 間的花費名稱,改為 "額外花費"。
declare @i_name nvarchar(50), @i_start_datetime datetime, @i_end_datetime datetime, @sql nvarchar(2000) set @i_name = '額外花費' set @i_start_datetime = '2012/01/05 20:00:00' set @i_end_datetime = '2012/01/06 00:00:00' set @sql = 'update DETAIL_RECORD ' + ' set Expense_Name = ' + '''' + @i_name + '''' + ' where Record_Date >= ' + '''' + cast(@i_start_datetime as varchar(20)) + '''' + ' and Record_Date < ' + '''' + cast(@i_end_datetime as varchar(20)) + '''' exec sp_executesql @sql -- 執行結果 select * from DETAIL_RECORD -- 組成的動態查詢字串 select @sql as Your_SQL_String
刪除資料:利用變數值組成 DELETE 指令字串,再以 sp_executesql 執行。
[範例] 刪除 2012/01/06 00:00:00 以後的所有資料
declare @i_datetime datetime, @sql nvarchar(2000) set @i_datetime = '2012/01/06 00:00:00' set @sql = 'delete from DETAIL_RECORD ' + ' where Record_Date > ' + '''' + cast(@i_datetime as varchar(20)) + '''' exec sp_executesql @sql -- 執行結果 select * from DETAIL_RECORD -- 組成的動態查詢字串 select @sql as Your_SQL_String
查詢資料 & 接收回傳值:利用變數值組成 SELECT 指令字串,再以 sp_executesql 執行。當需要針對資料逐筆處理、且藉由變數值重組查詢字串時,可採取此種方式(較常使用在 Stored Procedure 的 Cursor 中)。這裡出現一個問題,該如何接收動態 SQL 指令的結果回傳值呢? 其實並不困難,sp_executesql 可透過參數將值傳出來。在以下範例中,sp_executesql 會將輸出值傳給參數 @inner1、@inner2、@inner3、@inner4;而我們就用 o_record_date、o_expense_name、o_expense_amount、o_memo 4 個參數來做一對一的接收。也就是說,所查詢的結果會被指定給 o_record_date、o_expense_name、o_expense_amount、o_memo 等參數。
[範例] 查詢 2012/1/2 最後一筆花費
declare @boundary_datetime datetime, @i_datetime datetime, @o_record_date datetime, @o_expense_name nvarchar(50), @o_expense_amount int, @o_memo nvarchar(500), @sql nvarchar(2000) set @i_datetime = '2012/01/03' set @sql = 'select top 1 @inner1 = Record_Date, @inner2 = Expense_Name, @inner3 = Expense_Amount, @inner4 = Memo ' + ' from DETAIL_RECORD ' + ' where Record_Date < ' + '''' + cast(@i_datetime as varchar(20)) + '''' + ' order by Record_Date desc; ' exec sp_executesql @sql, N'@inner1 datetime output, @inner2 nvarchar(50) output, @inner3 int output, @inner4 nvarchar(500) output ', @inner1 = @o_record_date output, @inner2 = @o_expense_name output, @inner3 = @o_expense_amount output, @inner4 = @o_memo output -- 執行結果 select @o_record_date as Record_Date, @o_expense_name as Expense_Name, @o_expense_amount as Expense_Amount, @o_memo as Memo -- 組成的動態查詢字串 select @sql as Your_SQL_String
定義 Cursor
[範例] 以 Cursor 讀出 Table 資料
declare @sql nvarchar(2000), @tmp_record_date datetime, @tmp_expense_name nvarchar(50), @tmp_expense_amount int, @tmp_memo nvarchar(500) -- 定義 Cursor set @sql = 'declare TestCSR cursor for ' + 'select Record_Date, Expense_Name, Expense_Amount, Memo ' + ' from DETAIL_RECORD; ' exec sp_executesql @sql -- 測試 Cursor 是否建置成功 open TestCSR fetch next from TestCSR into @tmp_record_date, @tmp_expense_name, @tmp_expense_amount, @tmp_memo while @@fetch_status = 0 begin select @tmp_record_date as Record_Date, @tmp_expense_name as Expense_Name, @tmp_expense_amount as Expense_Amount, @tmp_memo as Memo fetch next from TestCSR into @tmp_record_date, @tmp_expense_name, @tmp_expense_amount, @tmp_memo end close TestCSR deallocate TestCSR -- 組成的動態查詢字串 select @sql as Your_SQL_String
呼叫 Stored Procedure & 接收回傳值
建議直接使用 exec,請參考 這篇文章 (於該篇文章中,以「呼叫 Stored Procedure & 接收回傳值」為關鍵字進行搜尋)。下例為 sp_executesql 搭配 exec 使用方式:
[範例] 呼叫 Stored Procedure (usp_First_Expense) 找出 2012/01/02 00:00:00 至 2012/01/03 00:00:00 期間的第一筆花費
declare @i_start_datetime datetime, @i_end_datetime datetime, @o_record_date datetime, @o_expense_name nvarchar(50), @o_expense_amount int, @sql nvarchar(2000) set @i_start_datetime = '2012/01/02 00:00:00' set @i_end_datetime = '2012/01/03 00:00:00' set @sql = 'exec usp_First_Expense ' + '''' + cast(@i_start_datetime as varchar(20)) + '''' + ', ' + + '''' + cast(@i_end_datetime as varchar(20)) + '''' + ', ' + + '@inner1 output, ' + + '@inner2 output, ' + + '@inner3 output' exec sp_executesql @sql, N'@inner1 datetime output, @inner2 nvarchar(50) output, @inner3 int output ', @inner1 = @o_record_date output, @inner2 = @o_expense_name output, @inner3 = @o_expense_amount output -- 執行結果 select @o_record_date as Record_Date, @o_expense_name as Expense_Name, @o_expense_amount as Expense_Amount -- 組成的動態查詢字串 select @sql as Your_SQL_String
欲直接使用 EXEC 指令的讀者,請參考我的另一篇文章:Transact-SQL 動態指令:使用 EXEC (Dynamic SQL Command Using EXEC)。