SQL Server 2012では800ミリ秒で実行されるクエリがあり、約SQL Server 2014では170秒かかります。これは、_Row Count Spool
_演算子のカーディナリティの見積もりが悪いことに絞り込んだと思います。スプールオペレーターについて少し読んだことがあります(例: here および here ))まだいくつかのことを理解するのに問題があります:
Row Count Spool
_演算子が必要なのはなぜですか?正確さのために必要だとは思わないので、具体的にどのような最適化を提供しようとしているのですか?Row Count Spool
_演算子への結合によってすべての行が削除されると推定するのはなぜですか?注:SQL Server 2012とSQL Server 2014の両方で許容可能なパフォーマンスを達成するために、クエリを_LEFT JOIN
_として書き直すか、テーブルにインデックスを追加できます。この質問は、この特定のクエリと計画の理解についての詳細ですクエリを別の言い方で表現する方法については、詳しく説明します。
遅いクエリ
完全なテストスクリプトについては this Pastebin を参照してください。これが私が見ている特定のテストクエリです:
_-- Prune any existing customers from the set of potential new customers
-- This query is much slower than expected in SQL Server 2014
SELECT *
FROM #potentialNewCustomers -- 10K rows
WHERE cust_nbr NOT IN (
SELECT cust_nbr
FROM #existingCustomers -- 1MM rows
)
_
SQL Server 2014:推定クエリプラン
SQL Serverは、_Left Anti Semi Join
_から_Row Count Spool
_までの10,000行を1行にフィルタリングすると考えています。このため、_LOOP JOIN
_への後続の結合のために_#existingCustomers
_を選択します。
SQL Server 2014:実際のクエリプラン
予想どおり(SQL Server以外のすべての人が!)、_Row Count Spool
_は行を削除しませんでした。したがって、SQL Serverが1回だけループすると予想される場合は、10,000回ループします。
SQL Server 2012:推定クエリプラン
SQL Server 2012(またはSQL Server 2014ではOPTION (QUERYTRACEON 9481)
)を使用する場合、_Row Count Spool
_は推定行数を削減せず、ハッシュ結合が選択されるため、はるかに優れた計画になります。
左結合の書き換え
参考までに、すべてのSQL Server 2012、2014、2016で良好なパフォーマンスを達成するためにクエリを書き直す方法を次に示します。ただし、上記のクエリの特定の動作と、新しいSQL Server 2014 Cardinality Estimatorのバグです。
_-- Re-writing with LEFT JOIN yields much better performance in 2012/2014/2016
SELECT n.*
FROM #potentialNewCustomers n
LEFT JOIN (SELECT 1 AS test, cust_nbr FROM #existingCustomers) c
ON c.cust_nbr = n.cust_nbr
WHERE c.test IS NULL
_
このクエリに行カウントスプール演算子が必要なのはなぜですか? ...それが提供しようとしている具体的な最適化は何ですか?
_cust_nbr
_の_#existingCustomers
_列はNULL可能です。実際にnullが含まれている場合、正しい応答はゼロ行を返すことです(NOT IN (NULL,...)
は常に空の結果セットを生成します)。
したがって、クエリは次のように考えることができます
_SELECT p.*
FROM #potentialNewCustomers p
WHERE NOT EXISTS (SELECT *
FROM #existingCustomers e1
WHERE p.cust_nbr = e1.cust_nbr)
AND NOT EXISTS (SELECT *
FROM #existingCustomers e2
WHERE e2.cust_nbr IS NULL)
_
行数スプールを使用すると、
_EXISTS (SELECT *
FROM #existingCustomers e2
WHERE e2.cust_nbr IS NULL)
_
一回以上。
これは、仮定のわずかな違いがパフォーマンスに非常に壊滅的な違いをもたらす可能性がある場合のようです。
以下のように単一の行を更新した後...
_UPDATE #existingCustomers
SET cust_nbr = NULL
WHERE cust_nbr = 1;
_
...クエリは1秒未満で完了しました。計画の実際のバージョンと推定されたバージョンの行数がほぼスポットになりました。
_SET STATISTICS TIME ON;
SET STATISTICS IO ON;
SELECT *
FROM #potentialNewCustomers
WHERE cust_nbr NOT IN (SELECT cust_nbr
FROM #existingCustomers
)
_
上記のようにゼロ行が出力されます。
SQL Serverの統計ヒストグラムと自動更新のしきい値は、この種の単一行の変更を検出するのに十分な粒度ではありません。おそらく、列がnull可能である場合、統計ヒストグラムが現在その存在を示していない場合でも、少なくとも1つのNULL
が含まれていることに基づいて作業することは合理的かもしれません。
このクエリに行カウントスプール演算子が必要なのはなぜですか?正確さのために必要だとは思わないので、具体的にどのような最適化を提供しようとしているのですか?
この質問については、 Martinの徹底した回答 を参照してください。重要な点は、NOT IN
内の単一の行がNULL
である場合、ブールロジックは「正しい応答はゼロ行を返すこと」であるように機能することです。 Row Count Spool
演算子は、この(必要な)ロジックを最適化しています。
SQL Serverは、行カウントスプールオペレーターへの結合がすべての行を削除すると推定するのはなぜですか?
Microsoftは SQL 2014 Cardinality Estimatorに関する優れたホワイトペーパー を提供しています。このドキュメントでは、次の情報を見つけました。
新しいCEは、値がヒストグラムの範囲外にある場合でも、クエリされた値がデータセットに存在すると想定します。この例の新しいCEは、テーブルのカーディナリティに密度を掛けて計算される平均頻度を使用します。
多くの場合、そのような変更は非常に良いものです。 昇順キーの問題 を大幅に軽減し、通常、統計ヒストグラムに基づいて範囲外の値に対して、より保守的なクエリプラン(より高い行推定)を生成します。
ただし、この特定のケースでは、NULL
値が見つかると想定すると、Row Count Spool
に結合すると#potentialNewCustomers
からすべての行が除外されると想定されます。実際にNULL
行がある場合、これは正しい見積もりです(マーティンの回答に見られるように)。ただし、NULL
行が存在しない場合、SQL Serverは、表示される入力行の数に関係なく、結合後の推定1行を生成するため、影響が大きくなる可能性があります。これは、クエリプランの残りの部分で非常に貧弱な結合の選択につながる可能性があります。
これはSQL 2014のバグですか?もしそうなら、私は接続でファイルします。しかし、私は最初により深い理解をお願いします。
バグと、SQL Serverの新しいCardinality Estimatorのパフォーマンスに影響を与える仮定または制限の間の灰色の領域にあると思います。ただし、この癖により、たまたまNULL
値を持たないnull可能なNOT IN
句の特定のケースでは、SQL 2012に比べてパフォーマンスが大幅に低下する可能性があります。
したがって、SQLチームがCardinality Estimatorに対するこの変更の潜在的な影響を認識できるように、 a Connectの問題 を提出しました。
更新:現在SQL16のCTP3を使用しており、問題が発生しないことを確認しました。
マーティン・スミスの answer とあなたの self-answer はすべての主要なポイントに正しく対処しました。将来の読者のために領域を強調したいだけです。
したがって、この質問は、この特定のクエリを理解し、詳細に計画することに関するものであり、クエリを別の言い方で表現する方法に関するものではありません。
クエリの目的は次のとおりです。
-- Prune any existing customers from the set of potential new customers
この要件は、いくつかの方法でSQLで簡単に表現できます。どちらを選択するかは、他の何よりもスタイルの問題ですが、どのような場合でも正しい結果を返すようにクエリ仕様を作成する必要があります。 これには、nullの計算が含まれます。
論理的な要件を完全に表現する:
次に、必要な構文を使用して、これらの要件に一致するクエリを記述できます。例えば:
WITH DistinctPotentialNonNullCustomers AS
(
SELECT DISTINCT
PNC.cust_nbr
FROM #potentialNewCustomers AS PNC
WHERE
PNC.cust_nbr IS NOT NULL
)
SELECT
DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
DPNNC.cust_nbr NOT IN
(
SELECT
EC.cust_nbr
FROM #existingCustomers AS EC
WHERE
EC.cust_nbr IS NOT NULL
);
これにより、正しい結果を返す効率的な実行プランが作成されます。
NOT IN
なので <> ALL
またはNOT = ANY
計画や結果に影響を与えません。
WITH DistinctPotentialNonNullCustomers AS
(
SELECT DISTINCT
PNC.cust_nbr
FROM #potentialNewCustomers AS PNC
WHERE
PNC.cust_nbr IS NOT NULL
)
SELECT
DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
DPNNC.cust_nbr <> ALL
(
SELECT
EC.cust_nbr
FROM #existingCustomers AS EC
WHERE
EC.cust_nbr IS NOT NULL
);
WITH DistinctPotentialNonNullCustomers AS
(
SELECT DISTINCT
PNC.cust_nbr
FROM #potentialNewCustomers AS PNC
WHERE
PNC.cust_nbr IS NOT NULL
)
SELECT
DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
NOT DPNNC.cust_nbr = ANY
(
SELECT
EC.cust_nbr
FROM #existingCustomers AS EC
WHERE
EC.cust_nbr IS NOT NULL
);
またはNOT EXISTS
:
WITH DistinctPotentialNonNullCustomers AS
(
SELECT DISTINCT
PNC.cust_nbr
FROM #potentialNewCustomers AS PNC
WHERE
PNC.cust_nbr IS NOT NULL
)
SELECT
DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
NOT EXISTS
(
SELECT *
FROM #existingCustomers AS EC
WHERE
EC.cust_nbr = DPNNC.cust_nbr
AND EC.cust_nbr IS NOT NULL
);
これについて魔法はなく、IN
、ANY
、またはALL
を使用することに特に不快な点はありません。クエリを正しく記述するだけでよいので、常に正しい結果。
最もコンパクトな形式はEXCEPT
を使用します:
SELECT
PNC.cust_nbr
FROM #potentialNewCustomers AS PNC
WHERE
PNC.cust_nbr IS NOT NULL
EXCEPT
SELECT
EC.cust_nbr
FROM #existingCustomers AS EC
WHERE
EC.cust_nbr IS NOT NULL;
これにより正しい結果も生成されますが、ビットマップフィルタリングがないため、実行プランの効率が低下する可能性があります。
元の質問は興味深いもので、必要なnullチェック実装のパフォーマンスに影響を与える問題が明らかになっています。この回答の要点は、クエリを正しく記述することでも問題を回避できることです。