EXISTSで存在を確認してください。…違いますか?


35

行の存在を常に COUNTではなくEXISTSで確認する必要がある場合によく読んでいます。

しかし、最近のいくつかのシナリオでは、カウントを使用したときのパフォーマンスの改善を測定しました。
パターンは次のようになります。

LEFT JOIN (
    SELECT
        someID
        , COUNT(*)
    FROM someTable
    GROUP BY someID
) AS Alias ON (
    Alias.someID = mainTable.ID
)

私はSQL Serverの「内部」で何が起こっているのかを知る方法に精通していないので、私が行った測定に完全に意味のあるEXISTSの前例のない欠陥があるのではないかと思っていました(RISTが存在する可能性があります!)。

その現象についての説明はありますか?

編集:

実行できる完全なスクリプトを次に示します。

SET NOCOUNT ON
SET STATISTICS IO OFF

DECLARE @tmp1 TABLE (
    ID INT UNIQUE
)


DECLARE @tmp2 TABLE (
    ID INT
    , X INT IDENTITY
    , UNIQUE (ID, X)
)

; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp1
SELECT n
FROM tally AS T1
WHERE n < 10000


; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp2
SELECT T1.n
FROM tally AS T1
CROSS JOIN T AS T2
WHERE T1.n < 10000
AND T1.n % 3 <> 0
AND T2.n < 1 + T1.n % 15

PRINT '
COUNT Version:
'

WAITFOR DELAY '00:00:01'

SET STATISTICS IO ON
SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN n > 0 THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
LEFT JOIN (
    SELECT
        T2.ID
        , COUNT(*) AS n
    FROM @tmp2 AS T2
    GROUP BY T2.ID
) AS T2 ON (
    T2.ID = T1.ID
)
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF

PRINT '

EXISTS Version:'

WAITFOR DELAY '00:00:01'

SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN EXISTS (
        SELECT 1
        FROM @tmp2 AS T2
        WHERE T2.ID = T1.ID
    ) THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF 

SQL Server 2008R2(7 64ビット)でこの結果が得られます

COUNT バージョン:

テーブル「#455F344D」。スキャンカウント1、論理読み取り8、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み0。
表 '#492FC531'。スキャンカウント1、論理読み取り30、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。

SQL Serverの実行時間:
CPU時間= 0ミリ秒、経過時間= 81ミリ秒。

EXISTS バージョン:

テーブル「#492FC531」。スキャンカウント1、論理読み取り96、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み0。
表 '#455F344D'。スキャンカウント1、論理読み取り8、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0

SQL Serverの実行時間:
CPU時間= 0ミリ秒、経過時間= 76ミリ秒。

回答:


43

行の存在を常に COUNTではなくEXISTSで確認する必要がある場合によく読んでいます。

特にデータベースに関しては、常に真であることが非常にまれです。SQLで同じセマンティックを表現する方法はいくつもあります。有用な経験則がある場合は、利用可能な最も自然な構文を使用してクエリを作成し(そして、はい、それは主観的です)、取得するクエリプランまたはパフォーマンスが許容できない場合にのみ書き換えを考慮することです。

価値があることについては、私自身の問題に対する考え方は、存在クエリはを使用して最も自然に表現されるということですEXISTS。また、拒否の選択肢EXISTS より最適化する傾向があるのは私の経験でした。使用すると、上のフィルタリングは、SQL Serverクエリオプティマイザにいくつかのサポートを持っているために起こる別の代替として、ですが、私は個人的に、これは、より複雑なクエリでは信頼できないことが判明しています。いずれにせよ、これらの選択肢のいずれよりも(私には)ずっと自然に思えます。OUTER JOINNULLCOUNT(*)=0EXISTS

私が行った測定に完全に意味を与えるEXISTSの前例のない欠陥があるかどうか疑問に思っていました

オプティマイザがCASE式(およびEXISTS特にテスト)のサブクエリを処理する方法を強調しているため、特定の例は興味深いものです。

CASE式のサブクエリ

次の(完全に合法な)クエリを検討してください。

DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);

SELECT
    CASE
        WHEN (SELECT W.b FROM @When AS W) = 1
            THEN (SELECT T.c FROM @Then AS T)
        ELSE (SELECT E.d FROM @Else AS E)
    END
FROM @Base AS B;

セマンティクスCASEは、WHEN/ELSE句が通常テキスト順で評価されるということです。上記のクエリでELSEWHEN句が満たされた場合にサブクエリが複数の行を返した場合、SQL Serverがエラーを返すのは正しくありません。これらのセマンティクスを尊重するために、オプティマイザーはパススルー述語を使用するプランを作成します。

パススルー述語

ネストされたループ結合の内側は、パススルー述語がfalseを返す場合にのみ評価されます。全体的な効果は、CASE式が順番にテストされ、前の式が満たされていない場合にのみサブクエリが評価されることです。

EXISTSサブクエリを含むCASE式

どこCASEサブクエリの用途はEXISTS、論理的な存在テストは、半結合として実装されていますが、通常は半結合によって拒否される行は、後で句がそれらを必要とする場合に保持する必要があります。この特別な種類の準結合を通過する行は、準結合が一致を検出したかどうかを示すフラグを取得します。このフラグは、プローブ列と呼ばれます

実装の詳細は、論理サブクエリがプローブ列との相関結合(「適用」)に置き換えられることです。作業は、クエリオプティマイザーの単純化規則RemoveSubqInPrj(投影のサブクエリを削除)によって実行されます。トレースフラグ8606を使用して詳細を確認できます。

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);

EXISTSテストを示す入力ツリーの一部を以下に示します。

ScaOp_Exists 
    LogOp_Project
        LogOp_Select
            LogOp_Get TBL: #T2
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier [T2].ID
                ScaOp_Identifier [T1].ID

これは、以下によってRemoveSubqInPrj先頭に立つ構造に変換されます。

LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)

これは、前述のプローブを使用した左の準結合適用です。この初期変換は、これまでSQL Serverクエリオプティマイザーで使用可能な唯一のものであり、この変換が無効になっているとコンパイルは失敗します。

このクエリで可能な実行計画の形状の1つは、その論理構造の直接実装です。

NLJ半結合プローブ

最後のCompute ScalarはCASE、プローブ列の値を使用して式の結果を評価します。

スカラー式の計算

最適化がセミ結合の他の物理結合タイプを考慮する場合、プランツリーの基本形状は保持されます。マージ結合のみがプローブ列をサポートしているため、論理的には可能ですが、ハッシュ半結合は考慮されません。

プローブ列とマージ

マージでExpr1008は、ラベルが付けられた式が出力されることに注意してください(名前は以前と同じであることが偶然です)。これは再びプローブ列です。前と同様に、最後のCompute Scalarはこのプローブ値を使用してを評価しCASEます。

問題は、オプティマイザーが、マージ(またはハッシュ)準結合でのみ価値のある代替案を完全に探索しないことです。ネストされたループプランではT2、すべての反復で範囲内の行が一致するかどうかをチェックする利点はありません。マージまたはハッシュプランを使用すると、これは便利な最適化になります。

一致するBETWEEN述語をT2クエリに追加すると、マージセミジョインの残余として各行に対してこのチェックが実行されるだけです(実行プランで見つけるのは困難ですが、そこにあります)。

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
            AND T2.ID BETWEEN 5000 AND 7000 -- New
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

残存述語

BETWEEN代わりに、述語がT2シークの結果にプッシュダウンされることを願っています。通常、オプティマイザはこれを行うことを検討します(クエリに余分な述語がなくても)。それは認識して暗黙の述語をBETWEENT1との間の述語に参加T1し、T2一緒に暗示BETWEENT2)を、それらが元のクエリのテキスト内に存在せず。残念ながら、apply-probeパターンは、これが検討されていないことを意味します。

クエリを記述して、マージ半結合への両方の入力でシークを生成する方法があります。1つの方法は、非常に不自然な方法でクエリを記述することを伴います(私が一般的に好む理由を破りますEXISTS):

WITH T2 AS
(
    SELECT TOP (9223372036854775807) * 
    FROM #T2 AS T2 
    WHERE ID BETWEEN 5000 AND 7000
)
SELECT 
    T1.ID, 
    DoesExist = 
        CASE 
            WHEN EXISTS 
            (
                SELECT * FROM T2 
                WHERE T2.ID = T1.ID
            ) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

トップトリックプラン

実稼働環境でそのクエリを書くのはうれしくありません。目的の計画形状が可能であることを示すだけです。記述する必要がある実際のクエリがCASEこの特定の方法で使用され、マージセミジョインのプローブ側にシークがないためにパフォーマンスが低下する場合は、正しい結果を生成する異なる構文を使用してクエリを記述することを検討してください。より効率的な実行計画。


6

引数には、「対COUNT(*)は、EXISTS」レコードが存在するかどうかをチェックして行うことです。例えば:

WHERE (SELECT COUNT(*) FROM Table WHERE ID=@ID)>0

WHERE EXISTS(SELECT ID FROM Table WHERE ID=@ID)

SQLスクリプトはCOUNT(*)レコードの存在チェックとして使用していないため、シナリオに適用できるとは言いません。


私が投稿したスクリプトに基づいて、しかし/結論はありますか?
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.