web-dev-qa-db-ja.com

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

SQL Server 2012で導入された新しいOFFSET ... FETCHモデルは、シンプルで高速なページングを提供します。 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

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

15
usr

質問の例では、同じ結果が得られません(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です。

Row Number Plan

OFFSETプランの推定コストは0.0196955です。

Offset Plan

つまり、0.000098推定コスト単位の節約になります(ただし、OFFSETプランでは、行番号を返す場合、追加の演算子が必要になります各行)。 OFFSETプランは、一般的に言えばまだ少し安くなりますが、推定コストはまさにそれであることに注意してください-実際のテストが依然として必要です。両方の計画のコストの大部分は、入力セットのすべての種類のコストであるため、有用なインデックスは両方のソリューションにメリットがあります。

定数リテラル値が使用されている場合(元の例ではOFFSET 30)、オプティマイザは完全な並べ替えの後にTopが続く代わりに、TopN Sortを使用できます。 TopNソートから必要な行が定数リテラルで<= 100(OFFSETFETCHの合計)の場合、実行エンジンは 別のソートアルゴリズム を使用できます。これは、一般的なTopNソートよりも高速に実行できます。 3つのケースはすべて、全体的にパフォーマンス特性が異なります。

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

  1. 既存のすべての用途に一致する変換を作成することはほとんど不可能です
  2. 一部のページングクエリが自動的に変換され、他のクエリは変換されない場合、混乱を招く可能性があります
  3. OFFSETプランがすべてのケースでより優れているとは限りません

上記の3番目のポイントの一例は、ページングセットが非常に広い場合です。 必要なキーを探す 非クラスター化インデックスを使用し、OFFSETまたはROW_NUMBERでインデックスをスキャンするよりも、クラスター化インデックスに対して手動でルックアップする方がはるかに効率的です。ページングアプリケーションが合計で行またはページの数を知る必要がある場合、 検討すべき追加の問題 があります。 「キーシーク」と「オフセット」メソッドの相対的なメリットについては、別のよい議論があります here

概して、十分なテストを行った後、ページングクエリを変更して、適切な場合はOFFSETを使用するように、情報に基づいた決定を下す方がおそらく良いでしょう。

13
Paul White 9

あなたのクエリを少しいじると、等しいコスト推定(50/50)と等しいIO stats:

; 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

これにより、object_idではなくrでソートすることにより、バージョンに表示される追加のソートが回避されます。

5