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
的最后一个索引。
你没有这个索引,所以它有两个选择
- 扫描
IX_Regs_Date
- 这是按日期顺序排列的,因此它可以在找到匹配 c.Id = r.ColId
的第一行后立即停止扫描
- 求
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
)
以下两个查询是用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
的最后一个索引。
你没有这个索引,所以它有两个选择
- 扫描
IX_Regs_Date
- 这是按日期顺序排列的,因此它可以在找到匹配c.Id = r.ColId
的第一行后立即停止扫描
- 求
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
)