我們偶爾,非常偶爾的情況下會在一個查詢計划中看到這樣的警告:
大紅叉,好嚇人啊!
把鼠標放上去一看顯示這樣的信息
No join predicate
直譯過來就是:沒有連接謂詞
在真實的生產環境下我們很少能看到這種警告,什么時候才出這種警告呢?當然就是~~~沒有連接謂詞(汗)的時候,也許這么解釋起來很找打,但是真實情況就是這樣。
我們知道,在sqlserver連接操作的時候,他的本質實際上就是生成一個笛卡爾積表,那么連接謂詞就是在笛卡爾積表上進行篩選的條件
比如我們寫如下的查詢:
select sod.ProductID from sales.SalesOrderDetail sod
join production.product pd
on 1=1
可以看到,我在on的位置上只寫了on 1=1,實際上這個查詢等同於
select sod.productid from
sales.SalesOrderDetail sod ,production.product pd
where 1=1--或者where 1=1這個可以也是可以不加的
我們都知道上面兩種寫法只是生成了一個笛卡爾積表的全集
他們的執行計划生成是一模一樣的,如下,可以注意一下上方顯示的查詢語句:
這時,因為兩個表之間出現了沒有任何可供連接的謂詞,換句話說就是沒有對笛卡爾積生成的表進行任何篩選,這種查詢可能會帶來巨大的性能損耗,所以發出了no join predicate的警告,而事實也是如此
我們可以看到12W對500條數據的表做積后生成的數據量高達6KW條,可想而知這種查詢的消耗有多么大,所以我們一般在查詢中一定要注意在做表連接的時候避免這種寫法,也許有人會說,誰會這么寫查詢?我只能說什么都可能發生 ,比如連接表時是以拼串的形式生成的sql語句,或者用我提到的第二種古老的寫法進行的查詢,都是有可能的。
OK,以上是對no join predicate警告的一個基本說明,但是今天我們的重點不在這里
我這篇文章想說的是:有一種情況下的警告出現的很奇怪,比如下面這個查詢
SELECT sod.SalesOrderID,pd.Name FROM sales.SalesOrderDetail sod
INNER JOIN production.product2 pd
on sod.productid=pd.ProductID
where sod.productid=777
計划:
這個計划中就有無連接謂詞警告,但是明明寫了ON也有where條件,為什么還會生成這樣的計划呢,我們來看一下它是怎么產生的:
我們以最開始的查詢開始
注意:product 表就是AdventureWorks2008R2原生的產品表
SELECT sod.SalesOrderID,pd.Name FROM sales.SalesOrderDetail sod
INNER JOIN production.product pd
on sod.productid=pd.ProductID
where sod.productid=777
它的計划如下:
OK,沒有任何的問題,那么我們假設有如下情況:product 表中的productID並不唯一,也就是說SalesOrderDetail 和product 的productid是多對多的情況,這種情況在生產環境中就相當常見了
所以我生成了如下的表:select * into Production.product2 from Production.product
我們知道,這樣生成的表會把identify列一起生成,上面說過productid應該是不唯一的情況,所以我們把idenify屬性去除(過程省略),並生成一個不唯一索引
為這說明之后發生的問題,我們再添加一個列,顯示的是產品第一批訂單發生的日期
alter table Production.product2 add firstorder int
update Production.product2 set firstorder=c.SalesOrderDetailID
from Production.product2 a
cross apply(select top 1 SalesOrderDetailID from Sales.SalesOrderDetail
where ProductID=a.ProductID order by ModifiedDate) c
再創建一個索引
create index IX_test on Production.product2 (productid,firstorder,name)
之后運行:
SELECT sod.SalesOrderID,pd.Name FROM sales.SalesOrderDetail sod
INNER JOIN production.product2 pd
on sod.productid=pd.ProductID
where sod.productid=777 option(recompile)
再查看執行計划
好的,還是沒有任何問題出現,我們知道,查詢計划的生成是依靠統計信息的,所以我們查看一下777這個鍵值的統計信息:
DBCC SHOW_STATISTICS("Production.product2",IX_test)
可以看出,777這個值顯示為一個唯一值(或者說mssql通過統計信息認為777這個值也許,大概,可能是唯一的),之前我們說過,我們的場景是多對多,那我們開始生成重復數據,並且手動刷新統計信息:
insert into Production.product2 select * from Production.product2 where productid=777
update STATISTICS Production.product2 IX_test with fullscan—在sql2014中由於預估算法有改進,不用更新統計直接執行也可以重現,但是在直方圖中就看不出來了
再看上面查詢的計划
噢!變成沒有謂詞的提示了!
統計信息呢,變成這樣了
這是為什么呢?
解釋如下:我們知道,在查詢處理的過程中,優化器對查詢有一系列簡化過程,比如代數代入,就是說在
on sod.productid=pd.ProductID
where sod.productid=777
這個條件下,會先通過productid=777把兩個表符合條件的數據篩選出來(為什么是兩個表,因為有sod.productid=pd.ProductID,所以常量參數直接傳遞了),之后再進行inner on的匹配
之前沒有重復數據的時候,由於product表productid列為主鍵,給定鍵值只有一條數據,那么對SalesOrderDetail來說,輸出的數據就是product表單條數據與SalesOrderDetail表的全部匹配數據
而把product進行重復插入后,mssql查覺到product表符合777的數據>1條了(見下圖)
但是連接謂詞列被指定常量777替換,但又沒有其它的篩選條件,那么實際上查詢等同於
SELECT sod.SalesOrderID,pd.Name FROM (select SalesOrderID from sales.SalesOrderDetail where productid=777) sod,
(select Name from production.product2 where productid=777) pd option(recompile)
同時,我們也知道,nest loop算法的偽碼如下 :
我們知道,nest loop的偽碼算法是這樣的,它的時間復雜度為N*M(或者說左表行數*右表行數)
for each row in tb1 loop for tb2 loop If match tb2.key= tb1.key then pass the row on to the next step If no match then discard the row end loop end loop
上面的查詢寫法就變成了兩個子結果集直接進行積卡爾笛,但並沒有任何可供比較的key值,於是產生了no join predicate警告
那么如果我換一個參數呢?比如productid=1的時候會是什么情況?
在本例中,由於SalesOrderDetail沒有符合productid=1的數據,所以預估行數據就為1,這時不管product表有多少productid=1的數據,也表現為輸出一對多的數據,所以也就沒有顯示出no join predicate警告。
其實可以說,如果查詢計划里出現了no join predicate警告,就必須要看一下這個查詢的業務邏輯是不是有問題,有可能是輸出大量無效的垃圾數據,並且影響了性能 ,但是這個說法反過來說是不成立的,並不是說沒有沒有警告就沒產生重復的垃圾數據。
例如在之前我建立的復合索引是包含了productid和firstorder列
那我查詢的寫法可能會這么寫
SELECT sod.SalesOrderID ,
pd.Name
FROM Sales.SalesOrderDetail sod
INNER JOIN Production.product2 pd ON
sod.salesorderdetailid= pd.firstorder and
sod.ProductID=pd.ProductID
WHERE pd.ProductID = 777 and pd.firstorder=28 option(recompile)
假設這兩個表的數據量很大,且有相當多的數據,這時就有可能產生行數估值錯誤,出現無謂詞警告,但是有可能業務邏輯並沒有問題(在本例中沒有出現這種情況,只是舉例說明)
表不變,但當查詢這么寫的時候
SELECT sod.SalesOrderID ,
pd.Name
FROM Sales.SalesOrderDetail sod
INNER JOIN Production.product2 pd ON
sod.ProductID=pd.ProductID
and sod.salesorderdetailid= pd.firstorder
WHERE pd.ProductID = 777-- and pd.firstorder=28
option(recompile)
因為兩個數據子集除去productid被常量參數傳遞后不參與匹配后,還需要進一步對sod.salesorderdetailid= pd.firstorder進行匹配,這樣不會出現無連接謂詞警告,但是業務邏輯明顯出現了問題,但我估計不會有人把上面查詢修改寫成下面這樣吧……
當然在本篇文章中演示的案例數據量太小,並不能重現這種情況。有興趣的同學可以擴大數據量測試一下。
結論
在進行連接時不管左表還是右表,只要有一個表可以產生一個唯一性數據(即nest loop的時間復雜度為1*M或者N*1),這種情況下即使寫成沒有連接謂詞的形式,也不會產生警告符,但是只要結果為N*M,且沒有任何可供匹配的連接謂詞(可以被常量傳遞的謂詞不算),則會產生警告。
我的確是在生產環境中實實在在遇到了這種情況
,由於涉及公司的業務就不把全部計划截出來了,但是可以告訴大家的是,在nested loop下方的那個數據表,其實就存在有類似(productid,firstorder)這樣的一個復合索引,且數據具有唯一性,但是因為統計信息的問題預估行數變成了1.25行,於是產生了無連接謂詞警告
那么究竟這種警告到底需要不需要處理呢?我的看法是:看情況。
出現了警告,肯定是DBA需要關注的,但是不是所有的警告一定就是有問題。
這需要與業務方溝通,到底是業務邏輯出現了問題?還是需求如此?又或者只是一個生成計划時的誤判?
如果只是統計信息誤判斷生成查詢計划顯示的警告,但業務邏輯沒有混亂,從我自己遇到的情況看,並沒有什么實質性的性能問題,可以忽略,如果您有什么不同見解可以與我聯系