web-dev-qa-db-ja.com

SQL Server 2014ではクエリが100倍遅くなり、行カウントスプール行が原因を推定していますか?

SQL Server 2012では800ミリ秒で実行されるクエリがあり、約SQL Server 2014では170秒かかります。これは、_Row Count Spool_演算子のカーディナリティの見積もりが悪いことに絞り込んだと思います。スプールオペレーターについて少し読んだことがあります(例: here および here ))まだいくつかのことを理解するのに問題があります:

  • このクエリに_Row Count Spool_演算子が必要なのはなぜですか?正確さのために必要だとは思わないので、具体的にどのような最適化を提供しようとしているのですか?
  • SQL Serverが_Row Count Spool_演算子への結合によってすべての行が削除されると推定するのはなぜですか?
  • これはSQL Server 2014のバグですか?もしそうなら、私は接続でファイルします。しかし、私は最初により深い理解をお願いします。

注: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_を選択します。

enter image description here


SQL Server 2014:実際のクエリプラン

予想どおり(SQL Server以外のすべての人が!)、_Row Count Spool_は行を削除しませんでした。したがって、SQL Serverが1回だけループすると予想される場合は、10,000回ループします。

enter image description here


SQL Server 2012:推定クエリプラン

SQL Server 2012(またはSQL Server 2014ではOPTION (QUERYTRACEON 9481))を使用する場合、_Row Count Spool_は推定行数を削減せず、ハッシュ結合が選択されるため、はるかに優れた計画になります。

enter image description here

左結合の書き換え

参考までに、すべての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
_

enter image description here

13
Geoff Patterson

このクエリに行カウントスプール演算子が必要なのはなぜですか? ...それが提供しようとしている具体的な最適化は何ですか?

_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 
                       ) 
_

enter image description here

上記のようにゼロ行が出力されます。

SQL Serverの統計ヒストグラムと自動更新のしきい値は、この種の単一行の変更を検出するのに十分な粒度ではありません。おそらく、列がnull可能である場合、統計ヒストグラムが現在その存在を示していない場合でも、少なくとも1つのNULLが含まれていることに基づいて作業することは合理的かもしれません。

10
Martin Smith

このクエリに行カウントスプール演算子が必要なのはなぜですか?正確さのために必要だとは思わないので、具体的にどのような最適化を提供しようとしているのですか?

この質問については、 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を使用しており、問題が発生しないことを確認しました。

9
Geoff Patterson

マーティン・スミスの answer とあなたの self-answer はすべての主要なポイントに正しく対処しました。将来の読者のために領域を強調したいだけです。

したがって、この質問は、この特定のクエリを理解し、詳細に計画することに関するものであり、クエリを別の言い方で表現する方法に関するものではありません。

クエリの目的は次のとおりです。

-- Prune any existing customers from the set of potential new customers

この要件は、いくつかの方法でSQLで簡単に表現できます。どちらを選択するかは、他の何よりもスタイルの問題ですが、どのような場合でも正しい結果を返すようにクエリ仕様を作成する必要があります。 これには、nullの計算が含まれます。

論理的な要件を完全に表現する:

  • まだ顧客ではない潜在的な顧客を返す
  • 各潜在顧客を最大で1回リストアップする
  • Nullの潜在顧客と既存の顧客を除外する(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
    );

これにより、正しい結果を返す効率的な実行プランが作成されます。

Execution plan

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
    );

これについて魔法はなく、INANY、または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;

これにより正しい結果も生成されますが、ビットマップフィルタリングがないため、実行プランの効率が低下する可能性があります。

Non-bitmap execution plan

元の質問は興味深いもので、必要なnullチェック実装のパフォーマンスに影響を与える問題が明らかになっています。この回答の要点は、クエリを正しく記述することでも問題を回避できることです。

5
Paul White 9