添加两列时查询不响应


9

当我在选择中添加两列时,查询没有响应。列的类型是nvarchar(2000)。这有点不寻常。

  • SQL Server版本为2014。
  • 只有一个主索引。
  • 整个记录只有1000行。

这是之前的执行计划(XML showplan):

在此处输入图片说明

之后的执行计划(XML showplan):

在此处输入图片说明

这是查询:

select top(100)
  Batch_Tasks_Queue.id,
  btq.id,
  Batch_Tasks_Queue.[Parameters], -- this field
  btq.[Parameters]  -- and this field
from
        Batch_Tasks_Queue with(nolock)
    inner join  Batch_Tasks_Queue btq with(nolock)  on  Batch_Tasks_Queue.Start_Time < btq.Start_Time
                            and btq.Start_Time < Batch_Tasks_Queue.Finish_Time
                            and Batch_Tasks_Queue.id <> btq.id                            
                            and btq.Start_Time is not null
                            and btq.State in (3, 4)                          
where
    Batch_Tasks_Queue.Start_Time is not null      
    and Batch_Tasks_Queue.State in (3, 4)
    and Batch_Tasks_Queue.Operation_Type = btq.Operation_Type
    and Batch_Tasks_Queue.Operation_Type not in (23, 24, 25, 26, 27, 28, 30)

order by
    Batch_Tasks_Queue.Start_Time desc

整个结果计数为17行。脏数据(nolock提示)并不重要。

这是表的结构:

CREATE TABLE [dbo].[Batch_Tasks_Queue](
    [Id] [int] NOT NULL,
    [OBJ_VERSION] [numeric](8, 0) NOT NULL,
    [Operation_Type] [numeric](2, 0) NULL,
    [Request_Time] [datetime] NOT NULL,
    [Description] [varchar](1000) NULL,
    [State] [numeric](1, 0) NOT NULL,
    [Start_Time] [datetime] NULL,
    [Finish_Time] [datetime] NULL,
    [Parameters] [nvarchar](2000) NULL,
    [Response] [nvarchar](max) NULL,
    [Billing_UserId] [int] NOT NULL,
    [Planned_Start_Time] [datetime] NULL,
    [Input_FileId] [uniqueidentifier] NULL,
    [Output_FileId] [uniqueidentifier] NULL,
    [PRIORITY] [numeric](2, 0) NULL,
    [EXECUTE_SEQ] [numeric](2, 0) NULL,
    [View_Access] [numeric](1, 0) NULL,
    [Seeing] [numeric](1, 0) NULL,
 CONSTRAINT [PKBachTskQ] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [Batch_Tasks_QueueData]
) ON [Batch_Tasks_QueueData] TEXTIMAGE_ON [Batch_Tasks_QueueData]
GO    
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue]  WITH NOCHECK ADD  CONSTRAINT [FK0_BtchTskQ_BlngUsr] FOREIGN KEY([Billing_UserId])
REFERENCES [dbo].[BILLING_USER] ([ID])
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue] CHECK CONSTRAINT [FK0_BtchTskQ_BlngUsr]
GO

有关此问题的讨论已移至此聊天室
保罗·怀特9

Answers:


15

摘要

主要问题是:

  • 优化器的计划选择假定值的均匀分布。
  • 缺少合适的索引意味着:
    • 扫描表是唯一的选择。
    • 该联接是幼稚的嵌套循环联接,而不是索引嵌套循环联接。在幼稚的联接中,联接谓词是在联接处评估的,而不是在联接的内侧向下推。

细节

这两个计划在本质上非常相似,尽管性能可能大不相同:

用额外的列计划

首先使用在合理时间内未完成的多余的列:

慢计划

有趣的功能是:

  1. 节点0的顶部将返回的行限制为100。它还为优化器设置了行目标,因此计划中位于其下方的所有内容均被选择为快速返回前100行。
  2. 节点4处的扫描从表中查找行,其中行Start_Time不为null,State为3或4,并且Operation_Type为列出的值之一。该表将被完全扫描一次,每一行都将根据提到的谓词进行测试。只有通过所有测试的行才能继续进行排序。优化程序估计将有38,283行。
  3. 节点3处的“排序”将消耗“节点4”处的“扫描”中的所有行,并按顺序对其进行排序Start_Time DESC。这是查询请求的最终显示顺序。
  4. 优化器估计必须从Sort中读取93行(实际上是93.2791),以便整个计划返回100行(考虑了联接的预期效果)。
  5. 嵌套循环联接在节点2上有望执行其内部输入(下部分支)94次(实际上是94.2791)。出于技术原因,节点1处的停止并行性交换需要额外的行。
  6. 节点5上的扫描在每次迭代时完全扫描表。它查找Start_Time不为null且State为3或4的行。估计每次迭代将产生400,875行。经过94.2791次迭代,总行数接近3800万。
  7. 节点2上的嵌套循环联接也应用联接谓词。它检查是否Operation_Type匹配,Start_Time从节点4小于Start_Time从节点5,Start_Time从节点5小于Finish_Time从节点4,以及两个Id值不匹配。
  8. 节点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处增加了过滤器。重要的区别是:

  1. 节点6处的索引假脱机是急切假脱机。它急切地消耗其下面的扫描结果,并构建一个在Operation_Type和上键控的临时索引Start_Time,并Id作为非键列。
  2. 现在,节点2上的嵌套循环连接是索引连接。没有连接谓词这里评估,而不是每一次迭代的电流值Operation_TypeStart_TimeFinish_Time,和Id从在节点4处扫描被传递到内侧分支作为外部引用。
  3. 在节点7上的扫描仅执行一次。
  4. 节点6的索引假脱机从临时索引中查找Operation_Type与当前外部参考值匹配的行,并且该行在Start_TimeStart_TimeFinish_Time外部参考定义的范围内。
  5. 节点5的过滤器测试Id索引假脱机中的值是否不等于当前外部参考值Id

关键改进包括:

  • 内侧扫描仅执行一次
  • 上临时索引(Operation_TypeStart_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]列的查询,使用过滤后的索引的估计计划为:

简单的筛选索引计划

索引扫描自动返回所有符合条件的行,而无需评估任何其他谓词。对于索引嵌套循环联接的每次迭代,索引查找执行两次查找操作:

  1. Operation_TypeState= 3 上的搜索前缀匹配,然后搜索Start_Time值的范围,Id不等式的残差谓词。
  2. Operation_TypeState= 4 上的查找前缀匹配,然后查找Start_Time值范围,Id不等式上的残差谓词。

[Parameters]需要该列的地方,查询计划仅为每个表最多添加100个单例查找:

带额外列的过滤索引计划

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.