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


12

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

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

注:LEFT JOINSQL Server 2012とSQL Server 2014の両方で許容可能なパフォーマンスを実現するために、クエリをとして書き換えるか、テーブルにインデックスを追加できます。この質問は、この特定のクエリを理解し、詳細に計画することに関するもので、詳細については説明しません。クエリの言い方を変えるには


遅いクエリ

完全なテストスクリプトについては、この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 Spool1行に10,000行を絞り込むます。このため、LOOP JOINへの後続の結合にa を選択し#existingCustomersます。

ここに画像の説明を入力してください


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

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

ここに画像の説明を入力してください


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

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

ここに画像の説明を入力してください

LEFT JOINの書き換え

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

ここに画像の説明を入力してください

回答:


9

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

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可能であるNULL場合、統計ヒストグラムが現在その存在を示していない場合でも、少なくとも1つが含まれていることに基づいて作業することは合理的かもしれません。


8

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

この質問については、マーティンの完全な回答を参照してください。重要な点は、内の単一の行があればということでNOT INありNULL、ブール論理が「正しい応答がゼロ行を返すことにある」ようにうまくいきます。Row Count Spoolオペレータは、この(必要な)ロジックを最適化されています。

SQL Serverが行カウントスプールオペレーターへの結合がすべての行を削除すると推定するのはなぜですか?

マイクロソフトは、SQL 2014 Cardinality Estimatorに関する優れたホワイトペーパーを提供しています。このドキュメントでは、次の情報を見つけました。

新しいCEは、値がヒストグラムの範囲外にある場合でも、クエリされた値がデータセットに存在することを前提としています。この例の新しいCEは、表のカーディナリティーに密度を掛けて計算される平均頻度を使用します。

多くの場合、そのような変更は非常に良いものです。これにより、昇順のキーの問題が大幅に軽減され、通常、統計ヒストグラムに基づいて範囲外の値に対して、より保守的なクエリプラン(より高い行推定)が得られます。

ただし、この特定のケースでは、NULL値が見つかると想定すると、に結合するRow Count Spoolとのすべての行が除外されると想定され#potentialNewCustomersます。実際にNULL行がある場合、これは正しい見積もりです(マーティンの回答に見られるように)。ただし、NULL行が存在しない場合、SQL Serverは、入力行の数に関係なく、結合後に1行の推定値を生成するため、壊滅的な影響を与える可能性があります。これにより、クエリプランの残りの部分で結合の選択が非常に悪くなる可能性があります。

これはSQL 2014のバグですか?もしそうなら、私はConnectにファイルします。しかし、私は最初により深い理解をお願いします。

バグと、SQL Serverの新しいCardinality Estimatorのパフォーマンスに影響を与える仮定または制限の間の灰色の領域にあると思います。ただし、この癖が原因で、値NOT INを持たないnull許容句の特定のケースでは、SQL 2012に比べてパフォーマンスが大幅に低下する可能性がありNULLます。

したがって、SQLチームがCardinality Estimatorに対するこの変更の潜在的な影響を認識できるように、接続の問題を提出しまし

更新: SQL16のCTP3を使用しており、問題が発生しないことを確認しました。


4

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

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

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

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

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

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

  • まだ顧客ではない潜在的な顧客を返す
  • 各潜在顧客を多くても一度にリストする
  • 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
    );

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

実行計画

計画や結果に影響を与えることなく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;

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

非ビットマップ実行プラン

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.