为什么我的 WHERE 子句受益于“包含”列?

sha*_*oth 12 index sql-server execution-plan azure-sql-database

根据这个答案,除非在用于限制的列上建立索引,否则查询将不会从索引中受益。

我有这个定义:

CREATE TABLE [dbo].[JobItems] (
    [ItemId]             UNIQUEIDENTIFIER NOT NULL,
    [ItemState]          INT              NOT NULL,
    [ItemPriority]       INT NOT NULL,
    [CreationTime]       DATETIME         NULL DEFAULT GETUTCDATE(),
    [LastAccessTime]     DATETIME         NULL DEFAULT GETUTCDATE(),
     -- other columns
 );

 CREATE UNIQUE CLUSTERED INDEX [JobItemsIndex]
    ON [dbo].[JobItems]([ItemId] ASC);
 GO

CREATE INDEX [GetItemToProcessIndex]
    ON [dbo].[JobItems]([ItemState], [ItemPriority], [CreationTime])
    INCLUDE (LastAccessTime);
GO
Run Code Online (Sandbox Code Playgroud)

和这个查询:

UPDATE TOP (150) JobItems 
SET ItemState = 17 
WHERE 
    ItemState IN (3, 9, 10)
    AND LastAccessTime < DATEADD (day, -2, GETUTCDATE()) 
    AND CreationTime < DATEADD (day, -2, GETUTCDATE());
Run Code Online (Sandbox Code Playgroud)

我查看了实际计划,只有一个索引搜索与谓词完全一样WHERE- 没有额外的“书签查找”要检索,LastAccessTime即使后者只“包含”到索引中,而不是索引的一部分。

在我看来,这种行为与列必须是索引的一部分而不仅仅是“包含”的规则相矛盾。

我观察到的行为是正确的吗?我如何提前知道我是否WHERE从包含的列中受益或需要该列成为索引的一部分?

Rob*_*ley 9

您的谓词与您的搜索谓词不同。

Seek Predicate 用于搜索索引中的有序数据。在这种情况下,它将执行三个查找,一个针对您感兴趣的每个 ItemState。除此之外,数据按 ItemPriority 顺序排列,因此无法执行进一步的“查找”操作。

但是在返回数据之前,它会使用 Predicate 检查每一行,我将其称为 Residual Predicate。它是根据 Seek Predicate 的结果完成的。

任何包含的列都不是有序数据的一部分,但可用于满足 Residual Predicate,而无需进行额外的 Lookup。

你可以看到我写的关于 Sargability 的材料。检查 SQLBits 的会话,特别是在http://bit.ly/Sargability

编辑:为了更好地显示 Residuals 的影响,请使用 undocumented 运行查询OPTION (QUERYTRACEON 9130),这会将 Residual 分离到一个单独的 Filter 运算符中(这实际上是将残差移动到 Seek 运算符之前的早期版本的计划)。它通过传递给过滤器的行数清楚地显示了无效搜索的影响。

还值得注意的是,由于 ItemState 上的 IN 子句,向左传递的数据实际上是按 ItemState 顺序,而不是按 ItemPriority 顺序。ItemState 上的复合索引后跟日期之一(例如 (ItemState, LastAccessTime))可用于具有三个 Seek(注意 Seek Predicate 显示一个 Seek 运算符内的三个搜索),每个针对两个级别,生成的数据为仍按 ItemState 顺序排列(例如,ItemState=3 且 LastAccessTime 小于某物,然后 ItemState=9 且 LastAccessTime 小于某物,然后 ItemState=10 且 LastAccessTime 小于某物)。

(ItemState, LastAccesTime, CreationTime) 上的索引不会比 (ItemState, LastAccessTime) 上的索引更有用,因为只有当您的 Seek 是针对特定 ItemState 和 LastAccessTime 组合而不是范围时,CreationTime 级别才有用。如果您对以 F 开头的姓氏感兴趣,就像电话簿不是按名字顺序排列的一样。

如果您想要一个复合索引,但由于您使用较早列的方式,您永远无法在 Seek Predicates 中使用较晚的列,那么您也可以将它们作为包含列使用,这样它们在索引(因为它们只存储在索引的叶级,而不是更高级别)但仍然可以避免查找并在 Residual 谓词中使用。

根据术语 Residual Predicate - 这是我自己对 Seek 属性的术语。合并连接显式地称其为等价的残差谓词,而哈希匹配称其为探测残差(如果匹配哈希,您可能会从 TSA 获得)。但是在 Seek 中,他们只是将其称为 Predicate,这使它看起来没有实际情况那么糟糕。


Jul*_*eur 3

GetItemToProcessIndex 不完全可查找,因为您的 where 子句处于 on ItemState + LastAccessTime + CreationTime。索引列和 where 子句并不完美匹配。

如果您在 上创建覆盖索引ItemState + LastAccessTime + CreationTime,则对于从 GetItemToProcessIndex 获得的每个匹配项,您还会获得主键 (ItemId) 的值。它只需确保第二个日期匹配即可。

这就是您跳转到该行在其页面上的位置并更新它所需的全部内容。

使用您当前的索引,它可能会帮助服务器找到具有您想要的 ItemState 的行,但它仍然必须从索引中读取所有行,以便在 LastAccessTime + CreationTime 上找到正确的匹配项。根据日期谓词和匹配集的大小以及必须排除的内容,它可能会导致比仅在 3 列上完全覆盖索引(它会查找 ItemState 和第二列(第一个索引日期))更多的 IO 。不过,可以包含索引中的第二个日期。额外的列不应在这 3 列之间建立索引,尽管它可以作为第四列(请参阅 rob 关于额外列的回答)。