为什么OFFSET…FETCH与旧式ROW_NUMBER方案之间的执行计划有所不同?


15

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

offset-fetch.png

可以通过在其上创建配置项object_id或添加过滤器来更改此测试用例,但无法消除所有计划差异。OFFSET ... FETCH总是更快,因为它在执行时的工作量较少。


不是很确定,因此将其作为注释,但是我猜想是因为您对行编号和最终结果集的条件排序顺序相同。由于在第二种条件下,优化器知道这一点,因此不需要再次对结果进行排序。但是,在第一种情况下,需要确保对外部选择的结果以及内部结果中的行编号进行排序。在#objects上创建适当的索引应该可以解决该问题
Akash

Answers:


13

问题中的示例所产生的结果并不完全相同(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;

ROW_NUMBER计划的估计成本为0.0197935

行号计划

OFFSET计划的估计成本为0.0196955

抵销计划

这样可以节省0.000098个估计成本单位(尽管OFFSET如果您想为每行返回一个行号,该计划将需要额外的运算符)。OFFSET总体而言,该计划仍会稍微便宜一些,但请记住,估计费用恰好是这样-仍然需要实际测试。这两个计划中的大部分成本都是整个输入集的成本,因此有用的索引会使这两个解决方案都受益。

在使用常量文字值的情况下(例如OFFSET 30,在原始示例中),优化器可以使用TopN排序,而不是紧随其后的完全排序。当从TOPN所需要的行进行排序是一个常量文字和<= 100(的总和OFFSETFETCH)执行引擎可以使用不同的排序算法可以比排序广义TOPN执行得更快。这三种情况总体上具有不同的性能特征。

关于优化器为何不自动将ROW_NUMBER语法模式转换为use OFFSET的原因,有很多原因:

  1. 编写与所有现有用途匹配的转换几乎是不可能的
  2. 使某些分页查询自动转换,而其他的则不会造成混淆
  3. OFFSET不能保证该计划在所有情况下都更好

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

总体而言,最好是OFFSET在经过全面测试后,让人们做出明智的决定,以更改其分页查询以使用。


1
因此,通常情况下不进行转换的原因可能是很难找到可接受的工程折衷。您提供了可能的原因的充分理由。我必须说这是一个很好的答案。许多见解和新思想。我将问题悬而未决,然后选择最佳答案。
usr

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

这样可以避免使用r而不是排序时出现在您的版本中的其他排序object_id


感谢您的见解。现在,考虑到这一点,我已经看到优化器之前不了解ROW_NUMBER输出的排序性质。它认为该集合不受object_id的排序。或者至少没有同时按r和object_id排序。
usr

2
@usr ROW_NUMBER()使用的ORDER BY定义了它如何分配数字。它并不能保证输出顺序-这是分开的。碰巧它经常重合,但是不能保证。
亚伦·伯特兰

@AaronBertrand我知道ROW_NUMBER不对输出进行排序。但是,如果ROW_NUMBER由与输出相同的列排序,那么可以保证相同的顺序,对吗?因此查询优化器可以利用这一事实。因此,此查询中始终不需要两个排序操作。
usr

1
@usr您遇到了优化器无法解决的常见用例,但这不是唯一的用例。考虑以下情况:ROW_NUMBER()内的顺序是该列以及其他内容。或者当外部顺序通过对另一列进行二级排序时。或当您要降序订购时。或完全通过其他方式。我喜欢按表达式r而不是基列进行排序,只要是因为它与我在非嵌套查询中所做的匹配,并按表达式进行排序-我将使用分配给表达式的别名而不是重复表达式。
亚伦·伯特兰

4
@usr在Paul看来,在某些情况下,您可能会发现优化器的功能差距。如果它们不会被修复,并且您知道编写查询的更好方法,请使用更好的方法。病人:“医生,我做x会很痛。” 医生:“不要做x。” :-)
亚伦·伯特兰

-3

他们修改了查询优化器以添加此功能。这意味着他们实现了专门支持offset ... fetch命令的机制。换句话说,对于最热门的查询,SQL Server必须做更多的工作。因此查询计划的差异。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.