为什么 OFFSET ... FETCH 和旧式 ROW_NUMBER 方案之间存在执行计划差异?

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)

偏移量获取.png

可以通过创建 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(的总和OFFSETFETCH)执行引擎可以使用不同的排序算法可以比排序广义TOPN执行得更快。这三种情况总体上具有不同的性能特征。

至于为什么优化器不自动将ROW_NUMBER语法模式转换为 use OFFSET,有以下几个原因:

  1. 编写一个匹配所有现有用途的转换几乎是不可能的
  2. 让一些分页查询自动转换而不是其他分页查询可能会令人困惑
  3. OFFSET计划不能保证在所有情况下都更好

上面第三点的一个例子发生在分页集很宽的地方。与使用或扫描索引相比,使用非聚集索引查找所需的键并针对聚集索引手动查找可以更有效。有需要考虑其他问题,如果寻呼应用程序需要知道有多少行或页总共有。有“抵消”方法的相对优劣的另一个很好的讨论“键寻找”和这里OFFSETROW_NUMBER

总的来说,OFFSET如果合适的话,在彻底测试之后,人们最好做出明智的决定来更改他们的分页查询以使用。


Mar*_*ith 5

稍微摆弄一下您的查询,我得到了相等的成本估算(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

  • @usr 对于 Paul 的观点,在某些情况下,您会发现优化器的功能存在差距。如果它们不会被修复,并且您知道编写查询的更好方法,请使用更好的方法。病人:“医生,我做x的时候疼。” 医生:“不要做x。” :-) (4认同)
  • @usr ROW_NUMBER() 使用的 ORDER BY 定义了它如何分配数字。它没有承诺输出顺序 - 这是分开的。碰巧它经常重合,但不能保证。 (2认同)