クエリの一部としてCOUNT(*) OVER ()
を使用して、テーブルへの約15の結合(一部は大きい、一部は小さい)で大量のデータをフェッチします。ここで数えるための最良の解決策は何ですか?
COUNT(*) OVER ()
は、クエリオプティマイザーにとって安価であるはずの操作の1つです。結局のところ、SQL Serverは、クエリによって返される行数を既に認識しています。その値をクエリの結果セットに投影するように要求しているだけです。残念ながら、COUNT(*) OVER ()
は、追加先のクエリによっては、コストのかかる操作になる可能性があります。
簡単なテストでは、適度な量のテストをテーブルに入れます。本当にどんなテーブルでも大丈夫ですが、これが自宅でフォローしている人のための私のサンプルデータです:
_SELECT
CAST(REPLICATE('A', 100) AS VARCHAR(100)) ID1
, CAST(REPLICATE('Z', 100) AS VARCHAR(100)) ID2
INTO #JUNK_DATA
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
_
このクエリは、結果セットを破棄するときに2.5秒かかります。
_SELECT ID1, ID2
FROM #JUNK_DATA;
_
このクエリをシリアルに実行すると2.7秒かかりますが、並列化できます。
_SELECT COUNT(*)
FROM #JUNK_DATA
OPTION (MAXDOP 1);
_
COUNT
集約クエリを使用したクエリは、私のマシンで完了するまでに47.2秒かかります。
_SELECT ID1, ID2, COUNT(*) OVER () CNT
FROM #JUNK_DATA;
_
おそらく私のマシンに問題がありますが、それは間違いなくより多くの仕事をしています。クエリプランは次のとおりです。
SQL Serverは、すべての列の値をテーブルスプールにロードしてから、そのデータを2回読み取り、それらを結合しています。結果セットに含まれる行数がすでにわかっているのに、なぜすべての作業を行うのですか?
SQL Serverクエリプランは、データを一度に1行ずつ(多かれ少なかれ)ストリーミング方式で処理します。したがって、行数を計算すると、次のようになります。
_╔═════╦═════╦══════════════╗
║ ID1 ║ ID2 ║ COUNT_SO_FAR ║
╠═════╬═════╬══════════════╣
║ A ║ B ║ 1 ║
║ C ║ D ║ 2 ║
║ E ║ F ║ 3 ║
║ ... ║ ... ║ ... ║
║ ZZZ ║ ZZZ ║ 6431296 ║
╚═════╩═════╩══════════════╝
_
その操作はデータの個別のコピーを必要としませんが、6431296の最終値を以前のすべての行にどのように適用できますか?この操作は、クエリプランで確認した二重スプールとして実装される場合があります。 SQL Serverで使用したいより効率的な内部アルゴリズムを想像することは可能ですが、それを直接制御することはできません。
私が見ているように、ここにあなたの問題を解決するための選択肢があります:
アプリケーションを修正します。 SQL Serverは、クエリが返す行数を既に認識しています。 _@@ROWCOUNT
_またはその他の方法を使用するのがはるかに望ましい方法です。
別のクエリを実行してカウントを取得します。これは、クライアントに大きな結果セットを返し、クエリ間でデータが変更されないことが保証されている場合に適しています。個別のCOUNT
は、並列処理の利点と、関連するテーブルが既にバッファキャッシュにロードされていることの利点があります。テーブルのインデックスに応じて、SQL Serverは、完全な結果セットとは対照的に、より少ないIOでカウントを取得できます)。
COUNT
集計をクエリに追加します。これは、一般的な結果セットが小さい場合に適しています。そうすれば、スプールに大量のデータをロードする必要がなくなります。
この回答の残りの部分には、ウィンドウ関数で実行できるいくつかのトリックに関する情報が含まれています。それらのすべてがSQL Server 2008のバージョンに関連しているわけではありません。
単純なROW_NUMBER()
追加では、結果セットをテーブルスプールに置く必要はありません。以前の例を考えると、それは理にかなっています。 SQL Serverは値をそのまま計算できるので、前の行に値を適用する必要はありません。このクエリは私のマシンで3.1秒で実行されました:
_SELECT
ID1
, ID2
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM #JUNK_DATA;
_
これはクエリプランで確認できます。
行数としてRN
の最大値を単純に取るようにアプリケーションを変更する場合、これはあなたにとって良いオプションになるでしょう。
ROW_NUMBER()
を_UNION ALL
_と一緒に使用して、最後のダミー行で探している値を取得したくなるかもしれません。これは、質問で提案したものと似ています。例えば:
_SELECT
t.ID1
, t.ID2
, CASE WHEN ROW_COUNT_ROW = 'Y' THEN -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) ELSE NULL END ROW_COUNT
FROM
(
SELECT
ID1
, ID2
, 'N' ROW_COUNT_ROW
FROM #JUNK_DATA
UNION ALL
SELECT
NULL ID1
, NULL ID2
, 'Y' ROW_COUNT_ROW
) t;
_
上記のクエリは3.4秒で終了し、正しい結果が返されました。ただし、指定されていない結合順序に依存しているため、誤った結果が返されることもあります。計画を見る:
青色のクエリの部分は、連結の上部でなければなりません。赤のクエリの部分は下半分でなければなりません。クエリ最適化では、クエリプランを自由に再配置したり、UNION
を別の物理演算子で実装したりして、常に正しい結果が得られるとは限りません。
SQL Server 2012以降では、LEAD
ウィンドウ関数を使用して、上記の例と同様のことを実行できます。
_SELECT
t.ID1
, t.ID2
, CASE WHEN LD IS NULL THEN RN ELSE NULL END ROW_COUNT
FROM
(
SELECT ID1, ID2
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
, LEAD(ID1) OVER (ORDER BY (SELECT NULL)) LD
FROM #JUNK_DATA
) t;
_
このクエリは明示的なテーブルスプールを回避し、他のクエリよりも安全だと感じます。結局、SQL Serverがウィンドウ関数を異なる順序で処理するのはなぜですか?ただし、正しい結果が保証されるわけではなく、私のマシンでは12.5秒で終了します。
クエリに明示的なORDER
を追加して、常に正しい結果を得ることができます。
_SELECT
t.ID1
, t.ID2
, CASE WHEN ORDER_BY_COL = 1 THEN -1 + ROW_NUMBER() OVER (ORDER BY ORDER_BY_COL) ELSE NULL END ROW_COUNT
FROM
(
SELECT
ID1
, ID2
, 0 ORDER_BY_COL
FROM #JUNK_DATA
UNION ALL
SELECT
NULL ID1
, NULL ID2
, 1 ORDER_BY_COL
) t
OPTION (MAXDOP 1);
_
ただし、これにはクエリプランに高価な並べ替え演算子が必要です。
クエリは21.5秒でシリアルに実行され、並列で実行すると、より速く終了します。
最後に、SQL Server 2016では、 ウィンドウ関数を実装する演算子のバッチモードを追加 することが可能です。空のCCIテーブルを作成してクエリに追加します。
_CREATE TABLE #ADD_BATCH_MODE (
ID INT NOT NULL,
INDEX CCI_BATCH CLUSTERED COLUMNSTORE
);
SELECT ID1, ID2, COUNT(*) OVER () ROW_COUNT
FROM #JUNK_DATA
LEFT OUTER JOIN #ADD_BATCH_MODE ON 1 = 0;
_
これにより、SQL Serverが集計にバッチモードを使用するようになります。ここに計画があります:
クエリは確かに単純に見えます。ディスク上のスプールはもう存在せず、5.4秒で終了します。複雑なクエリにバッチモードを追加すると他の効果があり、一般的には効果がありますが、テストしないとわかりません。