2010年4月15日 星期四

SQL 的 while exists

在 MS SQL 裡,盡量避免用 cursor 去處理迴圈的運算,你也許聽人說過。不管是考量效能問題也好,還是恐嚇你也好,但...下一步呢?你的問題還是需要去解決。有些人可能會提議說,那,就建立一個暫存資料表,處理過一筆資料後,就刪除他,直到這資料表沒資料為止。這真的是一個簡單好方法,可是,你有考慮過效能問題嗎?我們來做個實驗。

想要推薦的方法,是使用 while exists ( ... ) 來解決。

while exists 範例:

declare @seqsn bigint
set @seqsn=682

declare @id bigint
select  top 1 @id=gcf_id from prog_checkpoint_first 
where gcf_seqsn=@seqsn  order by gcf_id

--Do Your Job here
print @id

set @id=@id+1
while exists (select * from prog_checkpoint_first 
where gcf_seqsn=@seqsn and gcf_id>=@id)
begin

 --Do Your Job here
 select  top 1 @id=gcf_id from prog_checkpoint_first 
 where gcf_seqsn=@seqsn and gcf_id>=@id  order by gcf_id

 print @id

 set @id=@id+1

end

使用暫存資料表的方法:

declare @seqsn bigint
set @seqsn=682

declare @id bigint


select  gcf_id into #tmp from prog_checkpoint_first  
where gcf_seqsn=@seqsn  

while (select count(*) from #tmp)>0
begin
 select top 1 @id=gcf_id from #tmp 
 
 --Do Your Job here
 print @id

 delete #tmp where gcf_id = @id
end
drop table #tmp

我接著在自己的資料庫上建了一個測試資料表:prog_checkpoint_first



CREATE TABLE [dbo].[prog_checkpoint_first](
 [gcf_id] [bigint] IDENTITY(1,1) NOT NULL,
 [gcf_seqsn] [bigint] NOT NULL,
 CONSTRAINT [PK_prog_checkpoint_first] PRIMARY KEY CLUSTERED 
(
 [gcf_id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

緊接著,就是新增測試資料。我新增了2萬筆資料進去,然後分別執行「while exists 範例」與「使用暫存資料表的方法」

declare @count bigint
set @count=1

while @count<=20000
begin
 insert into prog_checkpoint_first (gcf_seqsn) values (682)
 set @count=@count+1
end

最後觀察這兩種方式的執行時間。
「while exists 範例」:2 秒
「使用暫存資料表的方法」:34秒

這結果,真的差很大。所以,當你有需要透過 SQL去處理迴圈問題,如果可預期資料量很大,那會建議使用 while exists 的寫法。如果資料量很小,則用那種都好。

7 則留言:

  1. 補充一下為何二個範例會有不小的效能差異
    1.暫存資料表是存在tempdb,所以當我們一定要產生暫存資料表時,在SELECT LIST上只要列出"你要的"的欄位,以免產生過多不必要的I/O動作,列如SELECT * 應該避免。
    2.另一個影響效能的地方是EXISTS與COUNT(*),基本上EXISTS的效能是比COUNT(*)好,至於原因請參考網址http://www.sqlmag.com/article/tsql3/exists-vs-count-.aspx

    回覆刪除
  2. 說得太好了,
    謝謝補充。

    回覆刪除
  3. 剛剛看了 Ken 提供的參考網頁。的確,使用 count(*) ,即使已經找到一筆資料,但他還是要把所有資料都跑完才停止。而使用 exists(),則只要找到一筆資料,後面就不會繼續執行了。這兩種方法經這麼一比較,真的效能上的差異就明顯多了。感謝 Ken 的補充,真是獲益良多。

    回覆刪除
  4. 補充一段比較簡潔的 code:
    ---------------------------------

    declare @guid nvarchar(50)
    set @guid='64536941-4476-4342-96b2-fa6584597ac8'

    declare @gatt_id bigint
    declare @tmp_id bigint

    if exists(select tmp_id from tmp_upload_file where tmp_guid=@guid)
    begin
    --將 @tmp_id 設為最小的值
    set @tmp_id=-1

    while exists (select tmp_id from tmp_upload_file where tmp_guid=@guid and tmp_id>@tmp_id )
    begin
    select top 1 @gatt_id=gatt_id,@tmp_id=tmp_id from tmp_upload_file where tmp_guid=@guid and tmp_id>@tmp_id order by tmp_id
    --Do
    print @gatt_id

    end

    end

    回覆刪除
  5. 補充一段針對字串的處理方式
    ----------------------------------
    declare @strExcludes nvarchar(max)
    set @strExcludes='B,C'

    declare @tmp_id nvarchar(50)

    if exists(SELECT * FROM [dbo].[fn_split] (@strExcludes ,',') )
    begin

    --將 @tmp_id 設為最小的值
    set @tmp_id=''
    while exists (select [Value] from [dbo].[fn_split] (@strExcludes ,',') where [Value]>@tmp_id )
    begin
    select top 1 @tmp_id=[Value] from [dbo].[fn_split] (@strExcludes ,',') where [Value]>@tmp_id order by [Value]
    --Do
    print 'p'
    end
    end

    回覆刪除
  6. 自己曾經將資料格式為 varchar() 的數字內容,如: "1","2","11","12" 等資料當作是 key 值來使用 while exists ,發生了有些項目該執行卻沒有執行的情況。後來發現問題出在 Order by 時,"11" 比 "2" 排得還要前面,也就是 "11" < "2" 。所以,如果確定是要用數字格式來當 Key,千萬別偷懶,一定要確保該欄位格式為數字,否則要用 convert(int,[欄位]) 來轉換。

    回覆刪除
  7. select ROW_NUMBER() over (order by abd_gcarstyleno,abd_gcarstyleno2) RowID,*
    into #tmp
    from eea_valid
    where abd_vechiletype='E' and CHARINDEX('-',abd_gcarstyleno)>0

    declare @tmp_id bigint


    --將 @tmp_id 設為最小的值
    set @tmp_id=-1

    while exists (select RowID from #tmp where RowID>@tmp_id )
    begin
    select top 1 @tmp_id=RowID from #tmp where RowID>@tmp_id order by RowID

    --Do
    print @gatt_id

    end



    drop table #tmp

    回覆刪除