關於join時顯示no join predicate的那點事


我們偶爾,非常偶爾的情況下會在一個查詢計划中看到這樣的警告:

image

大紅叉,好嚇人啊!

把鼠標放上去一看顯示這樣的信息

image

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這個可以也是可以不加的

我們都知道上面兩種寫法只是生成了一個笛卡爾積表的全集

他們的執行計划生成是一模一樣的,如下,可以注意一下上方顯示的查詢語句:

imageimage

這時,因為兩個表之間出現了沒有任何可供連接的謂詞,換句話說就是沒有對笛卡爾積生成的表進行任何篩選,這種查詢可能會帶來巨大的性能損耗,所以發出了no join predicate的警告,而事實也是如此

image

我們可以看到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

計划:

image

這個計划中就有無連接謂詞警告,但是明明寫了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

 

它的計划如下:

image

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)

 

再查看執行計划

image

好的,還是沒有任何問題出現,我們知道,查詢計划的生成是依靠統計信息的,所以我們查看一下777這個鍵值的統計信息:

DBCC SHOW_STATISTICS("Production.product2",IX_test)

image

可以看出,777這個值顯示為一個唯一值(或者說mssql通過統計信息認為777這個值也許,大概,可能是唯一的),之前我們說過,我們的場景是多對多,那我們開始生成重復數據,並且手動刷新統計信息:

insert into Production.product2 select * from Production.product2 where productid=777

update STATISTICS Production.product2 IX_test with fullscan—在sql2014中由於預估算法有改進,不用更新統計直接執行也可以重現,但是在直方圖中就看不出來了

再看上面查詢的計划

image

噢!變成沒有謂詞的提示了!

統計信息呢,變成這樣了

image

 

這是為什么呢?

解釋如下:我們知道,在查詢處理的過程中,優化器對查詢有一系列簡化過程,比如代數代入,就是說在

on sod.productid=pd.ProductID
where sod.productid=777

這個條件下,會先通過productid=777把兩個表符合條件的數據篩選出來(為什么是兩個表,因為有sod.productid=pd.ProductID,所以常量參數直接傳遞了),之后再進行inner on的匹配

imageimage

 

 

之前沒有重復數據的時候,由於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,且沒有任何可供匹配的連接謂詞(可以被常量傳遞的謂詞不算),則會產生警告。

我的確是在生產環境中實實在在遇到了這種情況

image,由於涉及公司的業務就不把全部計划截出來了,但是可以告訴大家的是,在nested loop下方的那個數據表,其實就存在有類似(productid,firstorder)這樣的一個復合索引,且數據具有唯一性,但是因為統計信息的問題預估行數變成了1.25行,於是產生了無連接謂詞警告

那么究竟這種警告到底需要不需要處理呢?我的看法是:看情況。

出現了警告,肯定是DBA需要關注的,但是不是所有的警告一定就是有問題。

這需要與業務方溝通,到底是業務邏輯出現了問題?還是需求如此?又或者只是一個生成計划時的誤判?

如果只是統計信息誤判斷生成查詢計划顯示的警告,但業務邏輯沒有混亂,從我自己遇到的情況看,並沒有什么實質性的性能問題,可以忽略,如果您有什么不同見解可以與我聯系


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM