预暂存数据导致执行计划成本飙升

Kri*_*yer 5 performance sql-server optimization execution-plan sql-server-2016 query-performance

我有一个麻烦的查询,我们正在尝试调整。我们的第一个想法是采用更大的执行计划的一部分并将这些结果存储到中间临时表中,然后执行其他操作。

我观察到的是,当我们将数据预先准备到临时表中时,执行计划成本会飙升(22 -> 1.1k)。现在,这样做的好处是允许计划并行执行,这将执行时间减少了 20%,但在我们的情况下,每次执行的 CPU 使用率要高得多,这不值得。

我们正在使用带有旧版 CE 的 SQL Server 2016 SP2。

原始计划(成本 ~20):

低成本计划

https://www.brentozar.com/pastetheplan/?id=ry-QGnkCM

原始 SQL:

WITH Object1(Column1, Column2, Column3, Column4, Column5, Column6)
AS
(
    SELECT  Object2.Column1, 
            Object2.Column2, 
            Object3.Column3, 
            Object3.Column4, 
            Object3.Column5, 
            Object3.Column6
    FROM Object4 AS Object5
    INNER JOIN Object6 AS Object2 ON Object2.Column2 = Object5.Column2 AND Object2.Column7 = 0
    INNER JOIN Object7 AS Object8 ON Object8.Column8 = Object2.Column9 AND Object8.Column7 = 0
    INNER JOIN Object9 AS Object3 ON Object3.Column10 = Object8.Column11 AND Object3.Column7 = 0
    INNER JOIN Object10 AS Object11 ON Object2.Column1 = Object11.Column1
    WHERE   Object8.Column12 IS NULL AND
            Object8.Column13 = Object5.Column13 AND 
            Object3.Column3 = Object5.Column3 AND 
            Object11.Column14 = Variable1
)
insert Object12
SELECT  Object13.Column2,
        Object13.Column3,
        MIN(Object13.Column4) AS Column15,
        MAX(Object13.Column4) AS Column16,
        COUNT(DISTINCT (CASE WHEN Object13.Column5 = 1 THEN Object13.Column1 END)) AS Column17,
        COUNT(DISTINCT (CASE WHEN Object13.Column6 = 0 THEN Object13.Column1 END)) AS Column18,
        COUNT(DISTINCT Object13.Column1) AS Column19
FROM Object1 AS Object13
GROUP BY Object13.Column2, Object13.Column3 OPTION (RECOMPILE) 
Run Code Online (Sandbox Code Playgroud)

新计划(上面以蓝色突出显示的区域已预先安排到临时表中 - 成本约为 1.1k):

在此处输入图片说明

https://www.brentozar.com/pastetheplan/?id=rycqG3JRf

新 SQL:

SELECT  Object1.Column1,
        Object1.Column2,
        MIN(Object2.Column3) AS Column4,
        MAX(Object2.Column3) AS Column5,
        COUNT(DISTINCT (CASE WHEN Object2.Column6 = 1 THEN Object1.Column7 END)) AS Column8,
        COUNT(DISTINCT (CASE WHEN Object2.Column9 = 0 THEN Object1.Column7 END)) AS Column10,
        COUNT(DISTINCT Object1.Column7) AS Column11
from Object3 Object1
join Object4 Object2 on Object2.Column12 = Object1.Column13 and Object2.Column2 = Object1.Column2
where Object2.Column14 = 0
GROUP BY Object1.Column1, Object1.Column2 OPTION (RECOMPILE) 
Run Code Online (Sandbox Code Playgroud)

有人可以帮助我们理解为什么新计划的成本会如此高吗?如果需要,我很乐意提供有关下面的表/索引的其他信息。

在原始计划的情况下,我们确实意识到它正在执行插入而不是选择。即便如此,选择不应该(在我们的脑海中)被认为更加昂贵。

这是实际的执行计划。这是一个问题,因为由于计划成本非常高,它是并行的。因此使用更高的CPU。此外,我们只是好奇为什么计划成本会因为诸如预先暂存数据之类的事情而增加那么多,这通常会使您接近(如果不是更好)原始成本。

临时表在第二个查询中被索引为 Object1.Column13 和 Object1.Column2 上的复合聚集 PK。这与 Object4 的列(和顺序)相匹配。添加MAXDOP提示是一种选择,但这也是“世界上为什么成本上涨那么多”的学术练习?

添加OPTION (ORDER GROUP)到第二个查询结果没有变化,相同的运营商/成本。

笔记:

  • 第一个查询中的 Object9 与第二个查询中的 Object4 是同一个对象。

Joe*_*ish 6

成本是基于估计的,即使是在“实际计划”中。您不能并排比较两个查询计划并仅根据运算符或总计划成本得出其中一个将需要更多 CPU 来执行的结论。我可以创建一个在一秒钟内执行的成本为数百万的查询。我还可以创建一个成本很小的查询,它实际上需要永远执行。对于您的情况,由于散列连接后的基数估计不佳,第一个查询的成本仅为 22 个优化器单元:

在此处输入图片说明

红色的操作符执行了数百万次,但查询优化器希望它们只执行几千次。基于估算的成本不会反映该工作。蓝色的运算符是一个表假脱机,基数估计器希望为其插入单行。相反,它插入了几百万。因此,黑色的运算符(以及其他一些未显示的运算符)效率低下并溢出到 tempdb。

使用另一个计划,您将大量行放入 tempdb,因此基数估计更合理,尽管它仍然不理想:

在此处输入图片说明

查询优化器预计需要处理更多的行,因此查询计划的成本更高。作为一个非常普遍的经验法则,您可能会看到通过改进估计来提高性能,但它并不总是如您所愿。查看带有临时表的计划,我看到了一些可能改进的领域:

  1. 将原始查询中的完整 CTE 加载到临时表中。具有多个不同聚合的查询可能很难优化。有时您会得到一个查询计划,其中所有数据都加载到一个假脱机(到 tempdb)中,并且一些聚合被单独应用到假脱机中。根据我的经验,所有这些工作总是在串行区域中完成。如果您消除查询中的所有连接,我相信您不会得到这种优化。聚合将仅应用于临时表。这将节省您将几乎相同的数据写入 tempdb 的工作,并且整个计划应该符合并行性。

  2. 将临时表定义为堆并使用TABLOCK. 现在看起来您有一个聚集索引,这意味着您不符合并行插入的条件。

  3. 考虑使用这些技巧之一使查询符合批处理模式。批处理模式聚合可以显着提高多个不同聚合的效率。

我希望这些步骤的某种组合能够显着改善运行时间。请注意,我进行了快速分析,部分原因是匿名计划难以解释。