通常對 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)。