为什么我的两台服务器之间的实际执行计划不同?

Why am I getting a different actual execution plan between my two servers?

我有一个 SQL 服务器查询 运行 在生产和开发环境中。完全相同的查询。

SELECT DISTINCT 
    [Record_Transformation_ACCRUALS],
    [Record_Transformation_FA:AMORTIZATION],   
    [Record_Transformation_BONUS:AMORTIZATION],
    [Record_Transformation_CPH:BYLABOUR],
    [Record_Transformation_CPH:BYTARGETHOURS],
    [Record_Transformation_OVERHEAD:CULTURE],
    [Record_Transformation_DEDICATED COSTCENTER],
    [Record_Transformation_PUSHDOWN:EXPENSE],
    [Record_Transformation_OVERHEAD:FACILITIES],
    [Record_Transformation_OVERHEAD:GENOME],
    [Record_Transformation_TAXES:MANAGEMENT],
    [Record_Transformation_TAXES:MARKETING],
    [Record_Transformation_OVERHEAD:OFFICETECH],
    [Record_Transformation_EXPENSE:PASSTHROUGH],
    [Record_Transformation_OVERHEAD:PEOPLEPRACTICES],
    [Record_Transformation_OVERHEAD:RECRUITING],
    [Record_Transformation_TAXES:SALES],
    [Record_Transformation_Static Transfer],
    [Record_Label] 
FROM
    Warehouse_20181204 
WHERE 
    Is_Target_Employee = 1 OR Is_Source_Employee = 1

我们比较了这两个 table 的创建脚本,它们是相同的(除了相关 table 的名称)。

我们还验证了它们都在使用聚集列存储索引。

在开发过程中,这个查询花费的时间不到一秒钟。在产品上大约需要一分钟。起初我们认为可能是数据大小的问题,但差异很小(几十万行)。

然后我们检查了两者的实际执行计划。在 dev 上,实际执行计划是:

虽然实际执行计划非常不同:

我们发现自己对这是为什么感到困惑。我们已验证 SQL 服务器的版本相同:

Microsoft SQL Server 2017 (RTM-CU5) (KB4092643) - 14.0.3023.8 (X64)   
Web Edition (64-bit) on Windows Server 2016 Datacenter 10.0 <X64> (Build 14393:) (Hypervisor)

我的问题有两个:

  1. 我们如何确定生产和开发之间的执行计划如此不同的原因?
  2. 在给定类似数据集的情况下,我们如何才能使生产环境 运行 与开发环境一样快?

编辑:

一些额外要求的详细信息:

希望这些对当前 SQL 计划的所有其他细节有所帮助:

DEV sql 计划:https://gist.github.com/klick-barakgall/17a7ce926777a3257f7eecb32859458e

PROD sql 计划:https://gist.github.com/klick-barakgall/76eabf1008f5bfb0c51259c2ba3f509d

为那些有兴趣深入研究执行的人添加粘贴计划的链接。

DEV

PROD

DISTINCT 使您的查询 shorthand 为此:

SELECT
    [Record_Transformation_ACCRUALS],
    [Record_Transformation_FA:AMORTIZATION],   
    [Record_Transformation_BONUS:AMORTIZATION],
    [Record_Transformation_CPH:BYLABOUR],
    [Record_Transformation_CPH:BYTARGETHOURS],
    [Record_Transformation_OVERHEAD:CULTURE],
    [Record_Transformation_DEDICATED COSTCENTER],
    [Record_Transformation_PUSHDOWN:EXPENSE],
    [Record_Transformation_OVERHEAD:FACILITIES],
    [Record_Transformation_OVERHEAD:GENOME],
    [Record_Transformation_TAXES:MANAGEMENT],
    [Record_Transformation_TAXES:MARKETING],
    [Record_Transformation_OVERHEAD:OFFICETECH],
    [Record_Transformation_EXPENSE:PASSTHROUGH],
    [Record_Transformation_OVERHEAD:PEOPLEPRACTICES],
    [Record_Transformation_OVERHEAD:RECRUITING],
    [Record_Transformation_TAXES:SALES],
    [Record_Transformation_Static Transfer],
    [Record_Label] 
FROM   Warehouse_20181204 
WHERE  Is_Target_Employee = 1 OR Is_Source_Employee = 1
GROUP BY 
    [Record_Transformation_ACCRUALS],
    [Record_Transformation_FA:AMORTIZATION],   
    [Record_Transformation_BONUS:AMORTIZATION],
    [Record_Transformation_CPH:BYLABOUR],
    [Record_Transformation_CPH:BYTARGETHOURS],
    [Record_Transformation_OVERHEAD:CULTURE],
    [Record_Transformation_DEDICATED COSTCENTER],
    [Record_Transformation_PUSHDOWN:EXPENSE],
    [Record_Transformation_OVERHEAD:FACILITIES],
    [Record_Transformation_OVERHEAD:GENOME],
    [Record_Transformation_TAXES:MANAGEMENT],
    [Record_Transformation_TAXES:MARKETING],
    [Record_Transformation_OVERHEAD:OFFICETECH],
    [Record_Transformation_EXPENSE:PASSTHROUGH],
    [Record_Transformation_OVERHEAD:PEOPLEPRACTICES],
    [Record_Transformation_OVERHEAD:RECRUITING],
    [Record_Transformation_TAXES:SALES],
    [Record_Transformation_Static Transfer],
    [Record_Label] 

优化器尝试满足此类查询的两种最常见方式。首先,它过滤 Is_Target_Employee = 1 OR Is_Source_Employee = 1 的列存储索引;这就是您计划中的过滤器所显示的内容。接下来,要处理 GROUP BY(或 DISTINCT),它将:

  1. 对行进行排序,然后使用 Stream Aggregator 到 return 不同的集合(如 prod 执行计划所示)
  2. 使用哈希匹配过滤行和return一个不同的集合(如开发版本所示)

我怀疑优化器选择了不同的计划,因为基数估计值大不相同。显然,Dev 计划表现更好。它可能表现更好,因为哈希匹配计划在这种情况下更有效,它更有可能在 Dev 中表现更好,因为您在 Dev 中获得并行执行计划和串行计划在产品中

我建议的做法是: 运行 您在 Prod 中使用查询提示进行查询 -

OPTION (QUERYTRACEON 8649);

这将强制优化器运行 并行计划。如果您没有获得并行计划,那么您会遇到不同的问题(可能 Prod 中的 MAXDOP 设置设置为 1)。如果你确实得到了一个并行计划,并且它提高了性能,那么你就已经确定了问题(你需要一个并行计划)。如果并行计划不能解决问题,那么您可能需要考虑 table 上的非聚集过滤列存储索引,该索引包含查询中的所有列,然后使用以下内容进行过滤:

WHERE Is_Target_Employee = 1 OR Is_Source_Employee = 1

您现在正在进行大扫描,正在阅读大量您不需要阅读的行。

Post 如有任何问题请回复。

2018 年 12 月 6 日更新: 抱歉更新晚了,出现了很多工作内容。

我仔细研究了执行计划,发现了一些有趣的事情。在阅读@Martin_Smith 发布的内容之前,我截取了这些屏幕截图:

我 100% 同意串行与并行执行计划不是这里的问题,但是,就您的生产计划而言,运行串行执行计划会使糟糕的执行计划变得更慢。正如 Martin 解释的那样,问题在于散列匹配计划是更好的计划。

两个计划都从列存储索引中检索相似数量的行(Dev 中为 5M,Prod 中为 6M)。在每个计划中,所有行都被过滤,但是在 Prod 计划中 所有 行由排序运算符再次 处理,与开发计划中的 338 行及其哈希聚合相比。

不管你扔多少 CPU:针对 630 万行的 19 列排序会很慢,尤其是串行计划 . IMO 并行性的最佳用途之一是处理此类大型任务。也就是说,不需要排序。我以前见过这个,当优化器可以使用排序(如在您的 Prod 计划中)或哈希(如在您的开发计划中)来解决查询时 - 它会在您强制并行执行时选择具有哈希的计划计划。我怀疑在您的情况下强制执行并行计划会导致优化器选择使用哈希的计划。

最后 - 我之前忘了提这个,不要在 Prod 中使用 OPTION (QUERYTRACEON 8649);它没有记录。我用它来测试。在产品中使用 make_parallel by Adam Machanic

串行与并行并不是真正的问题,因为无论如何你的最大并行度只有 2。

内存分配不足和大量排序溢出(达到 8 级)

您的查询 returns 305 行,但 SQL 服务器在一个计划中估计 2,561,980,在另一个计划中估计 3,709,060

对于 305 行,您需要一个哈希聚合,因为它只需要内存用于 305 个不同的分组值,而不需要内存用于整个 600 万加上排序使用的额外开销。

即使在使用散列聚合的计划中,输出行数的高估也意味着您会收到过多的内存授予警告。

The query memory grant detected "ExcessiveGrant", which may impact the reliability. Grant size: Initial 831,800 KB, Final 831,800 KB, Used 20,480 KB.

为此,您可以尝试以下操作来创建多列统计信息,目的是准确估计组数,以便 SQL 服务器自然选择具有适当大小的内存授予的散列组。 FULLSCAN 可能不需要,但当我设置测试时,默认采样似乎不足以让优化器使用新统计数据中的密度信息和新基数估计器。

CREATE STATISTICS SomeName ON  Warehouse_20181204  (
    [Record_Transformation_ACCRUALS],
    [Record_Transformation_FA:AMORTIZATION],   
    [Record_Transformation_BONUS:AMORTIZATION],
    [Record_Transformation_CPH:BYLABOUR],
    [Record_Transformation_CPH:BYTARGETHOURS],
    [Record_Transformation_OVERHEAD:CULTURE],
    [Record_Transformation_DEDICATED COSTCENTER],
    [Record_Transformation_PUSHDOWN:EXPENSE],
    [Record_Transformation_OVERHEAD:FACILITIES],
    [Record_Transformation_OVERHEAD:GENOME],
    [Record_Transformation_TAXES:MANAGEMENT],
    [Record_Transformation_TAXES:MARKETING],
    [Record_Transformation_OVERHEAD:OFFICETECH],
    [Record_Transformation_EXPENSE:PASSTHROUGH],
    [Record_Transformation_OVERHEAD:PEOPLEPRACTICES],
    [Record_Transformation_OVERHEAD:RECRUITING],
    [Record_Transformation_TAXES:SALES],
    [Record_Transformation_Static Transfer],
    [Record_Label] ) WITH FULLSCAN

或者您可以考虑重组您的数据库,使 20 列左右的列与 305 行(加上数据中存在但被 WHERE 子句排除的任何其他组合)位于单独的 table 中而你原来的 table 只是有一个 id 引用回这个新的 table.

然后您可以将其重写为 SELECT from the small table where groupid IN (SELECT groupid FROM large table WHERE....)