web-dev-qa-db-ja.com

ROW_NUMBER()OVER(PARTITION BY B、A ORDER BY C)は(A、B、C)のインデックスを使用しません

次の2つの関数を検討してください。

_ROW_NUMBER() OVER (PARTITION BY A,B ORDER BY C)

ROW_NUMBER() OVER (PARTITION BY B,A ORDER BY C)
_

私が理解している限り、それらはまったく同じ結果を生成します。つまり、_PARTITION BY_句で列をリストする順序は重要ではありません。

_(A,B,C)_にインデックスがある場合、オプティマイザが両方のバリアントでこのインデックスを使用することを期待していました。

しかし、驚くべきことに、オプティマイザは2番目のバリアントで追加の明示的なソートを行うことにしました。

SQL Server 2008 StandardとSQL Server 2014 Expressで見ました。

これは私がそれを再現するために使用した完全なスクリプトです。

Microsoft SQL Server 2014で試した-12.0.2000.8(X64)Feb 20 2014 20:04:26 Copyright(c)Microsoft Corporation Express Edition(64-bit)on Windows NT 6.1(Build 7601:Service Pack 1)

およびMicrosoft SQL Server 2014(SP1-CU7)(KB3162659)-12.0.4459.0(X64)May 27 2016 15:33:17 Copyright(c)Microsoft Corporation Express Edition(64-bit)on Windows NT 6.1(Build 7601:Serviceパック1)

OPTION (QUERYTRACEON 9481)OPTION (QUERYTRACEON 2312)を使用して、新旧両方のカーディナリティエスティメータを使用します。

テーブル、インデックス、サンプルデータをセットアップします

_CREATE TABLE [dbo].[T](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [A] [int] NOT NULL,
    [B] [int] NOT NULL,
    [C] [int] NOT NULL,
    CONSTRAINT [PK_T] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, 
STATISTICS_NORECOMPUTE = OFF, 
IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS = ON, 
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

CREATE NONCLUSTERED INDEX [IX_ABC] ON [dbo].[T]
(
    [A] ASC,
    [B] ASC,
    [C] ASC
)WITH (PAD_INDEX = OFF, 
STATISTICS_NORECOMPUTE = OFF, 
SORT_IN_TEMPDB = OFF, 
DROP_EXISTING = OFF, 
ONLINE = OFF, 
ALLOW_ROW_LOCKS = ON, 
ALLOW_PAGE_LOCKS = ON)
GO

INSERT INTO [dbo].[T] ([A],[B],[C]) VALUES
(10, 20, 30),
(10, 21, 31),
(10, 21, 32),
(10, 21, 33),
(11, 20, 34),
(11, 21, 35),
(11, 21, 36),
(12, 20, 37),
(12, 21, 38),
(13, 21, 39);
_

クエリ

_SELECT -- AB
    ID,A,B,C
    ,ROW_NUMBER() OVER (PARTITION BY A,B ORDER BY C) AS rnAB
FROM T
ORDER BY C
OPTION(RECOMPILE);

SELECT -- BA
    ID,A,B,C
    ,ROW_NUMBER() OVER (PARTITION BY B,A ORDER BY C) AS rnBA
FROM T
ORDER BY C
OPTION(RECOMPILE);

SELECT -- both
    ID,A,B,C
    ,ROW_NUMBER() OVER (PARTITION BY A,B ORDER BY C) AS rnAB
    ,ROW_NUMBER() OVER (PARTITION BY B,A ORDER BY C) AS rnBA
FROM T
ORDER BY C
OPTION(RECOMPILE);
_

実行計画

PARTITION BY A、B

AB

PARTITION BY B、A

BA

両方

both

ご覧のとおり、2番目のプランには追加のソートがあります。 B、A、Cで注文します。オプティマイザーは、明らかに、_PARTITION BY B,A_が_PARTITION BY A,B_と同じであり、データを再ソートすることを認識できるほどスマートではありません。

興味深いことに、3番目のクエリには_ROW_NUMBER_の両方のバリアントがあり、余分なSortはありません!プランは最初のクエリと同じです。 (シーケンスプロジェクトの出力リストには、追加の列の追加の式がありますが、追加の並べ替えはありません)。したがって、このより複雑なケースでは、オプティマイザは_PARTITION BY B,A_が_PARTITION BY A,B_と同じであることを理解するのに十分賢く見えました。

最初と3番目のクエリでは、Index ScanオペレーターのプロパティにOrdered:Trueがあり、2番目のクエリではFalseです。

さらに興味深いのは、3番目のクエリを次のように書き直すと(2つの列を入れ替える)、

_SELECT -- both
    ID,A,B,C
    ,ROW_NUMBER() OVER (PARTITION BY B,A ORDER BY C) AS rnBA
    ,ROW_NUMBER() OVER (PARTITION BY A,B ORDER BY C) AS rnAB
FROM T
ORDER BY C
OPTION(RECOMPILE);
_

その後、追加のソートが再び表示されます!

誰かが光を当てることができますか?ここのオプティマイザで何が起こっているのですか?

12

あなたが開発者で内部を知らない限り、「オプティマイザで何が起こっているのか」という質問に対する明確な「答え」はないようです。

ここでコメントをまとめます。

全体として、クエリの最終結果が正しいため、バグと呼ぶのは厳しすぎるようです。場合によっては、実行計画が単に最適ではありません。 ypercubeᵀᴹMartin Smith および Aaron Bertrand これを「最適化の欠落」と呼びます。

  • GROUP BY a,bGROUP BY b,aは同じプランを生成するようですが、PARTITION BYは同じ変換を使用できません

  • 同じウィンドウ仕様のウィンドウ関数が、選択リストで仕様の異なるもので区切られている場合、追加のソート操作を実行できる他の欠落している最適化もあります。

  • ええ、これは最適化に失敗した別のようですが、それらはたくさんあります。オプティマイザは人間によって作成されており、完璧ではありません


Itzik Ben-Ganによるやや関連のある記事 降順のインデックス。インデックスの順序付け、並列処理、ランキング計算 があります。そこでは、Itzikが降順のインデックスについて説明し、インデックス定義の方向がパーティションを持つウィンドウ関数にどのように影響するかの例も示します。彼は、オプティマイザが回避できたはずの追加のソート演算子があるROW_NUMBERを使用したクエリと生成されたプランの例を示しています。


私にとっての実際的な結果は、このオプティマイザの特殊性を覚えておくことです。ウィンドウ関数でPARTITION BYを使用する場合は、常にPARTITION BYに列をリストする順序と、インデックスにリストされる順序を一致させるようにしてください。それは問題ではありませんが。

この予防策のもう1つの側面は、インデックスを確認し、インデックス定義内のいくつかの列を入れ替えることにした場合です。影響を受けていないように見える既存のクエリに誤って影響を与える可能性があることに注意してください。これが実際に私がオプティマイザのこの特異性に気付いた方法です。

そうしないと、オプティマイザがインデックスを最大限に活用できなくなる可能性があります。オプティマイザが最適なプランを選択した場合でも、そのようなプランは、SELECTステートメントの列の順序を変更するなど、クエリにわずかな無害な変更を加えて最適性が低下する可能性があります。

2