文章出處

    在SQL Server中,子查詢可以分為相關子查詢和無關子查詢,對于無關子查詢來說,Not In子句比較常見,但Not In潛在會帶來下面兩種問題:

  • 結果不準確
  • 查詢性能低下

 

    下面我們來看一下為什么盡量不使用Not In子句。

 

結果不準確問題

    在SQL Server中,Null值并不是一個值,而是表示特定含義,其所表示的含義是“Unknow”,可以理解為未定義或者未知,因此任何與Null值進行比對的二元操作符結果一定為Null,包括Null值本身。而在SQL Server中,Null值的含義轉換為Bool類型的結果為False。讓我們來看一個簡單的例子,如圖1所示。

image

圖1.Null值與任何值進行對比結果都為Null

    SQL Server提供了“IS”操作符與Null值做對比,用于衡量某個值是否為Null。

 

    那么Not In 的問題在哪呢,如圖2所示。

image   

圖2.Not In產生不準確的值

 

     在圖2中,條件3不屬于Not In后面列表的任意一個,該查詢卻不返回任何值,與預期的結果不同,那么具體原因就是Not In子句對于Null值的處理,在SQL Server中,圖2中所示的Not In子句其實可以等價轉換為如圖3所示的查詢。

image

圖3.對于Not In子句來說,可以進行等價轉換

 

    在圖3中可以看到Not In可以轉換為條件對于每個值進行不等比對,并用邏輯與連接起來,而前面提到過Null值與任意其他值做比較時,結果永遠為Null,在Where條件中也就是False,因此3<>null就會導致不返回任何行,導致Not In子句產生的結果在意料之外。

    因此,Not In子句如果來自于某個表或者列表很長,其中大量值中即使存在一個Null值,也會導致最終結果不會返回任何數據。

解決辦法?

    解決辦法就是不使用Not In,而使用Not Exists作為替代。Exists的操作符不會返回Null,只會根據子查詢中的每一行決定返回True或者False,當遇到Null值時,只會返回False,而不會由某個Null值導致整個子查詢表達式為Null。對于圖2中所示的查詢,我們可以改寫為子查詢,如圖4所示。

image

圖4.Not Exists可以正確返回結果

 

Not In導致的查詢性能低下

    前面我們可以看出,Not In的主要問題是由于對Null值的處理問題所導致,那么對Null值的處理究竟為什么會導致性能問題?讓我們來看圖5的示例。圖5中,我們使用了Adventurework示例數據庫,并為了演示目的將SalesOrderDetail表的ProductId的定義由Not Null改為Null,此時我們進行一個簡單的Not In查詢。如圖5所示。

image

圖5.Not In的執行計劃

 

    在圖5中,我們看到一個Row Count Spool操作符,該操作符用于確認ProductId列中是否有Null值(過程是對比總行數和非Null行數,不想等則為有Null值,雖然我們知道該列中沒有Null值,但由于列定義是允許Null的,因此SQL Server必須進行額外的確認),而該操作符占用了接近一半的查詢成本。因此我們對比Not Exists,如圖6所示。

image

圖6.Not In Vs Not Exists

 

    由圖6可以看出,Not In的執行成本幾乎是Not Exists的3倍,僅僅是由于SQL Server需要確認允許Null列中是否存在Null。根據圖3中Not In的等價形式,我們完全可以將Not In轉換為等價的Not Exist形式,如圖7所示。

image

圖7.Not In轉換為Not Exists

    我們來對比圖7和其等價Not In查詢的成本,如圖8所示。

image

圖8.成本上完全等價

 

    因此我們可以看到Not In需要額外的步驟處理Null值,上述情況是僅僅在SalesOrderDetail表中的ProductId列定義為允許Null,如果我們將SalesOrderHeader的SalesOrderID列也定義為允許Null時,會發現SQL Server還需要額外的成本確認該列上是否有Null值。如圖9所示。

image

圖9.SQL Server通過加入Left Anti Semi Join操作符解決列允許Null的問題

 

此時Not In對應的等價Not Exist形式變為如代碼清單1所示。

SELECT  *
FROM    Sales.SalesOrderHeader a
WHERE   NOT EXISTS ( SELECT *
                     FROM   Sales.SalesOrderDetail b
                     WHERE  a.SalesOrderID = b.ProductID )
        AND NOT EXISTS ( ( SELECT   *
                           FROM     Sales.SalesOrderDetail b
                           WHERE    b.ProductID IS NULL
                         ) )
        AND NOT EXISTS ( SELECT 1
                         FROM   ( SELECT    *
                                  FROM      Sales.SalesOrderHeader
                                ) AS c
                         WHERE  c.SalesOrderID IS NULL )

代碼清單1.當連接列兩列定義都允許Null時,Not In等價的Not Exists形式

 

    此時我們簡單對比Not In和Not Exists的IO情況,如圖10所示。

image

圖10.Not In吃掉很高的IO

 

小結

    本文闡述了Not In 的實現原理以及所帶來的數據不一致和性能問題,在寫查詢時,盡量避免使用Not In,而轉換為本文提供的Not Exists等價形式,將會減少很多麻煩。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()