OFFSET…FETCHと古いスタイルのROW_NUMBERスキームとの間に実行計画の違いがあるのはなぜですか?


15

OFFSET ... FETCHSQL Server 2012で導入された新しいモデルは、シンプルで高速なページングを提供します。2つの形式が意味的に同一であり、非常に一般的であることを考慮すると、なぜまったく違いがあるのですか?

オプティマイザーが両方を認識し、それらを(簡単に)最大限に最適化すると仮定します。

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

CIを作成するobject_idかフィルターを追加することでこのテストケースを変更できますが、すべての計画の違いを削除することはできません。OFFSET ... FETCH実行時の作業量が少ないため、常に高速です。


よくわからないので、コメントとして付けますが、行の番号付けと最終結果セットの条件で同じ順序になっているためだと思います。2番目の条件では、オプティマイザーはこれを知っているため、結果を再度並べ替える必要はありません。ただし、前者の場合は、外側の選択からの結果と、内側の結果の行番号がソートされていることを確認する必要があります。#objects上の適切なインデックスを作成すると、問題を解決しなければならない
Akashさん

回答:


13

質問の例では、まったく同じ結果が得られません(OFFSET例には1つずれたエラーがあります)。以下の更新されたフォームはその問題を修正し、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元の例など)、オプティマイザーは、完全なソートの後にTopが続く代わりにTopN Sortを使用できます。TopNソートで必要な行が定数リテラルで<= 100(OFFSETおよびの合計FETCH)である場合、実行エンジンは一般的なTopNソートよりも高速に実行できる別のソートアルゴリズムを使用できます。3つのケースはすべて、全体的に異なるパフォーマンス特性を持っています。

オプティマイザーがROW_NUMBER使用するようOFFSETに構文パターンを自動的に変換しない理由については、いくつかの理由があります。

  1. 既存のすべての用途に一致する変換を記述することはほとんど不可能です
  2. 一部のページングクエリが自動的に変換され、他のクエリは自動的に変換されない場合がある
  3. OFFSET計画はすべてのケースで改善することを保証するものではありません

上記の3番目のポイントの1つの例は、ページングセットが非常に広い場合に発生します。またはでインデックスをスキャンするよりも、非クラスター化インデックスを使用して必要なキーを検索し、クラスター化インデックスを手動で検索する方がはるかに効率的です。ページングアプリケーションが合計で行またはページの数を知る必要があるかどうかを考慮する追加の問題があります。「キーシーク」および「オフセット」メソッドの相対的なメリットについては、別の良い説明があります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が出力と同じ列で順序付けられている場合、同じ順序保証されますよね?そのため、クエリオプティマイザーはその事実を利用できます。そのため、このクエリでは常に 2つのソート操作が不要です。
usr

1
@usrオプティマイザが考慮しない一般的なユースケースを見つけましたが、それが唯一のユースケースではありません。ROW_NUMBER()内の順序がその列と他の何かである場合を考えます。または、外側の順序が別の列で2次ソートを行う場合。または、降順で注文する場合。またはまったく別の方法で。rネストされていないクエリで行うことと一致するという理由だけで、ベース列ではなく式による順序付けと式による順序付けが好きです-式を繰り返す代わりに式に割り当てられたエイリアスを使用します。
アーロンバートランド

4
@usrそしてポールのポイントに、オプティマイザーの機能のギャップを見つけることができる場合があります。それらが修正されない場合で、クエリを記述するより良い方法を知っているなら、より良い方法を使用してください。患者:「医師、私がxをやると痛い」医師:「xをしないでください。」:-)
アーロンバートランド

-3

彼らはクエリオプティマイザーを変更してこの機能を追加しました。つまり、offset ... fetchコマンドをサポートするためのメカニズムを具体的に実装しました。言い換えると、上位クエリの場合、SQL Serverはさらに多くの作業を行う必要があります。したがって、クエリプランの違い。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.