检查是否存在EXISTS胜过COUNT个!……不?


35

我经常阅读何时必须检查行是否存在的情况,应该始终使用EXISTS而不是COUNT来完成。

但是,在最近的几种情况下,我测量了使用count时的性能提升。
模式如下:

LEFT JOIN (
    SELECT
        someID
        , COUNT(*)
    FROM someTable
    GROUP BY someID
) AS Alias ON (
    Alias.someID = mainTable.ID
)

我不熟悉SQL Server内部“发生什么”的方法,因此我想知道EXISTS是否存在未预料到的缺陷,该缺陷对我所做的测量完全有意义(EXISTS是RBAR吗?!)。

您对此现象有一些解释吗?

编辑:

这是您可以运行的完整脚本:

SET NOCOUNT ON
SET STATISTICS IO OFF

DECLARE @tmp1 TABLE (
    ID INT UNIQUE
)


DECLARE @tmp2 TABLE (
    ID INT
    , X INT IDENTITY
    , UNIQUE (ID, X)
)

; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp1
SELECT n
FROM tally AS T1
WHERE n < 10000


; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp2
SELECT T1.n
FROM tally AS T1
CROSS JOIN T AS T2
WHERE T1.n < 10000
AND T1.n % 3 <> 0
AND T2.n < 1 + T1.n % 15

PRINT '
COUNT Version:
'

WAITFOR DELAY '00:00:01'

SET STATISTICS IO ON
SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN n > 0 THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
LEFT JOIN (
    SELECT
        T2.ID
        , COUNT(*) AS n
    FROM @tmp2 AS T2
    GROUP BY T2.ID
) AS T2 ON (
    T2.ID = T1.ID
)
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF

PRINT '

EXISTS Version:'

WAITFOR DELAY '00:00:01'

SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN EXISTS (
        SELECT 1
        FROM @tmp2 AS T2
        WHERE T2.ID = T1.ID
    ) THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF 

在SQL Server 2008R2(七个64位)上,我得到了这个结果

COUNT 版:

表'#455F344D'。扫描计数1,逻辑读8,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。
表'#492FC531'。扫描计数1,逻辑读30,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:
CPU时间= 0毫秒,经过的时间= 81毫秒。

EXISTS 版:

表'#492FC531'。扫描计数1,逻辑读96,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。
表'#455F344D'。扫描计数1,逻辑读8,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:
CPU时间= 0毫秒,经过的时间= 76毫秒。

Answers:


43

我经常阅读何时必须检查行是否存在的情况,应该始终使用EXISTS而不是COUNT来完成。

几乎没有什么事情总是对的,尤其是涉及数据库时。有多种方法可以在SQL中表达相同的语义。如果有一个有用的经验法则,它可能是使用最自然的可用语法编写查询(是的,这是主观的),并且仅在您获得的查询计划或性能不可接受时才考虑进行重写。

就其价值而言,我个人对此问题的看法是,存在查询最自然地使用表示EXISTS。根据我的经验,与拒绝选择相比EXISTS ,优化效果更好。使用和筛选是另一种选择,它恰好在SQL Server查询优化器中提供了一些支持,但是我个人发现这在更复杂的查询中是不可靠的。在任何情况下,对我来说,似乎比任何一种选择都自然得多。OUTER JOINNULLCOUNT(*)=0EXISTS

我想知道EXISTS是否存在前所未闻的缺陷,该缺陷对我所做的测量非常有意义

您的特定示例很有趣,因为它突出了优化器处理CASE表达式(EXISTS尤其是测试)中的子查询的方式。

CASE表达式中的子查询

考虑以下(完全合法)查询:

DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);

SELECT
    CASE
        WHEN (SELECT W.b FROM @When AS W) = 1
            THEN (SELECT T.c FROM @Then AS T)
        ELSE (SELECT E.d FROM @Else AS E)
    END
FROM @Base AS B;

语义CASEWHEN/ELSE从句通常以文本顺序进行。在上面的ELSE查询中,如果WHEN满足子句,则如果子查询返回多行,则SQL Server返回错误是不正确的。为了尊重这些语义,优化器生成了一个使用直通谓词的计划:

传递谓词

仅当直通谓词返回false时,才评估嵌套循环连接的内侧。总体效果是CASE按顺序测试表达式,并且仅当不满足先前的表达式时才评估子查询。

具有EXISTS子查询的CASE表达式

在使用CASE子查询的情况下EXISTS,逻辑存在性测试将实现为半联接,但是如果后面的子句需要它们,则必须保留通常会被半联接拒绝的行。流过这种特殊类型的半联接的行将获得一个标志,以指示半联接是否找到匹配项。此标志称为探针列

实现的细节是,逻辑子查询被带有探测列的相关联接(“应用”)代替。该工作由查询优化器中称为的简化规则执行RemoveSubqInPrj(删除投影中的子查询)。我们可以使用跟踪标志8606查看详细信息:

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);

输入树中显示EXISTS测试的部分如下所示:

ScaOp_Exists 
    LogOp_Project
        LogOp_Select
            LogOp_Get TBL: #T2
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier [T2].ID
                ScaOp_Identifier [T1].ID

这被转换为RemoveSubqInPrj以以下内容为首的结构:

LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)

这是左半连接,适用于先前描述的探针。迄今为止,此初始转换是SQL Server查询优化器中唯一可用的转换,如果禁用此转换,编译将完全失败。

该查询可能的执行计划形状之一是该逻辑结构的直接实现:

NLJ半探针连接

最终的Compute Scalar CASE使用探针列值评估表达式的结果:

计算标量表达式

当优化为半联接考虑其他物理联接类型时,将保留计划树的基本形状。仅合并连接支持探针列,因此,尽管在逻辑上可行,但不考虑哈希半连接:

与探针列合并

请注意,Expr1008尽管合并没有输出定义,但在计划中的任何运算符上均未显示定义,该合并输出带有一个标记为(名称与之前相同的表达式)。这只是探针列。与之前一样,最终的Compute Scalar使用此探测值评估CASE

问题在于,优化器无法完全探索只有通过合并(或哈希)半联接才有意义的替代方案。在嵌套循环计划中,检查行是否T2在每次迭代中都与范围匹配没有好处。使用合并或哈希计划,这可能是有用的优化。

如果在查询中添加匹配的BETWEEN谓词T2,则所有操作都是对每一行执行此检查,作为合并半联接上的残差(很难在执行计划中发现,但确实存在):

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
            AND T2.ID BETWEEN 5000 AND 7000 -- New
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

残留谓词

我们希望将BETWEEN谓词下推以T2寻求结果。通常,优化器会考虑这样做(即使查询中没有多余的谓词)。它可以识别隐式谓词BETWEENon T1以及谓词之间的联接谓词T1T2一起暗示BETWEENon T2),而它们没有出现在原始查询文本中。不幸的是,应用探针模式意味着没有对此进行探讨。

有一些方法可以编写查询以在合并半联接的两个输入上生成搜索。一种方法涉及以一种非常不自然的方式编写查询(击败了我通常偏爱的原因EXISTS):

WITH T2 AS
(
    SELECT TOP (9223372036854775807) * 
    FROM #T2 AS T2 
    WHERE ID BETWEEN 5000 AND 7000
)
SELECT 
    T1.ID, 
    DoesExist = 
        CASE 
            WHEN EXISTS 
            (
                SELECT * FROM T2 
                WHERE T2.ID = T1.ID
            ) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

最佳技巧计划

我不会在生产环境中编写该查询,只是为了证明所需的计划形状是可能的。如果您需要编写的实际查询CASE以这种特定方式使用,并且由于在合并半联接的探针侧未进行查找而导致性能下降,则可以考虑使用产生正确结果和不同结果的不同语法编写查询更有效的执行计划。


6

“COUNT(*)对是否存在”的说法是检查记录是否存在这样做的。例如:

WHERE (SELECT COUNT(*) FROM Table WHERE ID=@ID)>0

WHERE EXISTS(SELECT ID FROM Table WHERE ID=@ID)

您的SQL脚本未COUNT(*)用作记录是否存在检查,因此我不会说它适用于您的方案。


根据我发布的脚本有任何结论吗?
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.