摘要
主要问题是:
- 优化器的计划选择假定值的均匀分布。
- 缺少合适的索引意味着:
- 扫描表是唯一的选择。
- 该联接是幼稚的嵌套循环联接,而不是索引嵌套循环联接。在幼稚的联接中,联接谓词是在联接处评估的,而不是在联接的内侧向下推。
细节
这两个计划在本质上非常相似,尽管性能可能大不相同:
用额外的列计划
首先使用在合理时间内未完成的多余的列:

有趣的功能是:
- 节点0的顶部将返回的行限制为100。它还为优化器设置了行目标,因此计划中位于其下方的所有内容均被选择为快速返回前100行。
- 节点4处的扫描从表中查找行,其中行
Start_Time
不为null,State
为3或4,并且Operation_Type
为列出的值之一。该表将被完全扫描一次,每一行都将根据提到的谓词进行测试。只有通过所有测试的行才能继续进行排序。优化程序估计将有38,283行。
- 节点3处的“排序”将消耗“节点4”处的“扫描”中的所有行,并按顺序对其进行排序
Start_Time DESC
。这是查询请求的最终显示顺序。
- 优化器估计必须从Sort中读取93行(实际上是93.2791),以便整个计划返回100行(考虑了联接的预期效果)。
- 嵌套循环联接在节点2上有望执行其内部输入(下部分支)94次(实际上是94.2791)。出于技术原因,节点1处的停止并行性交换需要额外的行。
- 节点5上的扫描在每次迭代时完全扫描表。它查找
Start_Time
不为null且State
为3或4的行。估计每次迭代将产生400,875行。经过94.2791次迭代,总行数接近3800万。
- 节点2上的嵌套循环联接也应用联接谓词。它检查是否
Operation_Type
匹配,Start_Time
从节点4小于Start_Time
从节点5,Start_Time
从节点5小于Finish_Time
从节点4,以及两个Id
值不匹配。
- 节点1处的Gather流(停止并行交换)会合并来自每个线程的有序流,直到产生100行为止。跨多个流进行合并的顺序保持性要求在步骤5中提到额外的行。
效率低下显然在上面的步骤6和7。如果每次迭代仅扫描94次(如优化程序所预测的那样),则对节点5的表进行完全扫描仅是稍微合理一点。节点2上每行约3,800万的比较也是一个很大的代价。
至关重要的是,由于93/94行目标估计值取决于值的分布,因此也很可能是错误的。在没有更多详细信息的情况下,优化器假定分布均匀。简而言之,这意味着如果希望表中1%的行符合条件,则优化程序将说明要找到1个匹配的行,则需要读取100行。
如果您运行此查询完成操作(这可能需要很长时间),则很可能会发现必须从“排序”中读取多于93/94行才能最终生成100行。在最坏的情况下,将使用“排序”中的最后一行找到第100行。假设优化器在节点4上的估计是正确的,这意味着在节点5上运行扫描38,284次,总计约150亿行。如果“扫描”估计值也没有,可能会更多。
该执行计划还包括缺少索引警告:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 72.7096%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([Operation_Type],[State],[Start_Time])
INCLUDE ([Id],[Parameters])
优化器提醒您以下事实:向表添加索引会提高性能。
没有多余列的计划

这实际上与上一个计划完全相同,只是在节点6处增加了索引假脱机,在节点5处增加了过滤器。重要的区别是:
- 节点6处的索引假脱机是急切假脱机。它急切地消耗其下面的扫描结果,并构建一个在
Operation_Type
和上键控的临时索引Start_Time
,并Id
作为非键列。
- 现在,节点2上的嵌套循环连接是索引连接。没有连接谓词这里评估,而不是每一次迭代的电流值
Operation_Type
,Start_Time
,Finish_Time
,和Id
从在节点4处扫描被传递到内侧分支作为外部引用。
- 在节点7上的扫描仅执行一次。
- 节点6的索引假脱机从临时索引中查找
Operation_Type
与当前外部参考值匹配的行,并且该行在Start_Time
由Start_Time
和Finish_Time
外部参考定义的范围内。
- 节点5的过滤器测试
Id
索引假脱机中的值是否不等于当前外部参考值Id
。
关键改进包括:
- 内侧扫描仅执行一次
- 上临时索引(
Operation_Type
,Start_Time
)与Id
作为包括列允许索引嵌套循环联接。该索引用于在每次迭代中查找匹配的行,而不是每次扫描整个表。
和以前一样,优化器包括有关缺少索引的警告:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 24.1475%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([State],[Start_Time])
INCLUDE ([Id],[Operation_Type])
GO
结论
没有优化器的计划更快,因为优化器选择为您创建一个临时索引。
带有额外列的计划会使临时索引的构建成本更高。所述[Parameters
]列是nvarchar(2000)
,这将高达4000个字节添加到索引中的每一行。额外的成本足以使优化器相信,在每次执行时构建临时索引将无法收回成本。
在两种情况下,优化器都会警告说永久索引将是更好的解决方案。索引的理想组成取决于您的工作量。对于此特定查询,建议的索引是一个合理的起点,但是您应该了解所涉及的收益和成本。
建议
大量可能的索引对此查询将是有益的。重要的一点是,需要某种非聚集索引。根据提供的信息,我认为一个合理的指标是:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);
我也很想组织查询更好一些,并延迟查找[Parameters]
聚簇索引中的宽列,直到找到前100行(Id
用作键)为止:
SELECT TOP (100)
BTQ1.id,
BTQ2.id,
BTQ3.[Parameters],
BTQ4.[Parameters]
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
-- Look up the [Parameters] values
JOIN dbo.Batch_Tasks_Queue AS BTQ3
ON BTQ3.Id = BTQ1.Id
JOIN dbo.Batch_Tasks_Queue AS BTQ4
ON BTQ4.Id = BTQ2.Id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
-- These predicates are not strictly needed
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
在[Parameters]
不需要列的地方,查询可以简化为:
SELECT TOP (100)
BTQ1.id,
BTQ2.id
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
该FORCESEEK
提示是存在的,以帮助确保优化器选择索引嵌套循环计划(有一个基于成本的诱惑优化程序选择散列或(多对多)合并联接否则,这往往并不好这种类型的在实践中进行查询。两者最终都会产生大量残差;在散列的情况下,每个存储桶都有很多项目,并且为合并而倒带很多)。
另类
如果查询(包括其特定值)对于读取性能特别重要,我将考虑使用两个过滤索引:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
CREATE NONCLUSTERED INDEX i2
ON dbo.Batch_Tasks_Queue (Operation_Type, [State], Start_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
对于不需要该[Parameters]
列的查询,使用过滤后的索引的估计计划为:

索引扫描自动返回所有符合条件的行,而无需评估任何其他谓词。对于索引嵌套循环联接的每次迭代,索引查找执行两次查找操作:
- 在
Operation_Type
和State
= 3 上的搜索前缀匹配,然后搜索Start_Time
值的范围,Id
不等式的残差谓词。
- 在
Operation_Type
和State
= 4 上的查找前缀匹配,然后查找Start_Time
值范围,Id
不等式上的残差谓词。
在[Parameters]
需要该列的地方,查询计划仅为每个表最多添加100个单例查找:

最后一点,您应该考虑使用内置的标准整数类型,而不是numeric
在适用的地方。