【问题标题】:SQL SERVER generates different and unoptimized execution plan for two similar queriesSQL SERVER 为两个相似的查询生成不同且未优化的执行计划
【发布时间】:2020-08-02 23:55:44
【问题描述】:

以下两个查询是用 SQL SERVER 执行的

SELECT TOP(10) [c].[Name] AS [I0], [c].[Surname] AS [I1],(
    SELECT MAX([r].[Date])
    FROM [Presences].[Regs] AS [r]
    WHERE ([c].[Id] = [r].[ColId]) AND [r].[ColId] IS NOT NULL) AS [I7], [c].[Id] AS [I10]
FROM [Presences].[Cols] AS [c]
SELECT TOP(10) [c].[Code] AS [I0], [c].[Description] AS [I1], (
    SELECT MAX([r].[Date])
    FROM [Presences].[Regs] AS [r]
    WHERE ([c].[Id] = [r].[CantId]) AND [r].[CantId] IS NOT NULL) AS [I9], [c].[Id] AS [I10]
FROM [Presences].[Cants] AS [c]

第二个查询的计算时间不到 1 秒,但第一个查询的计算时间超过 20 秒。

事实上,第一个生成的执行计划非常不同:

Bad execution plan of the first query

如果与第二个相比:

Good execution plan of the second query

我不清楚为什么不选择索引搜索。

这些是在表 [Regs] 上声明的索引:

CREATE NONCLUSTERED INDEX [IX_Regs_Date] ON [Presences].[Regs]
(
    [Date] ASC
)

CREATE NONCLUSTERED INDEX [IX_Regs_ColId] ON [Presences].[Regs]
(
    [ColId] ASC
)

CREATE NONCLUSTERED INDEX [IX_Regs_CantId] ON [Presences].[Regs]
(
    [CantId] ASC
)

表格 [Cols] 有大约 600 行,表格 [Cants] 有 21000 个。

有趣的事实是,以下查询(使用 FORCESEEK)生成了正确的执行计划:

SELECT TOP(10) [c].[Name] AS [I0], [c].[Surname] AS [I1],(
    SELECT MAX([r].[Date])
    FROM [Presences].[Regs] AS [r]
    WITH (FORCESEEK)  
    WHERE ([c].[Id] = [r].[ColId]) AND [r].[ColId] IS NOT NULL) AS [I8]
FROM [Presences].[Cols] AS [c]
ORDER BY [c].[Name], [c].[Surname]

但我无法指定此提示,因为查询是使用 ORM 生成的。

如果您需要更多信息,我很乐意提供。

【问题讨论】:

  • 你能在这里分享你的计划吗:brentozar.com/pastetheplan 如果你只提供计划的图片,很多细节都会丢失。
  • 你能分享一下 Cols 和 Cants 表的索引吗?
  • 这是错误的查询执行计划:link,这是好的:link。其他表只有主键 (Id) 上的聚集索引
  • 好像执行计划的不同与表的行数有关。如果我们从 600 Cols 行移动到 1000000(难以置信),查询就会成功
  • 那么你能上传粘贴计划实际显示问题的计划吗?扫描

标签: sql sql-server sql-execution-plan


【解决方案1】:

您的问题查询是

SELECT TOP(10) c.Name                      AS I0,
               c.Surname                   AS I1,
               (SELECT MAX(r.Date)
                FROM   Presences.Regs AS r
                WHERE  ( c.Id = r.ColId )) AS I7,
               c.Id                        AS I10
FROM   Presences.Cols AS c 
ORDER BY c.Name, c.Surname

好的计划和坏计划的顶部是相同的

它扫描Cols 表,按Name, Surname 排序,然后继续计算Presences.Regs 中的MAX(Date),其中r.ColId 与外行中对应的c.Id 匹配。

理想的索引是ColId, Date 上的一个索引——因此它可以查找索引并读取该ColId 的最后一个索引。

你没有这个索引,所以它有两个选择

  1. 扫描IX_Regs_Date - 这是按日期顺序,因此它可以在找到与c.Id = r.ColId 匹配的第一行后立即停止扫描
  2. 寻找IX_Regs_ColId。获取与谓词匹配的所有行,然后将它们聚合以找到MAX

对于选项 1,它估计平均每次扫描只需读取 283 行,然后才能找到第一个匹配 c.Id = r.ColId 的行。实际上,每次执行它读取1,358,719(并且有 10 次执行,因此这对应于 1358 万个键查找)。表中有1,517,230 行,因此看起来好像出于某种原因,所有匹配连接的行都聚集在表中的较晚日期。

解决此问题的最简单方法是为其提供理想的索引ColId, Date - 这涵盖了查询,因此即使在“好”计划中也会删除存在的查找,并且是明显的最佳选择,因此将删除诱惑 SQL Server 选择“坏”计划。

--Suggested replacement for IX_Regs_ColId
CREATE NONCLUSTERED INDEX [IX_Regs_ColId_Date] ON [Presences].[Regs]
(
    [ColId] ASC,
    [Date] ASC
)

【讨论】:

  • 非常详细的解释。我设法在包含 ColId 和 CantId 的 Date 上创建和索引,这涵盖了遵循相同行为的两个查询。感谢您的帮助!
  • @BrandoCaserotti - 如果索引在[Date] INCLUDE ([ColId], [CantId]) 上,那么这将消除查找,但您仍然可能会遇到从该索引扫描数百万行的情况。理想的索引(仅考虑此查询)是此答案中建议的索引。嵌套循环的每次执行只需要从Regs中读取一行
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-10-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-30
  • 1970-01-01
  • 2011-08-19
相关资源
最近更新 更多