添加两列时查询没有响应

Ham*_*thi 9 performance sql-server sql-server-2014 query-performance

当我将两列添加到我的选择中时,查询没有响应。列的类型是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
Run Code Online (Sandbox Code Playgroud)

整个结果计数为 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
Run Code Online (Sandbox Code Playgroud)

Pau*_*ite 15

概括

主要问题是:

  • 优化器的计划选择假定值的均匀分布。
  • 缺乏合适的索引意味着:
    • 扫描表格是唯一的选择。
    • 该联接是天真的嵌套循环连接,而不是指数嵌套循环连接。在朴素连接中,连接谓词在连接处计算,而不是被推到连接的内侧。

细节

这两个计划从根本上非常相似,但性能可能非常不同:

计划与额外的列

首先考虑没有在合理时间内完成的额外列:

慢计划

有趣的功能是:

  1. 节点 0 处的 Top 将返回的行数限制为 100。它还为优化器设置了一个行目标,因此计划中它下面的所有内容都被选择以快速返回前 100 行。
  2. 节点 4 处的扫描从表中查找Start_Time不为空、State为 3 或 4 并且Operation_Type是所列值之一的行。该表被完全扫描一次,每一行都针对提到的谓词进行测试。只有通过所有测试的行才会进入排序。优化器估计有 38,283 行符合条件。
  3. 节点 3 的 Sort 消耗节点 4 的 Scan 中的所有行,并按 的顺序对它们进行排序Start_Time DESC。这是查询请求的最终呈现顺序。
  4. 优化器估计必须从 Sort 读取 93 行(实际上是 93.2791),以便整个计划返回 100 行(考虑到连接的预期效果)。
  5. 节点 2 处的嵌套循环连接预计将执行其内部输入(下部分支)94 次(实际为 94.2791)。出于技术原因,节点 1 处的停止并行交换需要额外的行。
  6. 节点 5 的扫描在每次迭代时完全扫描表。它查找Start_Time不为空且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 Streams(停止并行交换)合并来自每个线程的有序流,直到产生 100 行。跨多个流合并的顺序保留性质是需要步骤 5 中提到的额外行。

极大的低效率显然是在上面的第 6 步和第 7 步。如果每次迭代完全扫描节点 5 处的表,如果它只像优化器预测的那样发生 94 次,那么它甚至只是稍微合理。节点 2 每行约 3800 万组比较也是一个很大的成本。

至关重要的是,93/94 排行目标估计也很可能是错误的,因为它取决于值的分布。在没有更详细信息的情况下,优化器假设均匀分布。简单来说,这意味着如果表中 1% 的行预期符合条件,优化器会认为要找到 1 个匹配的行,它需要读取 100 行。

如果您运行此查询直到完成(这可能需要很长时间),您很可能会发现必须从 Sort 读取多于 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])
Run Code Online (Sandbox Code Playgroud)

优化器会提醒您向表添加索引会提高性能。

没有额外列的计划

不那么慢的计划

这本质上与前一个计划完全相同,只是在节点 6 添加了 Index Spool,在节点 5 添加了过滤器。 重要的区别是:

  1. 节点 6 处的 Index Spool 是一个 Eager Spool。它急切地使用它下面的扫描结果,并构建一个以Operation_Typeand为键的临时索引Start_TimeId作为非键列。
  2. 节点 2 处的嵌套循环联接现在是一个索引联接。没有连接谓词这里评估,而不是每一次迭代的电流值Operation_TypeStart_TimeFinish_Time,和Id从在节点4处扫描被传递到内侧分支作为外部引用。
  3. 节点 7 处的扫描仅执行一次。
  4. 节点 6 处的索引假脱机从临时索引中查找Operation_Type与当前外部引用值相匹配的行,并且该行在由外部引用和外部引用Start_Time定义的范围内。Start_TimeFinish_Time
  5. 节点 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
Run Code Online (Sandbox Code Playgroud)

结论

没有额外列的计划更快,因为优化器选择为您创建临时索引。

带有额外列的计划会使临时索引的构建成本更高。所述[Parameters]列是nvarchar(2000),这将高达4000个字节添加到索引中的每一行。额外的成本足以说服优化器在每次执行时构建临时索引不会为它自己付出代价。

优化器在这两种情况下都警告说,永久索引将是更好的解决方案。索引的理想组合取决于您更广泛的工作量。对于此特定查询,建议的索引是一个合理的起点,但您应该了解所涉及的收益和成本。

推荐

范围广泛的可能索引将有利于此查询。重要的一点是需要某种非聚集索引。从提供的信息来看,我认为一个合理的指标是:

CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);
Run Code Online (Sandbox Code Playgroud)

我还想更好地组织查询,并延迟查找[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;
Run Code Online (Sandbox Code Playgroud)

[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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

对于不需要[Parameters]列的查询,使用过滤索引的估计计划为:

简单的过滤索引计划

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

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

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

带有额外列的过滤索引计划

最后要注意的是,您应该考虑使用内置的标准整数类型,而不是numeric在适用的情况下。