web-dev-qa-db-ja.com

アンチセミ参加バグの回避策

次のSQL Serverクエリを作成しましたが、SQL Server 2005で anti-semi join欠陥 が発生し、カーディナリティの推定が不正確になり(1-urgh!)、永久に実行されます。これは長年の運用SQLサーバーであるため、バージョンのアップグレードを簡単に提案することはできません。そのため、この特定のクエリに対してトレースフラグ4199ヒントを強制することはできません。

WHERE AND NOT IN (SELECT)のリファクタリングに苦労しています。誰でも手伝ってくれる?クラスタ化されたキーペアに基づいて、最適な結合を使用してみてください。

SELECT TOP 5000 d.doc2_id
    ,d.direction_cd
    ,a.address_type_cd
    ,d.external_identification
    ,s.hash_value
    ,d.publishdate
    ,d.sender_address_id AS [D2 Sender_Address_id]
    ,a.address_id AS [A Address_ID]
    ,d.message_size
    ,d.subject
    ,emi.employee_id
FROM assentor.emcsdbuser.doc2 d(NOLOCK)
INNER JOIN assentor.emcsdbuser.employee_msg_index emi(NOLOCK)
    ON d.processdate = emi.processdate
    AND d.doc2_id = emi.doc2_id
INNER LOOP JOIN assentor.emcsdbuser.doc2_address a(NOLOCK)
    ON emi.doc2_id = a.doc2_id
    AND emi.address_type_cd = a.address_type_cd
    AND emi.address_id = a.address_id
INNER JOIN sis.dbo.sis s(NOLOCK) ON d.external_identification = s.external_identification
WHERE d.publishdate > '2008-01-01'
    **AND d.doc2_id NOT IN (
        SELECT doc2_id
        FROM assentor.emcsdbuser.doc2_address d2a(NOLOCK)
        WHERE d.doc2_id = d2a.doc2_id
            AND d2a.address_type_cd = 'FRM'
        )**
OPTION (FAST 10)

Employee_MSG_Indexテーブルは500m行、doc2は1.5b行、SISは〜500m行です。

何か助けていただければ幸いです!

7
beeks

これは長年の運用SQL Serverであるため、バージョンのアップグレードを簡単に提案することはできません

反準結合カーディナリティの推定バグは、SQL Serverのすべてのバージョンで再現可能です2005から2012まで。修正を有効にするには、すべてトレースフラグ4199が必要であるため、アップグレードでは4199をアクティブ化しないと問題を解決できません(もちろん、2005からアップグレードする理由は他にもたくさんあります)。

...そのため、この特定のクエリに対してトレースフラグ4199ヒントを強制することはできません。

影響を受ける特定のクエリが1つだけある場合OPTION (QUERYTRACEON 4199)を使用して、そのクエリのみのトレースフラグを有効にすることができます。このクエリヒントは4199で使用するための ドキュメント化およびサポート であり、SQL Server 2005 Service Pack 2以降に適用されます。

このヒントは、クエリの周りでDBCC TRACEON (4199)およびDBCC TRACEOFF (4199)を効果的に実行し、結果としてsysadmin権限が必要です。それが問題である場合は、 計画ガイド を使用してヒントを追加します。

4199を有効にしたシステム全体のテストinstance-wideも確認する必要があります。計画の回帰は可能ですが、全体として、このフラグによって有効化されるさまざまなオプティマイザの修正は、それだけの価値があることに気付くでしょう。今後の計画に影響するすべてのクエリプロセッサの修正 このフラグが必要 をアクティブ化します。

それが言ったすべて...

ypercubeの回答 で述べたように、このバグでは、(詳細の中でも)マニフェストを作成するために2つ以上の結合列が必要です。 _NOT IN_句のredundancyにより、オプティマイザは2つの列の比較(論理的には1つしかありません)、それによってバグを公開します。

この冗長性を削除すると、この特定のクエリの問題が「解決」されますが、実際に複数の結合述語を持つ他のクエリは依然として脆弱です

説明のために、質問にリンクされたCSSブログ投稿に基づく例を次に示します(ただし、完全なスクリプトを使用します)。

_CREATE TABLE dbo.tst_TAB1
(
    c1 integer NOT NULL, 
    c2 integer NOT NULL, 
    c3 integer NOT NULL
);

CREATE TABLE dbo.tst_TAB2
(
    c1 integer NOT NULL, 
    c2 integer NOT NULL, 
    c3 integer NOT NULL
);

CREATE INDEX i ON dbo.tst_TAB1 (c1, c2);
CREATE INDEX i ON dbo.tst_TAB2 (c1, c2);
_

サンプルデータ:

_INSERT dbo.tst_TAB1
    (c1, c2, c3)
SELECT 
    number, number, number
FROM master.dbo.spt_values
WHERE 
    [type] = N'P' 
    AND number BETWEEN 1 AND 2047;

INSERT dbo.tst_TAB2 (c1, c2, c3)
VALUES (1, 1, 1);
_

冗長な述語で_NOT IN_を使用してクエリをテストします。

_SELECT
    T1.c1
FROM tst_TAB1 AS t1 
WHERE
    t1.c1 NOT IN 
    (
        SELECT 
            t2.c1 
        FROM tst_TAB2 AS t2
        -- This is redundant!
        WHERE
            t2.c1 = t1.c1
    );
_

推定実行プランは、反準結合後の1行の推定を示しています。

Estimate 1 row

サイドノート:実際、これは別の(まれな)バグの例です。 _t1.c1 = t2.c1_ではなく_t2.c1 = t1.c1_としてWHERE句を記述すると、オプティマイザは2つの結合述語が実際に同じであり、バグが発生しないことを確認できます。

OPTION (QUERYTRACEON 4199)と同じクエリ:

_SELECT
    T1.c1
FROM tst_TAB1 AS t1 
WHERE
    t1.c1 NOT IN 
    (
        SELECT 
            t2.c1 
        FROM tst_TAB2 AS t2
        WHERE
            t2.c1 = t1.c1
    )
OPTION (QUERYTRACEON 4199);
_

推定実行プランは、2046行の推定値を表示しますが、これは正確です。

Plan with TF 4199

冗長な述語を削除することもできます:

_SELECT
    T1.c1
FROM tst_TAB1 AS t1 
WHERE
    t1.c1 NOT IN 
    (
        SELECT 
            t2.c1 
        FROM tst_TAB2 AS t2
    );
_

実行計画では偶然、関連のない追加の最適化(Stream Aggregate)が使用されていますが、重要な点は、4199を有効にする必要なしに、結合後の推定が正しいことです。

Without redundant predicate

複数の反準結合列

_NOT IN_構文を使用して、複数の列に対する反準結合を表すことができます。これらのケースでは4199が必要です。たとえば、次のクエリは_c1_および_c2_で結合します。

_SELECT
    T1.c1
FROM tst_TAB1 AS t1 
WHERE
    t1.c1 NOT IN 
    (
        SELECT 
            t2.c1 
        FROM tst_TAB2 AS t2
        WHERE
            t2.c2 = t1.c2
    );
_

実行計画は、誤った1行の見積もりを示しています。

Multiple join columns

4199で、問題は解決しました:

_SELECT
    T1.c1
FROM tst_TAB1 AS t1 
WHERE
    t1.c1 NOT IN 
    (
        SELECT 
            t2.c1 
        FROM tst_TAB2 AS t2
        WHERE
            t2.c2 = t1.c2
    )
OPTION (QUERYTRACEON 4199);
_

Multiple join columns with 4199

その他の構文

この方法で_NOT IN_を使用することは、特にBooks Onlineに記載されている理由から、避けるのが最善です。

BOL Warning

_NOT IN_とNULLsに関するこの問題は で何度も発生しています。利用可能な多くの代替構文があり、そのうち_NOT EXISTS_は私の個人的な好みです。構文を変更しても、カーディナリティ推定のバグは回避されないことに注意してください。

_SELECT
    T1.c1
FROM dbo.tst_TAB1 AS t1 
WHERE 
    NOT EXISTS 
    ( 
        SELECT 1 
        FROM dbo.tst_TAB2 AS t2 
        WHERE 
            t2.c1 = t1.c1 
            AND t2.c2 = t1.c2
    );
_

その2列の反セミ結合は1行の見積もりを生成し、それを修正するには4199が必要です。実行計画は前に見たものとまったく同じなので、繰り返しはしません。 _NOT EXISTS_構文は、_NOT IN_のNULLs問題を回避します。

その他の観察

Ypercubeの他の観察に同意します。

  • クエリ内のすべてのテーブルにNOLOCKヒントを振りかけるのは、コードの悪臭です。クエリが_READ UNCOMMITTED_トランザクションセマンティクスを本当に許容できる場合は、分離レベルを明示的に設定します。

  • _ORDER BY_なしのTOPは、コードが不十分であることを示すもう1つの兆候です。 TOPは、TOPの意味を定義するために_ORDER BY_句を必要とします。観察された動作に決して依存せず、明示的なトップレベルの_ORDER BY_を使用して保証を取得してください。

  • _INNER LOOP JOIN_および結合ヒントは、一般に_FORCE ORDER_クエリヒントを意味します。これはオプティマイザの自由を厳しく制限し、通常は誤解され、誤って適用されます。完全に理解していないヒントを使用しないでください。

13
Paul White 9

あなたが提供したリンクは、バグが複数の列を持つ結合にのみ影響を与えると言っています

上記の例のように、複数の結合列が結合に含まれている場合にのみ、この問題が発生することに注意してください。

そして、なぜあなたがこのようにNOT INを書き込んだのか(サブクエリにd.doc2_id = d2a.doc2_id条件を追加している)は理解できません。 NOT INは次のように書くことができます:

AND d.doc2_id NOT IN (
    SELECT d2a.doc2_id
    FROM assentor.emcsdbuser.doc2_address d2a
    WHERE d2a.address_type_cd = 'FRM'
    )

またはNOT EXISTSを使用:

AND NOT EXISTS (
    SELECT 1
    FROM assentor.emcsdbuser.doc2_address d2a
    WHERE d.doc2_id = d2a.doc2_id
        AND d2a.address_type_cd = 'FRM'
    )

両方を試して、カーディナリティの見積もりの​​問題が解決したかどうかを確認してください。

その他の注意事項:

  • address_type_cdのインデックスはありますか?
  • なぜ複数がNOLOCKを使用するのですか?
  • TOPなしでORDER BYを指定しないと、実行ごとに異なる結果が得られる場合があります。
6
ypercubeᵀᴹ