sqlserver的表變量在沒有預估誤差的狀況下,與物理表可join產生的性能問題

衆所周知,在sqlserver中,表變量最大的特性之一就是沒有統計信息,沒法較爲準備預估其數據分佈狀況,所以不適合參與較爲複雜的SQL運算。
當SQL相對簡單的時候,使用表變量,在某些場景下,即使是對錶變量的預估沒有產生誤差的狀況下,仍舊會有問題。
sqlserver的優化引擎對於表變量的支持十分不友好,再次對錶變量的使用產生了警戒。sql

 

測試環境搭建ide

理搭建一個簡單的測試環境,來驗證本文的想要表達的主題,
測試表TestTableVariable 上有KeyCode1 ~KeyCode5 5個字段,分別建立非彙集索引,
對於數據分佈,刻意設計出當前這種場景:KeyCode1 ~KeyCode5的字段值,分別趨於稀疏(非空值的愈來愈少,null值愈來愈多)
以下,寫入100W行數據,就能夠出來下面要表達的效果了。sqlserver

create table TestTableVariable
(
    Id int identity(1,1),
    KeyCode1 varchar(10),
    KeyCode2 varchar(10),
    KeyCode3 varchar(10),
    KeyCode4 varchar(10),
    KeyCode5 varchar(10),
    CreateDate datetime
)

alter table TestTableVariable
add constraint pk_TestTableVariable primary key(Id) 


create index idx_KeyCode1 on TestTableVariable(KeyCode1)
create index idx_KeyCode2 on TestTableVariable(KeyCode2)
create index idx_KeyCode3 on TestTableVariable(KeyCode3)
create index idx_KeyCode4 on TestTableVariable(KeyCode4)
create index idx_KeyCode5 on TestTableVariable(KeyCode5)

insert into TestTableVariable(KeyCode1,CreateDate) values (CONCAT('XX',CAST(RAND()*1000000 AS INT)),GETDATE())
GO 1000000



update TestTableVariable set KeyCode2 = KeyCode1 where Id%10 = 0
update TestTableVariable set KeyCode3 = KeyCode1 where Id%1000 = 0
update TestTableVariable set KeyCode4 = KeyCode1 where Id%10000= 0
update TestTableVariable set KeyCode5 = KeyCode1 where Id%100000 = 0
GO

 

問題重現測試

對於普通的查詢,找一個KeyCode1 ~KeyCode5均有值的條件進行查詢,執行計劃都在預期之中,都可以用到索引,不過多表述優化

select * from TestTableVariable where KeyCode1 = 'XX156876'
select * from TestTableVariable where KeyCode2 = 'XX156876'
select * from TestTableVariable where KeyCode3 = 'XX156876'
select * from TestTableVariable where KeyCode4 = 'XX156876'
select * from TestTableVariable where KeyCode5 = 'XX156876'

下面將查詢條件寫入一張表變,讓表變量與物理表TestTableVariable進行join
以下語句,分別用KeyCode1 ~KeyCode5進行查詢,對於非空值分佈相對較多的KeyCode1 ~KeyCode3,作查詢的時候,執行計劃也在預期之中(索引查找)spa

 

從非空值分佈愈來愈少的KeyCode4開始,執行計劃開始變成非預期的索引查找,變成了表掃描設計

KeyCode5依舊是非預期的索引查找,也是表掃描code

 

這裏不是提出相似問題的解決辦法的,固然解決辦法也比較簡單,
1,添加一個不影響邏輯的條件,至關於簡單地改寫SQL,以下增長where a.KeyCode5 is not null 篩選條件,由於null值不等於任何值,包括null值,所以增長這個條件不會影響這個SQL的邏輯
2,將表變量的數據寫入臨時表,讓臨時表與測試表JOIN,其餘不作任何修改
兩種方式均可以達到index seek的效果。server

declare @tb table ( KeyCode varchar(10))
insert into @tb values ('XX156876')
select * from TestTableVariable a inner join @tb b on a.KeyCode5 = b.KeyCode
where a.KeyCode5 is not null
go

declare @tb table ( KeyCode varchar(10))
insert into @tb values ('XX156876')
select * into #t from @tb
select * from TestTableVariable a inner join #t b on a.KeyCode5 = b.KeyCode
go

如下是二者的執行計劃,都是index seekblog

以上是解決辦法,暫不過多表述。

 

存在的疑問

問題就在於:
即使是表變量沒有統計信息,sqlserver默認狀況下老是會預估爲1行(不加任何查詢提示),既然預估爲1行,在當前狀況下也是準確的,不認爲是預估出現誤差致使執行計劃出現非最優。
對於臨時表,一樣是1行數據,來驅動物理表TestTableVariable,就能夠正常使用到index seek,而表變量不行?
再就是,對於TestTableVariable表上的統計信息,通過幾個SQL查詢事後,觸發了統計信息的更新,統計信息也相對準確地預估到了999999行爲null,1行是一個特定的值XX156876)

1,對於物理表TestTableVariable與表變量的join,因爲NULL值跟任何值對比都是沒有結果的,換句話說就是,無論表變量裏的數據量有多少,按照統計信息中的預估,這個查詢對於TestTableVariable這個表來講,最多隻有1行數據(統計信息中的那個非NULL)的數據參與查詢運算
2,對於表變量,既然預估爲1行,哪有爲何不使用索引查找的方式,就算是用不到索引查找,join雙方,按照預估,都只有一行數據參與運算的狀況下,爲何居然要選擇HASH JOIN?

表變量參數join的時候,優化器爲何連這麼一個簡單的推斷邏輯都作不到,並無很是複雜的邏輯,或者說數據分佈異常的狀況在裏面,最終選擇了最差的執行計劃進行運算。
反觀臨時表,用臨時表join的狀況下,一切都回歸到預期的索引查找,能否認爲,sqlserver對錶變量的join或者說運算,支持的很是不友好(2012~2016均沒有改善)。

 

後面懷疑是否是KeyCode5上的統計信息取樣百分比不夠大,形成的執行計劃錯誤,嘗試100%取樣

繼續測試,問題依舊

當前這個case,並非那種經典的,由於對錶變量預估誤差形成的執行計劃錯誤,暫時也沒法理解,sqlserver爲何會對錶變量參數參與的join,在當前這種case中,採用如此保守的執行方式。

 

愈來愈多的case證實,在sqlserver中使用表變量參與join,就比如是一顆定時炸彈,隨時能夠引爆你的系統,看來要慎重。

相關文章
相關標籤/搜索