在SQL Server 2014中查询慢100倍,行计数假脱机行估计是罪魁祸首?


13

我有一个查询,该查询在SQL Server 2012中运行800毫秒,在SQL Server 2014中运行约170秒。我认为我已将其范围缩小到Row Count Spool运营商的基数估计不佳。我已经读过一些关于假脱机操作符的信息(例如,herehere),但是仍然难以理解以下几点:

  • 为什么此查询需要Row Count Spool运算符?我认为正确性不是必需的,那么它试图提供什么特定的优化?
  • 为什么SQL Server估计联接到Row Count Spool运算符会删除所有行?
  • 这是SQL Server 2014中的错误吗?如果是这样,我将提交Connect。但是我想先加深了解。

注意:LEFT JOIN为了在SQL Server 2012和SQL Server 2014中都能达到可接受的性能,我可以将查询重新编写为或向表中添加索引。因此,此问题更多地是关于深入了解此特定查询和计划的,而较少涉及如何用不同的措词查询。


慢查询

有关完整的测试脚本,请参见此Pastebin。这是我正在查看的特定测试查询:

-- Prune any existing customers from the set of potential new customers
-- This query is much slower than expected in SQL Server 2014 
SELECT *
FROM #potentialNewCustomers -- 10K rows
WHERE cust_nbr NOT IN (
    SELECT cust_nbr
    FROM #existingCustomers -- 1MM rows
)


SQL Server 2014:估计的查询计划

SQL Server相信Left Anti Semi JoinRow Count Spool会过滤掉10,000行到1行。因此,它LOOP JOIN为的后续连接选择一个#existingCustomers

在此处输入图片说明


SQL Server 2014:实际的查询计划

如预期的那样(除SQL Server以外的每个人都!),Row Count Spool没有删除任何行。因此,当SQL Server预期仅循环一次时,我们将循环10,000次。

在此处输入图片说明


SQL Server 2012:估计的查询计划

使用SQL Server 2012(或OPTION (QUERYTRACEON 9481)SQL Server 2014)时,Row Count Spool不会减少估计的行数,而是选择了哈希联接,因此可以制定更好的计划。

在此处输入图片说明

LEFT JOIN重新编写

供参考,这是一种我可以重新编写查询以在所有SQL Server 2012、2014和2016中获得良好性能的方法。但是,我仍然对上面查询的特定行为以及是否对该查询感兴趣。是新的SQL Server 2014基数估计器中的错误。

-- Re-writing with LEFT JOIN yields much better performance in 2012/2014/2016
SELECT n.*
FROM #potentialNewCustomers n
LEFT JOIN (SELECT 1 AS test, cust_nbr FROM #existingCustomers) c
    ON c.cust_nbr = n.cust_nbr
WHERE c.test IS NULL

在此处输入图片说明

Answers:


10

为什么此查询需要行计数假脱机运算符?...它试图提供什么具体的优化?

中的cust_nbr列可以#existingCustomers为空。如果它实际上包含任何null,则此处的正确响应是返回零行(NOT IN (NULL,...) 将始终产生空结果集。)。

因此查询可以认为是

SELECT p.*
FROM   #potentialNewCustomers p
WHERE  NOT EXISTS (SELECT *
                   FROM   #existingCustomers e1
                   WHERE  p.cust_nbr = e1.cust_nbr)
       AND NOT EXISTS (SELECT *
                       FROM   #existingCustomers e2
                       WHERE  e2.cust_nbr IS NULL) 

使用rowcount假脱机可以避免评估

EXISTS (SELECT *
        FROM   #existingCustomers e2
        WHERE  e2.cust_nbr IS NULL) 

不止一次。

这似乎是一种假设,假设的微小差异可能会对性能造成巨大的影响。

如下更新单个行后...

UPDATE #existingCustomers
SET    cust_nbr = NULL
WHERE  cust_nbr = 1;

...查询在不到一秒钟的时间内完成。计划的实际版本和估计版本中的行计数现在几乎可以确定。

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT *
FROM   #potentialNewCustomers
WHERE  cust_nbr NOT IN (SELECT cust_nbr
                        FROM   #existingCustomers 
                       ) 

在此处输入图片说明

如上所述,输出零行。

SQL Server中的统计直方图和自动更新阈值不够精细,无法检测到这种单行更改。可以说,如果该列可为空,则NULL即使统计直方图当前未指示存在任何列,也可以基于它至少包含一个列来工作。


9

为什么此查询需要行计数假脱机运算符?我认为正确性不是必需的,那么它试图提供什么特定的优化?

有关此问题,请参阅马丁的完整答案。关键的一点是,如果内部的单行NOT INNULL,布尔逻辑的作品出来,使得“正确的响应是返回零行”。所述Row Count Spool操作者是优化此(必要时)的逻辑。

为什么SQL Server估计联接到“行计数假脱机”运算符会删除所有行?

Microsoft提供了有关SQL 2014基数估计器出色白皮书。在本文档中,我发现了以下信息:

新的CE假定查询的值确实存在于数据集中,即使该值不在直方图的范围内。在此示例中,新的CE使用的平均频率是通过将表基数乘以密度计算得出的。

通常,这样的改变是非常好的。它极大地缓解了升序的关键问题,并且通常针对基于统计直方图的超出范围的值产生更为保守的查询计划(较高的行估计)。

但是,在这种特定情况下,假设NULL将找到一个值将导致一个假设,即加入Row Count Spool会过滤掉中的所有行#potentialNewCustomers。在实际上有NULL一行的情况下,这是一个正确的估计(如马丁的回答所示)。但是,在碰巧没有NULL一行的情况下,效果可能是毁灭性的,因为无论出现多少输入行,SQL Server都会产生1行的联接后估计。这可能导致在查询计划的其余部分中联接选择非常差。

这是SQL 2014中的错误吗?如果是这样,我将提交Connect。但是我想先加深了解。

我认为它处于错误和SQL Server的新基数估计器的影响性能的假设或限制之间的灰色区域。但是,对于可空NOT IN子句(碰巧没有任何NULL值)的特定情况,此怪癖可能导致性能相对于SQL 2012大幅下降。

因此,我提出了一个Connect问题,以便SQL团队知道此更改对基数估计器的潜在影响。

更新:我们现在在CTP3上使用SQL16,并且我确认那里没有发生问题。


5

马丁·史密斯(Martin Smith)的回答和您的自我回答正确地解决了所有要点,我只想强调一个面向未来读者的领域:

因此,这个问题更多地是关于深入了解此特定查询和计划,而不是如何用不同的措辞来表达该查询。

查询的陈述目的是:

-- Prune any existing customers from the set of potential new customers

这种要求很容易以几种方式在SQL中表达。选择哪种样式与样式样式一样重要,但是在所有情况下仍应编写查询规范以返回正确的结果。这包括考虑空值。

充分表达逻辑要求:

  • 回报尚未成为潜在客户的潜在客户
  • 最多列出一次潜在客户
  • 排除潜在的潜在客户和现有客户(无论无效客户意味着什么)

然后,我们可以使用我们喜欢的任何一种语法来编写满足这些要求的查询。例如:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr NOT IN
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

这将产生有效的执行计划,并返回正确的结果:

执行计划

我们可以表达NOT IN作为<> ALLNOT = ANY不影响计划或结果:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr <> ALL
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );
WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    NOT DPNNC.cust_nbr = ANY
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

或使用NOT EXISTS

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE 
    NOT EXISTS
    (
        SELECT * 
        FROM #existingCustomers AS EC
        WHERE
            EC.cust_nbr = DPNNC.cust_nbr
            AND EC.cust_nbr IS NOT NULL
    );

这样做没有什么魔力,或者对于使用INANYALL- 尤其令人反感,我们只需要正确编写查询,因此它将始终产生正确的结果。

最紧凑的形式使用EXCEPT

SELECT 
    PNC.cust_nbr 
FROM #potentialNewCustomers AS PNC
WHERE 
    PNC.cust_nbr IS NOT NULL
EXCEPT
SELECT
    EC.cust_nbr 
FROM #existingCustomers AS EC
WHERE 
    EC.cust_nbr IS NOT NULL;

尽管由于缺少位图过滤,执行计划的效率可能较低,但这也会产生正确的结果:

非位图执行计划

最初的问题很有趣,因为它通过必要的null检查实现公开了影响性能的问题。该答案的重点是正确编写查询也可以避免该问题。

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.