結合ではなく集計による「ToTime」の検索
1回の論理読み取りでテーブルを1回スキャンするだけの非常にワイルドなクエリを共有したいと思います。比較すると、ページ上の他の最良の答えであるSimon Kingstonのクエリは、2回のスキャンを行います。
非常に大きなデータセット(17,408の入力行、8,193の結果行を生成)では、CPU 574と時間2645が必要ですが、SimonKingstonのクエリはCPU63,820と時間37,108を必要とします。
インデックスを使用すると、ページ上の他のクエリのパフォーマンスが何倍も向上する可能性がありますが、クエリを書き直すだけで、CPUが111倍、速度が14倍向上するのは興味深いことです。
(注意:Simon Kingstonや他の人をまったく軽視しているわけではありません。このクエリがうまく機能するという私の考えに興奮しています。彼のクエリは、パフォーマンスが十分で、実際に理解可能で保守可能であるため、私のものよりも優れています。 、私のものとは異なります。)
これが不可能なクエリです。わかりにくいです。書くのは大変でした。しかし、それは素晴らしいです。 :)
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
注:これにはSQL2008以降が必要です。 SQL 2005で機能させるには、VALUES句を SELECT 1 UNION ALL SELECT 2
に変更します。 。
更新されたクエリ
これについて少し考えた後、2つの別々の論理タスクを同時に実行していることに気付きました。これにより、クエリが不必要に複雑になりました。1)最終的なソリューションに関係のない中間行(開始しない行)を削除します。新しいタスク)および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;
この更新されたクエリには、説明で示したものとすべて同じ問題がありますが、余分な不要な行を処理していないため、解決が簡単です。 Row_Number()/ 2
もわかります 値0を除外する必要があり、以前のクエリから除外しなかった理由はわかりませんが、いずれの場合も、これは完全に機能し、驚くほど高速です。
アウターアプライタイズシングスアップ
最後に、これは基本的に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))
を使用して、行を2回繰り返すことにより、終了時刻を2つの異なる行に表示する必要があります。 。これで、すべての行が複製されました。アイデアは、JOINを使用して列全体の計算を行う代わりに、何らかの形式の集計を使用して、必要な行の各ペアを1つにまとめることです。 -
次のタスクは、重複する各行を適切に分割して、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)
を実行するための構文がありませんTime
RANK
は発生しませんName
の各変更を除いて変更する計算 。少し考えた後、Itzik Ben-Ganのグループ化された島のソリューションの背後にあるロジックから少し理解できることに気付き、Time
で並べ替えられた行のランクを見つけました。 、Name
で分割された行のランクから差し引かれますTime
で並べ替え 、は、同じグループの各行で同じであるが、他のグループとは異なる値を生成します。一般的なグループ化された島の手法は、4 5 6
などの行とロックステップで上昇する2つの計算値を作成することです。 および12 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」または「Walking」のいずれのグループも、反対のタイプからの同じ数の介在値を持つことはありません。つまり、最初のグループは「Running」で始まり、次の「Running」グループの前に2つの「Walking」行が介在するため、Nの値はT
> その次の「実行中」グループで。これについて考える1つの方法は、T --N
であることに気づきました。 計算では、現在の行の前に、同じ値「Running」または「Walking」に属していない行の数がカウントされます。これが真実であることを示す考えもあります。3番目の「Running」グループに移動すると、「Walking」グループがそれらを分離しているため、3番目のグループにすぎないため、介在する行の数が異なります。その前に、より高い位置から開始するため、値を複製できないほど十分に高くなっています。 -
最後に、最終グループは1行のみで構成されているため(終了時刻はなく、
NULL
を表示する必要があります。 代わりに)終了時刻があるかどうかを判断するために使用できる計算を投入する必要がありました。これは、Min(Num)
で実現されます。 式を作成し、最後にMin(Num)が2の場合(「次の」行がないことを意味します)、NULL
を表示することを検出します。Max(ToTime)
の代わりに 値。
この説明が人々の役に立つことを願っています。私の「行乗算」手法が一般的に有用であり、本番環境のほとんどのSQLクエリ作成者に適用できるかどうかはわかりません。これは、理解が難しく、メンテナンスが難しいため、次の訪問者に確実に提示されるためです。コード(反応はおそらく「一体何をしているのか!?!」の後に「書き直しの時間!」が続く)
あなたがこれまでにそれを成し遂げたなら、私はあなたの時間と信じられないほど楽しいsql-puzzle-landへの私の小さな遠足に私を甘やかしてくれてありがとう。
自分で見てください
別名「PREORDERBY」のシミュレーション:
最後にもう1つ。 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
に対して同じ値を持つことです。 これは、同じ名前の他のグループとは異なります。
パフォーマンス
自分のクエリが他の人よりも速いという点については、詳しく説明したくありません。ただし、(インデックスがない場合の)違いがどれほど顕著であるかを考えると、数値を表形式で表示したかったのです。これは、この種の行間の相関の高性能が必要な場合に適した手法です。
各クエリを実行する前に、DBCCFREEPROCCACHEを使用しました。 DBCC DROPCLEANBUFFERS;
。並列処理の時間折りたたみ効果を取り除くために、クエリごとにMAXDOPを1に設定しました。クライアントのデータ送信ではなくパフォーマンスのみを測定するために、各結果セットをクライアントに返すのではなく、変数に選択しました。すべてのクエリに同じORDERBY句が与えられました。すべてのテストで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
インデックス付きCREATEUNIQUE 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
インデックス付きCREATEUNIQUE 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
したがって、この話の教訓は次のとおりです。
適切なインデックスはクエリウィザードよりも重要です
適切なインデックスを使用すると、特にクエリの複雑さ/保守性を含めると、SimonKingstonのバージョンが全体的に勝ちます。
このレッスンによく注意してください! 38kの読み取りはそれほど多くはなく、SimonKingstonのバージョンは私の半分の時間で実行されました。私のクエリの速度の向上は、テーブルにインデックスがないことと、それに付随する壊滅的なコストが、結合を必要とするクエリに与えたためです(私の場合はそうではありませんでした):全表スキャンハッシュマッチはそのパフォーマンスを殺します。インデックスを使用すると、彼のクエリはクラスター化インデックスシーク(ブックマークルックアップとも呼ばれます)を使用してネストされたループを実行でき、本当に 速い。
時間だけのクラスター化されたインデックスでは不十分だったのは興味深いことです。 Timesは一意であり、1回に1つの名前しか発生しませんでしたが、それを適切に使用するには、名前をインデックスの一部にする必要がありました。
データがいっぱいになったときに1秒未満かかったときに、クラスター化されたインデックスをテーブルに追加します。インデックスをおろそかにしないでください。