usr*_*usr 15 sql-server optimization execution-plan sql-server-2012 offset-fetch
OFFSET ... FETCHSQL Server 2012 引入的新模型提供了简单且快速的分页。考虑到这两种形式在语义上相同且非常常见,为什么会有任何差异?
人们会假设优化器可以识别两者并将它们(简单地)优化到最大程度。
这是一个非常简单的案例,OFFSET ... FETCH根据成本估算,速度提高了约 2 倍。
SELECT * INTO #objects FROM sys.objects
SELECT *
FROM (
SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
FROM #objects
) x
WHERE r >= 30 AND r < (30 + 10)
ORDER BY object_id
SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY
Run Code Online (Sandbox Code Playgroud)

可以通过创建 CIobject_id或添加过滤器来改变此测试用例,但不可能消除所有计划差异。OFFSET ... FETCH总是更快,因为它在执行时做的工作更少。
Pau*_*ite 14
问题中的示例不会产生完全相同的结果(该OFFSET示例存在一对一错误)。下面更新的表单解决了这个问题,删除了ROW_NUMBER案例的额外排序,并使用变量使解决方案更通用:
DECLARE
@PageSize bigint = 10,
@PageNumber integer = 3;
WITH Numbered AS
(
SELECT TOP ((@PageNumber + 1) * @PageSize)
o.*,
rn = ROW_NUMBER() OVER (
ORDER BY o.[object_id])
FROM #objects AS o
ORDER BY
o.[object_id]
)
SELECT
x.name,
x.[object_id],
x.principal_id,
x.[schema_id],
x.parent_object_id,
x.[type],
x.type_desc,
x.create_date,
x.modify_date,
x.is_ms_shipped,
x.is_published,
x.is_schema_published
FROM Numbered AS x
WHERE
x.rn >= @PageNumber * @PageSize
AND x.rn < ((@PageNumber + 1) * @PageSize)
ORDER BY
x.[object_id];
SELECT
o.name,
o.[object_id],
o.principal_id,
o.[schema_id],
o.parent_object_id,
o.[type],
o.type_desc,
o.create_date,
o.modify_date,
o.is_ms_shipped,
o.is_published,
o.is_schema_published
FROM #objects AS o
ORDER BY
o.[object_id]
OFFSET @PageNumber * @PageSize - 1 ROWS
FETCH NEXT @PageSize ROWS ONLY;
Run Code Online (Sandbox Code Playgroud)
该ROW_NUMBER计划的估计成本为0.0197935:

该OFFSET计划的估计成本为0.0196955:

这节省了0.000098 个估计成本单位(尽管OFFSET如果您想为每一行返回一个行号,该计划将需要额外的运算符)。OFFSET一般来说,该计划仍会稍微便宜一些,但请记住,估计成本正是如此——仍然需要进行真正的测试。两个计划中的大部分成本是完整排序的输入集的成本,因此有用的索引将对两种解决方案都有益。
在使用常量字面值的情况下(例如OFFSET 30在原始示例中),优化器可以使用 TopN 排序而不是后跟 Top 的完整排序。当从TOPN所需要的行进行排序是一个常量文字和<= 100(的总和OFFSET和FETCH)执行引擎可以使用不同的排序算法可以比排序广义TOPN执行得更快。这三种情况总体上具有不同的性能特征。
至于为什么优化器不自动将ROW_NUMBER语法模式转换为 use OFFSET,有以下几个原因:
OFFSET计划不能保证在所有情况下都更好上面第三点的一个例子发生在分页集很宽的地方。与使用或扫描索引相比,使用非聚集索引查找所需的键并针对聚集索引手动查找可以更有效。有需要考虑其他问题,如果寻呼应用程序需要知道有多少行或页总共有。有“抵消”方法的相对优劣的另一个很好的讨论“键寻找”和这里。OFFSETROW_NUMBER
总的来说,OFFSET如果合适的话,在彻底测试之后,人们最好做出明智的决定来更改他们的分页查询以使用。
稍微摆弄一下您的查询,我得到了相等的成本估算(50/50) 和相等的 IO 统计数据:
; WITH cte AS
(
SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
FROM #objects
)
SELECT *
FROM cte
WHERE r >= 30 AND r < 40
ORDER BY r
SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY
Run Code Online (Sandbox Code Playgroud)
这通过排序r而不是 来避免出现在您的版本中的额外排序object_id。