ROW_NUMBERウィンドウ関数には、明らかなランキングのニーズをはるかに超えた、数多くの実用的なアプリケーションがあります。ほとんどの場合、行番号を計算するときは、ある順序に基づいてそれらを計算する必要があり、関数のウィンドウ順序句で目的の順序指定を指定します。ただし、特定の順序で行番号を計算する必要がある場合があります。言い換えれば、非決定論的な順序に基づいています。これは、クエリ結果全体、またはパーティション内で発生する可能性があります。たとえば、結果の行に一意の値を割り当てる、データを重複排除する、グループごとに任意の行を返すなどがあります。
非決定論的な順序に基づいて行番号を割り当てる必要があることは、ランダムな順序に基づいて行番号を割り当てる必要があることとは異なることに注意してください。前者の場合、割り当てられる順序や、クエリを繰り返し実行しても同じ行に同じ行番号が割り当てられるかどうかは関係ありません。後者の場合、繰り返し実行すると、どの行にどの行番号が割り当てられるかが変わり続けることが予想されます。この記事では、非決定的な順序で行番号を計算するためのさまざまな手法について説明します。信頼性が高く、最適な手法を見つけることが望まれます。
定数畳み込みに関するヒント、実行時の定数テクニック、そして常に優れた情報源であるPaulWhiteに特に感謝します。
注文が重要な場合
行番号の順序が重要な場合から始めます。
例では、T1というテーブルを使用します。次のコードを使用してこのテーブルを作成し、サンプルデータを入力します。
SET NOCOUNT ON; USE tempdb; DROP TABLE IF EXISTS dbo.T1; GO CREATE TABLE dbo.T1 ( id INT NOT NULL CONSTRAINT PK_T1 PRIMARY KEY, grp VARCHAR(10) NOT NULL, datacol INT NOT NULL ); INSERT INTO dbo.T1(id, grp, datacol) VALUES (11, 'A', 50), ( 3, 'B', 20), ( 5, 'A', 40), ( 7, 'B', 10), ( 2, 'A', 50);
次のクエリを検討してください(これをクエリ1と呼びます):
SELECT id, grp, datacol, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY datacol) AS n FROM dbo.T1;
ここでは、列データ列の順に、列grpで識別される各グループ内で行番号を割り当てる必要があります。システムでこのクエリを実行すると、次の出力が得られました。
id grp datacol n --- ---- -------- --- 5 A 40 1 2 A 50 2 11 A 50 3 7 B 10 1 3 B 20 2
ここでは、行番号が部分的に決定論的および部分的に非決定論的な順序で割り当てられています。これが意味するのは、同じパーティション内で、datacol値が大きい行ほど、行番号の値が大きくなるという保証があるということです。ただし、datacolはgrpパーティション内で一意ではないため、同じgrpおよびdatacol値を持つ行間での行番号の割り当ての順序は非決定的です。これは、ID値が2と11の行の場合です。どちらもgrp値Aとdatacol値50を持っています。システムでこのクエリを初めて実行したとき、ID2の行は行番号2を取得しました。 ID 11の行は行番号3を取得しました。これが、SQLServerで実際に発生する可能性を気にしないでください。クエリを再度実行すると、理論的には、ID 2の行に行番号3を割り当て、ID11の行に行番号2を割り当てることができます。
完全に決定論的な順序に基づいて行番号を割り当てる必要がある場合は、基になるデータが変更されない限り、クエリの実行全体で繰り返し可能な結果を保証するために、ウィンドウのパーティション分割と順序付け句の要素の組み合わせが一意である必要があります。この場合、タイブレーカーとして列IDをウィンドウ順序句に追加することでこれを実現できます。その場合、OVER句は次のようになります。
OVER (PARTITION BY grp ORDER BY datacol, id)
いずれにせよ、クエリ1のように意味のある順序付け仕様に基づいて行番号を計算する場合、SQL Serverは、ウィンドウのパーティション分割と順序付け要素の組み合わせによって順序付けられた行を処理する必要があります。これは、インデックスから事前に並べ替えられたデータをプルするか、データを並べ替えることによって実現できます。現時点では、クエリ1のROW_NUMBER計算をサポートするインデックスがT1にないため、SQLServerはデータの並べ替えを選択する必要があります。これは、図1に示すクエリ1の計画で確認できます。
図1:サポートインデックスなしのクエリ1の計画
プランは、Ordered:Falseプロパティを使用してクラスター化インデックスからデータをスキャンすることに注意してください。これは、スキャンがインデックスキー順に並べられた行を返す必要がないことを意味します。クラスター化インデックスがここで使用されているのは、キーの順序ではなく、クエリをカバーしているからです。次に、計画はソートを適用し、その結果、追加のコスト、N Log Nスケーリング、および応答時間の遅延が発生します。セグメント演算子は、行がパーティションの最初であるかどうかを示すフラグを生成します。最後に、Sequence Projectオペレーターは、各パーティションに1から始まる行番号を割り当てます。
ソートの必要性を回避したい場合は、パーティション化および順序付け要素に基づくキー・リストと、カバーリング要素に基づくインクルード・リストを使用して、カバーリング・インデックスを作成できます。このインデックスをPOCインデックス(パーティション化用)と考えるのが好きです。 、注文 およびカバー )。クエリをサポートするPOCの定義は次のとおりです。
CREATE INDEX idx_grp_data_i_id ON dbo.T1(grp, datacol) INCLUDE(id);
クエリ1を再度実行します:
SELECT id, grp, datacol, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY datacol) AS n FROM dbo.T1;
この実行の計画を図2に示します。
図2:POCインデックスを使用したクエリ1の計画
今回は、プランがOrdered:Trueプロパティを使用してPOCインデックスをスキャンすることに注意してください。これは、スキャンによって行がインデックスキーの順序で返されることが保証されることを意味します。ウィンドウ関数が必要とするように、データはインデックスから事前に並べ替えられてプルされるため、明示的な並べ替えは必要ありません。この計画のスケーリングは線形であり、応答時間は良好です。
順序が重要でない場合
完全に非決定的な順序で行番号を割り当てる必要がある場合は、少し注意が必要です。このような場合に実行したいのは、ウィンドウの順序句を指定せずにROW_NUMBER関数を使用することです。まず、SQL標準でこれが許可されているかどうかを確認しましょう。ウィンドウ関数の構文規則を定義する標準の関連部分は次のとおりです。
構文規則…
5)WNSを<ウィンドウ名または仕様>とします。 WDXを、WNSによって定義されたウィンドウを記述するウィンドウ構造記述子とします。
6)
a)
…
f)ROW_NUMBER()OVER WNSは、
…
項目6には、関数
それでは、試してみて、SQLServerでウィンドウの順序を指定せずに行番号を計算してみましょう。
SELECT id, grp, datacol, ROW_NUMBER() OVER() AS n FROM dbo.T1;
この試行により、次のエラーが発生します:
メッセージ4112、レベル15、状態1、53行目関数'ROW_NUMBER'には、ORDERBYを含むOVER句が必要です。
実際、SQL ServerのROW_NUMBER関数のドキュメントを確認すると、次のテキストが見つかります。
「order_by_clauseORDER BY句は、指定されたパーティション内で行に一意のROW_NUMBERが割り当てられる順序を決定します。必須です。」
したがって、SQLServerのROW_NUMBER関数にはウィンドウ順序句が必須であるようです。ちなみに、これはOracleにも当てはまります。
この要件の背後にある理由を理解しているかどうかはわかりません。クエリ1のように、部分的に非決定的な順序に基づいて行番号を定義できることを忘れないでください。では、非決定性をずっと許可しないのはなぜですか。おそらく、私が考えていない理由がいくつかあります。そのような理由が考えられる場合は、共有してください。
いずれにせよ、ウィンドウの順序句が必須であることを考えると、順序を気にしない場合は、任意の順序を指定できると主張することができます。このアプローチの問題は、クエリされたテーブルからいくつかの列で注文した場合、不必要なパフォーマンスの低下を伴う可能性があることです。サポートするインデックスがない場合は、明示的な並べ替えの料金を支払います。サポートするインデックスが設定されている場合、ストレージエンジンをインデックス順序スキャン戦略に制限します(インデックスリンクリストに従います)。インデックス順序スキャンと割り当て順序スキャン(IAMページに基づく)のどちらかを選択する際に順序が重要ではない場合に通常あるように、柔軟性を高めることはできません。
試す価値のあるアイデアの1つは、ウィンドウの順序句に1などの定数を指定することです。サポートされている場合は、オプティマイザーがすべての行の値が同じであることを認識できるほど賢いことを期待します。したがって、実際の順序の関連性はなく、したがって、並べ替えやインデックスの順序スキャンを強制する必要はありません。このアプローチを試みるクエリは次のとおりです。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY 1) AS n FROM dbo.T1;
残念ながら、SQLServerはこのソリューションをサポートしていません。次のエラーが発生します:
メッセージ5308、レベル16、状態1、行56ウィンドウ関数、集計、およびNEXT VALUE FOR関数は、ORDERBY句式として整数インデックスをサポートしていません。
明らかに、SQL Serverは、ウィンドウの順序句で整数定数を使用している場合、プレゼンテーションのORDER BY句で整数を指定する場合のように、SELECTリスト内の要素の序数位置を表すと想定しています。その場合、試してみる価値のある別のオプションは、次のように非整数定数を指定することです。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY 'No Order') AS n FROM dbo.T1;
このソリューションもサポートされていないことがわかりました。 SQLServerは次のエラーを生成します。
メッセージ5309、レベル16、状態1、行65ウィンドウ関数、集計、およびNEXT VALUE FOR関数は、ORDERBY句の式として定数をサポートしていません。
どうやら、ウィンドウ順序句はいかなる種類の定数もサポートしていません。
これまで、SQLServerでのROW_NUMBER関数のウィンドウ順序の関連性について次のことを学びました。
- ORDERBYが必要です。
- SQL Serverは、SELECTで序数位置を指定しようとしていると見なすため、整数定数で並べ替えることはできません。
- どのような種類の定数でも注文できません。
結論として、定数ではない式で並べ替える必要があります。明らかに、クエリされたテーブルから列リストで並べ替えることができます。しかし、私たちは、オプティマイザーが順序の関連性がないことを認識できる効率的なソリューションを見つけることを目指しています。
定数畳み込み
これまでの結論は、ROW_NUMBERのウィンドウ順序句では定数を使用できないということですが、次のクエリのように、定数に基づく式についてはどうでしょうか。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY 1+0) AS n FROM dbo.T1;
ただし、この試みは定数畳み込みと呼ばれるプロセスの犠牲になります。これは通常、クエリのパフォーマンスにプラスの影響を及ぼします。この手法の背後にある考え方は、クエリ処理の初期段階で、定数に基づく一部の式を結果定数にフォールドすることにより、クエリのパフォーマンスを向上させることです。常に折りたたむことができる式の種類の詳細については、こちらをご覧ください。式1+0は1に折りたたまれ、定数1を直接指定した場合とまったく同じエラーが発生します。
メッセージ5308、レベル16、状態1、79行目ウィンドウ関数、集計、およびNEXT VALUE FOR関数は、ORDERBY句式として整数インデックスをサポートしていません。
次のように、2つの文字列リテラルを連結しようとすると、同様の状況に直面します。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY 'No' + ' Order') AS n FROM dbo.T1;
リテラル「NoOrder」を直接指定した場合と同じエラーが発生します:
メッセージ5309、レベル16、状態1、行55ウィンドウ関数、集計、およびNEXT VALUE FOR関数は、ORDERBY句の式として定数をサポートしていません。
Bizarro world –エラーを防ぐエラー
人生は驚きに満ちています…
定数畳み込みを妨げる1つのことは、式が通常エラーになる場合です。たとえば、式2147483646 + 1は、有効なINT型の値になるため、一定に折りたたむことができます。したがって、次のクエリを実行しようとすると失敗します。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY 2147483646+1) AS n FROM dbo.T1;メッセージ5308、レベル16、状態1、行109
ウィンドウ関数、集計、およびNEXT VALUE FOR関数は、ORDERBY句式として整数インデックスをサポートしていません。
ただし、式2147483647 + 1は、INTオーバーフローエラーが発生する可能性があるため、常に折りたたむことはできません。注文への影響は非常に興味深いものです。次のクエリを試してください(これをクエリ2と呼びます):
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY 2147483647+1) AS n FROM dbo.T1;
奇妙なことに、このクエリは正常に実行されます。何が起こるかというと、一方でSQL Serverは定数畳み込みを適用できないため、順序は単一の定数ではない式に基づいています。一方、オプティマイザは、順序付けの値がすべての行で同じであると判断するため、順序付け式を完全に無視します。これは、図3に示すように、このクエリの計画を調べるときに確認されます。
図3:クエリ2の計画
プランがOrdered:Falseプロパティを使用してカバーするインデックスをスキャンすることを確認します。これはまさに私たちのパフォーマンス目標でした。
同様に、次のクエリは定数畳み込みの試行が成功するため、失敗します。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY 1/1) AS n FROM dbo.T1;メッセージ5308、レベル16、状態1、行123
ウィンドウ関数、集計、およびNEXT VALUE FOR関数は、ORDERBY句式として整数インデックスをサポートしていません。
次のクエリには、失敗した定数畳み込みの試行が含まれているため、成功し、図3で前述したプランが生成されます。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY 1/0) AS n FROM dbo.T1;
次のクエリは、定数畳み込みの試行が成功することを含み(VARCHARリテラル「1」は暗黙的にINT 1に変換され、次に1 + 1は2に折り畳まれます)、したがって失敗します。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY 1+'1') AS n FROM dbo.T1;メッセージ5308、レベル16、状態1、行134
ウィンドウ関数、集計、およびNEXT VALUE FOR関数は、ORDERBY句式として整数インデックスをサポートしていません。
次のクエリは、失敗した定数畳み込みの試行を含み('A'をINTに変換できません)、したがって成功し、図3で前に示した計画を生成します。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY 1+'A') AS n FROM dbo.T1;
正直なところ、この奇妙なテクニックは当初のパフォーマンス目標を達成しましたが、安全だとは言えないため、信頼するのはそれほど快適ではありません。
関数に基づく実行時定数
非決定論的な順序で行番号を計算するための優れたソリューションの検索を続けると、最後の風変わりなソリューションよりも安全と思われるいくつかの手法があります。関数に基づくランタイム定数の使用、定数に基づくサブクエリの使用、に基づくエイリアス列の使用定数と変数を使用します。
T-SQLのバグ、落とし穴、およびベストプラクティス(決定論)で説明しているように、T-SQLのほとんどの関数は、行ごとではなく、クエリ内の参照ごとに1回だけ評価されます。これは、GETDATEやRANDなどのほとんどの非決定論的関数にも当てはまります。このルールには、行ごとに1回評価される関数NEWIDやCRYPT_GEN_RANDOMなどの例外はほとんどありません。 GETDATE、@@ SPIDなどのほとんどの関数は、クエリの開始時に1回評価され、それらの値は実行時定数と見なされます。このような関数への参照は、常に折りたたまれることはありません。これらの特性により、関数に基づく実行時定数がウィンドウ順序要素として適切に選択されます。実際、T-SQLはそれをサポートしているようです。同時に、オプティマイザーは、実際には順序の関連性がないことを認識し、不必要なパフォーマンスのペナルティを回避します。
GETDATE関数を使用した例を次に示します。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY GETDATE()) AS n FROM dbo.T1;
このクエリは、前に図3に示したものと同じ計画を取得します。
@@ SPID関数(現在のセッションIDを返す)を使用した別の例を次に示します。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY @@SPID) AS n FROM dbo.T1;
関数PIはどうですか?次のクエリを試してください:
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY PI()) AS n FROM dbo.T1;
これは次のエラーで失敗します:
メッセージ5309、レベル16、状態1、行153ウィンドウ関数、集計、およびNEXT VALUE FOR関数は、ORDERBY句の式として定数をサポートしていません。
GETDATEや@@SPIDなどの関数は、プランの実行ごとに1回再評価されるため、常に折りたたまれることはありません。 PIは常に同じ定数を表すため、定数は折りたたまれます。
前述のように、NEWIDやCRYPT_GEN_RANDOMなど、行ごとに1回評価される関数はほとんどありません。これにより、ランダムな順序と混同しないように、非決定的な順序が必要な場合は、ウィンドウの順序要素として不適切な選択になります。なぜ不必要なソートペナルティを支払うのですか?
NEWID関数を使用した例を次に示します。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY NEWID()) AS n FROM dbo.T1;
このクエリの計画を図4に示します。これは、SQLServerが関数の結果に基づいて明示的な並べ替えを追加したことを確認しています。
図4:クエリ3の計画
行番号をランダムな順序で割り当てたい場合は、必ず、それが使用したい手法です。ソートコストが発生することに注意する必要があります。
サブクエリの使用
ウィンドウの順序付け式として定数に基づくサブクエリを使用することもできます(例:ORDER BY(SELECT'No Order'))。また、このソリューションでは、SQL Serverのオプティマイザーは順序の関連性がないことを認識しているため、不要な並べ替えを課したり、ストレージエンジンの選択を順序を保証する必要のあるものに制限したりすることはありません。例として次のクエリを実行してみてください:
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY (SELECT 'No Order')) AS n FROM dbo.T1;
図3で前に示したのと同じ計画が得られます。
このテクニックの大きな利点の1つは、独自のタッチを追加できることです。たぶんあなたは本当にNULLが好きです:
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM dbo.T1;
たぶんあなたは特定の数が本当に好きです:
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY (SELECT 42)) AS n FROM dbo.T1;
誰かにメッセージを送りたいかもしれません:
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY (SELECT 'Lilach, will you marry me?')) AS n FROM dbo.T1;
ポイントがわかります。
実行可能ですが、厄介です
うまくいくテクニックがいくつかありますが、少し厄介です。 1つは、定数に基づいて式の列エイリアスを定義し、その列エイリアスをウィンドウの順序付け要素として使用することです。これは、テーブル式を使用するか、CROSSAPPLY演算子とテーブル値コンストラクターを使用して行うことができます。後者の例を次に示します。
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY [I'm a bit ugly]) AS n FROM dbo.T1 CROSS APPLY ( VALUES('No Order') ) AS A([I'm a bit ugly]);
図3で前に示したのと同じ計画が得られます。
もう1つのオプションは、ウィンドウの順序付け要素として変数を使用することです。
DECLARE @ImABitUglyToo AS INT = NULL; SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY @ImABitUglyToo) AS n FROM dbo.T1;
このクエリは、図3で前に示した計画も取得します。
自分のUDFを使用するとどうなりますか?
定数を返す独自のUDFを使用することは、非決定的な順序が必要な場合のウィンドウ順序要素として適切な選択であると思われるかもしれませんが、そうではありません。例として、次のUDF定義を検討してください。
DROP FUNCTION IF EXISTS dbo.YouWillRegretThis; GO CREATE FUNCTION dbo.YouWillRegretThis() RETURNS INT AS BEGIN RETURN NULL END; GO
次のように、UDFをウィンドウ順序句として使用してみてください(これをクエリ4と呼びます):
SELECT id, grp, datacol, ROW_NUMBER() OVER(ORDER BY dbo.YouWillRegretThis()) AS n FROM dbo.T1;
SQL Server 2019(または並列互換性レベル<150)より前では、ユーザー定義関数は行ごとに評価されます。定数を返しても、インライン化されません。したがって、一方ではウィンドウ順序付け要素としてそのようなUDFを使用できますが、他方ではこれによりソートペナルティが発生します。これは、図5に示すように、このクエリの計画を調べることで確認できます。
図5:クエリ4の計画
SQL Server 2019以降、互換性レベル> =150では、このようなユーザー定義関数はインライン化されます。これはほとんどの場合素晴らしいことですが、この場合はエラーが発生します:
メッセージ5309、レベル16、状態1、行217ウィンドウ関数、集計、およびNEXT VALUE FOR関数は、ORDERBY句の式として定数をサポートしていません。
したがって、ウィンドウの順序付け要素として定数に基づくUDFを使用すると、使用しているSQL Serverのバージョンとデータベースの互換性レベルに応じて、並べ替えまたはエラーが強制されます。要するに、これをしないでください。
非決定的な順序で分割された行番号
非決定論的な順序に基づくパーティション化された行番号の一般的な使用例は、グループごとに任意の行を返すことです。定義上、このシナリオにはパーティション化要素が存在することを考えると、このような場合の安全な手法は、ウィンドウ分割要素をウィンドウ順序付け要素としても使用することであると考えるでしょう。最初のステップとして、次のように行番号を計算します。
SELECT id, grp, datacol, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY grp) AS n FROM dbo.T1;
このクエリの計画を図6に示します。
図6:クエリ5の計画
サポートするインデックスがOrdered:Trueプロパティでスキャンされる理由は、SQLServerが各パーティションの行を単一のユニットとして処理する必要があるためです。これは、フィルタリング前の場合です。パーティションごとに1つの行のみをフィルタリングする場合、オプションとして順序ベースとハッシュベースの両方のアルゴリズムがあります。
2番目のステップは、行番号の計算を含むクエリをテーブル式に配置し、外側のクエリで、次のように各パーティションの行番号1の行をフィルター処理することです。
WITH C AS ( SELECT id, grp, datacol, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY grp) AS n FROM dbo.T1 ) SELECT id, grp, datacol FROM C WHERE n = 1;
理論的にはこの手法は安全であると考えられていますが、Paul whiteは、このメソッドを使用すると、パーティションごとに返される結果行のさまざまなソース行から属性を取得できることを示すバグを発見しました。関数に基づくランタイム定数または定数に基づくサブクエリを順序付け要素として使用することは、このシナリオでも安全であると思われるため、代わりに次のようなソリューションを使用するようにしてください。
WITH C AS ( SELECT id, grp, datacol, ROW_NUMBER() OVER(PARTITION BY grp ORDER BY (SELECT 'No Order')) AS n FROM dbo.T1 ) SELECT id, grp, datacol FROM C WHERE n = 1;
私の許可なしにこの道を通過することはできません
非決定論的な順序に基づいて行番号を計算しようとすることは、一般的なニーズです。 T-SQLが単にROW_NUMBER関数のウィンドウ順序句をオプションにしたとしたら良かったのですが、そうではありません。そうでない場合は、少なくとも順序付け要素として定数を使用できるようにしておけばよかったのですが、それもサポートされているオプションではありません。しかし、うまく質問すれば、定数に基づくサブクエリまたは関数に基づくランタイム定数の形式で、SQLServerはそれを許可します。これらは私が最も快適な2つのオプションです。うまくいくように見える風変わりな誤った表現にはあまり満足していないので、このオプションはお勧めできません。