SQL SERVER 为两个相似的查询生成了不同且未优化的执行计划

SQL SERVER generates different and unoptimized execution plan for two similar queries

以下两个查询是用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

我不清楚为什么没有选择索引查找。

这些是在 table [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
)

Table [Cols] 有大约 600 行和 table [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 生成的。

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

您的问题查询是

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 table,按 Name, Surname 排序,然后继续计算 Presences.Regs 中的 MAX(Date),其中 r.ColId 匹配外行的相应 c.Id

理想的索引是 ColId, Date 上的索引 - 因此它可以搜索索引并只读取 ColId 的最后一个索引。

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

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

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

解决此问题的最简单方法是为其提供 ColId, Date 的理想索引 - 这涵盖了查询,因此删除了甚至在 "good" 计划中也存在的查找,这是一个明显的最好的选择,这样可以消除 SQL 服务器对 select "bad" 计划的诱惑。

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