結合の代わりに集計による「ToTime」の検索
1 回の論理読み取りでテーブルを 1 回スキャンするだけの非常にワイルドなクエリを共有したいと思います。比較すると、このページの他の最良の回答である Simon Kingston のクエリは、2 回のスキャンが必要です。
非常に大きなデータ セット (17,408 の入力行、8,193 の結果行を生成) では、CPU 574 と 2645 の時間がかかりますが、Simon Kingston のクエリは CPU 63,820 と 37,108 の時間がかかります。
インデックスを使用すると、ページ上の他のクエリのパフォーマンスが何倍も向上する可能性がありますが、クエリを書き直すだけで CPU が 111 倍、速度が 14 倍向上することは興味深いことです。
(注意してください: サイモン キングストンや他の誰かを軽視しているわけではありません。このクエリのアイデアが非常にうまく機能することに単純に興奮しています。彼のクエリは、パフォーマンスが十分であり、実際に理解可能で保守しやすいため、私のクエリよりも優れています。 、私とは異なります。)
これが不可能なクエリです。わかりにくいです。書きづらかったです。しかし、それは素晴らしいです。:)
WITH Ranks AS (
SELECT
T = Dense_Rank() OVER (ORDER BY Time, Num),
N = Dense_Rank() OVER (PARTITION BY Name ORDER BY Time, Num),
*
FROM
#Data D
CROSS JOIN (
VALUES (1), (2)
) X (Num)
), Items AS (
SELECT
FromTime = Min(Time),
ToTime = Max(Time),
Name = IsNull(Min(CASE WHEN Num = 2 THEN Name END), Min(Name)),
I = IsNull(Min(CASE WHEN Num = 2 THEN T - N END), Min(T - N)),
MinNum = Min(Num)
FROM
Ranks
GROUP BY
T / 2
)
SELECT
FromTime = Min(FromTime),
ToTime = CASE WHEN MinNum = 2 THEN NULL ELSE Max(ToTime) END,
Name
FROM Items
GROUP BY
I, Name, MinNum
ORDER BY
FromTime
注: これには、SQL 2008 以降が必要です。SQL 2005 で機能させるには、VALUES 句を に変更しますSELECT 1 UNION ALL SELECT 2
。
更新されたクエリ
これについて少し考えた後、2 つの別々の論理タスクを同時に実行していることに気付きました。これにより、クエリが不必要に複雑になりました。新しいタスク) および 2) 次の行から「ToTime」値を取得します。#1を #2 の前に実行することにより、クエリはより単純になり、約半分の CPU で実行されます!
したがって、ここでは単純化されたクエリを示します。最初に重要でない行を削除し、次にJOIN ではなく集計を使用して ToTime 値を取得します。はい、2 つではなく 3 つのウィンドウ関数がありますが、最終的には行が少ないため (重要でない行を削除した後)、実行する作業が少なくなります。
WITH Ranks AS (
SELECT
Grp =
Row_Number() OVER (ORDER BY Time)
- Row_Number() OVER (PARTITION BY Name ORDER BY Time),
[Time], Name
FROM #Data D
), Ranges AS (
SELECT
Result = Row_Number() OVER (ORDER BY Min(R.[Time]), X.Num) / 2,
[Time] = Min(R.[Time]),
R.Name, X.Num
FROM
Ranks R
CROSS JOIN (VALUES (1), (2)) X (Num)
GROUP BY
R.Name, R.Grp, X.Num
)
SELECT
FromTime = Min([Time]),
ToTime = CASE WHEN Count(*) = 1 THEN NULL ELSE Max([Time]) END,
Name = IsNull(Min(CASE WHEN Num = 2 THEN Name ELSE NULL END), Min(Name))
FROM Ranges R
WHERE Result > 0
GROUP BY Result
ORDER BY FromTime;
この更新されたクエリには、説明で示したのと同じ問題がすべて含まれていますが、余分な不要な行を処理していないため、解決が容易です。また、除外しなければならなかった 0の値もわかりRow_Number() / 2
ます。前のクエリから除外しなかった理由がわかりませんが、いずれにせよ、これは完全に機能し、驚くほど高速です。
外側の適用で整理整頓
最後に、Simon Kingston のクエリと基本的に同じバージョンを示します。これは構文を理解しやすいと思います。
SELECT
FromTime = Min(D.Time),
X.ToTime,
D.Name
FROM
#Data D
OUTER APPLY (
SELECT TOP 1 ToTime = D2.[Time]
FROM #Data D2
WHERE
D.[Time] < D2.[Time]
AND D.[Name] <> D2.[Name]
ORDER BY D2.[Time]
) X
GROUP BY
X.ToTime,
D.Name
ORDER BY
FromTime;
より大きなデータ セットでパフォーマンスを比較する場合のセットアップ スクリプトは次のとおりです。
CREATE TABLE #Data (
RecordId int,
[Time] int,
Name varchar(10)
);
INSERT #Data VALUES
(1, 10, 'Running'),
(2, 18, 'Running'),
(3, 21, 'Running'),
(4, 29, 'Walking'),
(5, 33, 'Walking'),
(6, 57, 'Running'),
(7, 66, 'Running'),
(8, 77, 'Running'),
(9, 81, 'Walking'),
(10, 89, 'Running'),
(11, 93, 'Walking'),
(12, 99, 'Running'),
(13, 107, 'Running'),
(14, 113, 'Walking'),
(15, 124, 'Walking'),
(16, 155, 'Walking'),
(17, 178, 'Running');
GO
insert #data select recordid + (select max(recordid) from #data), time + (select max(time) +25 from #data), name from #data
GO 10
説明
これが私のクエリの背後にある基本的な考え方です。
切り替えを表す時間は、隣接する 2 つの行に表示する必要があります。1 つは前のアクティビティを終了する時間で、もう 1 つは次のアクティビティを開始する時間です。これに対する自然な解決策は、出力行がそれ自体の行 (開始時間) と次に変更された行 (終了時間)からプルできるように結合することです。
ただし、私のクエリでは、行をCROSS JOIN (VALUES (1), (2))
. これで、すべての行が複製されました。JOIN を使用して列全体で計算を行う代わりに、何らかの形式の集計を使用して、目的の行の各ペアを 1 つに折りたたむという考え方です。
次のタスクは、重複する各行を適切に分割して、1 つのインスタンスが前のペアに対応し、別のインスタンスが次のペアに対応するようにすることです。これは、T 列を でROW_NUMBER()
並べTime
替え、2 で割ることで実現されます (ただし、この場合は ROW_NUMBER と同じ値を返すため、対称性のために DENSE_RANK() を実行するように変更しました)。効率化のために、次のステップで除算を実行して、行番号を別の計算で再利用できるようにしました (読み続けてください)。行番号は 1 から始まり、暗黙のうちに 2 で除算すると int に変換さ0 1 1 2 2 3 3 4 4 ...
れるため、この計算値でグループ化することにより、目的の結果を持つシーケンスを生成する効果があります。Num
行番号では、最初のセットの後のすべてのセットが「前の」行の Num = 2 と「次の」行の Num = 1 で構成されることがわかりました。
次の難しい作業は、気にしない行を削除し、ブロックの開始時刻をブロックの終了時刻と同じ行にまとめる方法を見つけることです。必要なのは、ランニングまたはウォーキングの個別のセットごとに独自の番号を付けて、グループ化できるようにする方法です。DENSE_RANK()
は自然な解決策ですが、問題はORDER BY
節の各値に注意を払うことです。が変更されるたびに計算が変更されないDENSE_RANK() OVER (PREORDER BY Time ORDER BY Name)
ようにするための構文がありません。少し考えた後、 Itzik Ben-Gan のグループ化された島のソリューション の背後にあるロジックを少し理解できることに気付きました。Time
RANK
Name
Time
Name
で並べTime
替えた場合、同じグループ内の各行で同じであるが、他のグループとは異なる値が得られます。4 5 6
一般的なグループ化された島の手法では、となどの行とロックステップで上昇する 2 つの計算値を作成します1 2 3
。この値を減算すると同じ値が得られます (この例では、、 、3 3 3
の結果)。注:最初は計算のために始めましたが、うまくいきませんでした。正解は、申し訳ありませんが、その時点でなぜこれを結論付けたのか覚えていないため、もう一度掘り下げて理解する必要があるというものでした。でも、やっぱりそういうこと4 - 1
5 - 2
6 - 3
ROW_NUMBER()
N
DENSE_RANK()
T-N
計算: 1 つのステータス (ランニングまたはウォーキング) の各「島」を分離するためにグループ化できる数値。
でもこれで終わりではありません。まず、各グループの「次の」行にはName
、 、N
、およびの誤った値が含まれていますT
。これを回避するには、各グループから、Num = 2
行が存在する場合は行の値を選択します (存在しない場合は、残りの値を使用します)。これにより、次のような式が得られます。これによりCASE WHEN NUM = 2 THEN x END
、不適切な「次の」行の値が適切に取り除かれます。
T - N
いくつかの実験の後、ウォーキンググループとランニンググループの両方が同じ計算値を持つ可能性があるため、単独でグループ化するだけでは不十分であることに気付きました(最大 17 まで提供されたサンプルデータの場合、2 つのT - N
値があります)。 6)。ただし、単純にグループ化するだけで、Name
この問題は解決します。「ランニング」または「ウォーキング」のいずれのグループにも、反対のタイプから同じ数の介在値が含まれることはありません。つまり、最初のグループは「Running」で始まり、次の「Running」グループの前に 2 つの「Walking」行があるため、N の値はT
次の「Running」グループの値よりも 2 少なくなります。 . これについて考える 1 つの方法は、T - N
計算は、現在の行の前に同じ値 "Running" または "Walking" に属さない行の数をカウントします。これが正しいことを示すいくつかの考えがあります: 3 番目の "Running" グループに移ると、それらを分離する "Walking" グループがあるため、それは 3 番目のグループにすぎないため、間に入る行の数が異なります。その前であり、より高い位置から開始するため、値が複製できないほど十分に高いです。
最後に、最終的なグループは 1 行だけで構成されているため (終了時刻がなく、NULL
代わりに を表示する必要があります)、終了時刻があるかどうかを判断するために使用できる計算を投入する必要がありました。これはMin(Num)
式で達成され、最後に Min(Num) が 2 の場合 (「次の」行がないことを意味します) を検出しNULL
、値の代わりにa を表示しMax(ToTime)
ます。
この説明が人々の役に立つことを願っています。私の「行乗算」手法が一般的に有用であり、実稼働環境のほとんどの SQL クエリ作成者に適用できるかどうかはわかりません。これは、それを理解するのが難しく、メンテナンスが困難であるためです。コード (反応はおそらく「一体何をしているの!?!」の後に「書き直す時間だ!」というものです)。
あなたがここまでたどり着いたのなら、あなたの時間と、信じられないほど楽しい SQL パズルの世界への私の小さな遠足に私を甘やかしてくれてありがとう。
自分の目で確かめてください
別名「PREORDER BY」をシミュレートします。
最後のメモ。ジョブがどのように機能するかを確認するT - N
には (私の方法のこの部分を使用すると、SQL コミュニティには一般的に適用できない可能性があることに注意してください)、サンプル データの最初の 17 行に対して次のクエリを実行します。
WITH Ranks AS (
SELECT
T = Dense_Rank() OVER (ORDER BY Time),
N = Dense_Rank() OVER (PARTITION BY Name ORDER BY Time),
*
FROM
#Data D
)
SELECT
*,
T - N
FROM Ranks
ORDER BY
[Time];
これにより、次の結果が得られます。
RecordId Time Name T N T - N
----------- ---- ---------- ---- ---- -----
1 10 Running 1 1 0
2 18 Running 2 2 0
3 21 Running 3 3 0
4 29 Walking 4 1 3
5 33 Walking 5 2 3
6 57 Running 6 4 2
7 66 Running 7 5 2
8 77 Running 8 6 2
9 81 Walking 9 3 6
10 89 Running 10 7 3
11 93 Walking 11 4 7
12 99 Running 12 8 4
13 107 Running 13 9 4
14 113 Walking 14 5 9
15 124 Walking 15 6 9
16 155 Walking 16 7 9
17 178 Running 17 10 7
重要な部分は、「ウォーキング」または「ランニング」の各グループが同じ値をT - N
持ち、同じ名前を持つ他のグループとは区別されるということです。
パフォーマンス
私のクエリが他の人のクエリよりも高速であるという点について、詳しく説明したくありません。ただし、(インデックスがない場合) 違いがどれほど大きいかを考えると、数値を表形式で表示したかったのです。これは、この種の行間の相関の高いパフォーマンスが必要な場合に適した手法です。
各クエリを実行する前に、DBCC FREEPROCCACHE; DBCC DROPCLEANBUFFERS;
. クエリごとに MAXDOP を 1 に設定して、並列処理による時間の崩壊の影響を取り除きます。クライアントのデータ転送ではなくパフォーマンスのみを測定するために、結果セットをクライアントに返すのではなく、変数に選択しました。すべてのクエリに同じ ORDER BY 句が与えられました。すべてのテストで 17,408 の入力行が使用され、8,193 の結果行が生成されました。
次の人/理由の結果は表示されません:
RichardTheKiwi *Could not test--query needs updating*
ypercube *No SQL 2012 environment yet :)*
Tim S *Did not complete tests within 5 minutes*
インデックスなし:
CPU Duration Reads Writes
----------- ----------- ----------- -----------
ErikE 344 344 99 0
Simon Kingston 68672 69582 549203 49
インデックス付きCREATE UNIQUE CLUSTERED INDEX CI_#Data ON #Data (Time);
:
CPU Duration Reads Writes
----------- ----------- ----------- -----------
ErikE 328 336 99 0
Simon Kingston 70391 71291 549203 49 * basically not worse
インデックス付きCREATE UNIQUE CLUSTERED INDEX CI_#Data ON #Data (Time, Name);
:
CPU Duration Reads Writes
----------- ----------- ----------- -----------
ErikE 375 414 359 0 * IO WINNER
Simon Kingston 172 189 38273 0 * CPU WINNER
したがって、この話の教訓は次のとおりです。
適切なインデックスは、クエリ ウィザードよりも重要です
適切なインデックスを使用すると、Simon Kingston のバージョンが総合的に優れており、特にクエリの複雑性と保守性が含まれている場合に顕著です。
このレッスンによく注意してください!38k の読み取りは実際にはそれほど多くなく、Simon Kingston のバージョンは私のバージョンの半分の時間で実行されました。私のクエリの速度の向上は完全に、テーブルにインデックスがないことと、これが結合を必要とするすべてのクエリに与えた壊滅的なコスト (私はそうではありませんでした) によるものでした。インデックスを使用すると、彼のクエリはクラスター化されたインデックス シーク (ブックマーク ルックアップとも呼ばれます) を使用して入れ子になったループを実行できるようになり、処理が非常に高速になりました。
Time のクラスタ化インデックスだけでは不十分であったことは興味深いことです。時間は一意であり、名前は一度に 1 つしか発生しませんでしたが、名前を適切に利用するには、名前をインデックスの一部にする必要がありました。
データがいっぱいになったときにクラスター化インデックスをテーブルに追加するのに 1 秒もかかりませんでした。インデックスを無視しないでください。