为什么我看到的是所有读取行的键查找,而不是所有与 where 子句匹配的行?

Twi*_*mes 14 sql-server nonclustered-index bookmark-lookup pagination

我有一个如下表:

create table [Thing]
(
    [Id] int constraint [PK_Thing_Id] primary key,
    [Status] nvarchar(20),
    [Timestamp] datetime2,
    [Foo] nvarchar(100)
)
Run Code Online (Sandbox Code Playgroud)

Status在和字段上使用非聚集、非覆盖索引Timestamp

create nonclustered index [IX_Status_Timestamp] on [Thing] ([Status], [Timestamp] desc)
Run Code Online (Sandbox Code Playgroud)

如果我查询这些行的“页面”,使用偏移/获取如下,

select * from [Thing]
where Status = 'Pending'
order by [Timestamp] desc
offset 2000 rows
fetch next 1000 rows only
Run Code Online (Sandbox Code Playgroud)

我知道该查询需要读取总共 3000 行才能找到我感兴趣的 1000 行。然后我希望它对这 1000 行中的每一行执行键查找以获取索引中未包含的字段。

但是,执行计划表明它正在对所有 3000 行进行键查找。我不明白为什么,当唯一的条件(按[状态]过滤和按[时间戳]排序)都在索引中时。

在此输入图像描述

如果我用 cte 重新表述查询,如下所示,我或多或少会得到我期望第一个查询执行的操作:

with ids as
(
    select Id from [Thing]
    where Status = 'Pending'
    order by [Timestamp] desc
    offset 2000 rows
    fetch next 1000 rows only
)

select t.* from [Thing] t
join ids on ids.Id = t.Id
order by [Timestamp] desc
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

来自 SSMS 的一些统计数据用于比较 2 个查询:

原来的 具有热膨胀系数
逻辑读 12265 4140
子树成本 9.79 3.33
内存授予 0 3584 KB

乍一看,CTE 版本似乎“更好”,尽管我不知道该对它为工作台提供内存授予的事实给予多少重视。(来自的消息set statistics io on表明工作台上任何类型的读取均为零)

我说第一个查询应该能够首先隔离相关的 1000 行(尽管这需要先读取过去 2000 行其他行),然后只对这 1000 行进行键查找,我这样说是错误的吗?必须尝试使用​​ CTE 查询“强制”该行为似乎有点奇怪。

(作为第二个问题:我假设 CTE 方法的最后一部分需要order by对连接的结果执行自己的操作,即使 CTE 本身有一个order by,因为在连接期间排序可能会丢失。是这是正确的吗?)

Pau*_*ite 16

从根本上来说,这是一个长期存在的优化器限制。

SQL Server 不考虑将键查找转变为聚集索引查找。键查找必须几乎立即跟随与其关联的非聚集索引访问(由于I/O 原因,可能存在中间排序)。

有多种方法可以重写查询以尽可能长时间地仅对索引键进行操作,而无需引入示例中看到的排序:

WITH IDs AS
(
    SELECT T.* 
    FROM dbo.Thing AS T
    WHERE T.[Status] = N'Pending'
    ORDER BY T.[Timestamp] DESC
    OFFSET 2000 ROWS
    FETCH NEXT 1000 ROWS ONLY
)
SELECT 
    T.* 
FROM IDs AS I
JOIN dbo.Thing AS T
    ON T.Id = I.Id
ORDER BY
    I.[Timestamp] DESC;
Run Code Online (Sandbox Code Playgroud)

计划

或者

SELECT 
    T2.* 
FROM dbo.Thing AS T
JOIN dbo.Thing AS T2
    ON T2.Id = T.Id
WHERE 
    T.[Status] = N'Pending'
ORDER BY 
    T.[Timestamp] DESC
    OFFSET 2000 ROWS
    FETCH NEXT 1000 ROWS ONLY;
Run Code Online (Sandbox Code Playgroud)

计划

通过帮助优化器“查看”保留的排序顺序,可以消除排序的需要。

进一步阅读: