web-dev-qa-db-ja.com

ネストされたループで実行が遅いクエリを最適化する方法(内部結合)

TL; DR

この質問は何度も意見を得ているので、ここに要約しますので、初心者が歴史に苦しむ必要はありません。

JOIN table t ON t.member = @value1 OR t.member = @value2 -- this is slow as hell
JOIN table t ON t.member = COALESCE(@value1, @value2)    -- this is blazing fast
-- Note that here if @value1 has a value, @value2 is NULL, and vice versa

これはすべての人の問題ではないかもしれませんが、ON句の感度を強調することで、正しい方向に進むのに役立つかもしれません。いずれにせよ、元のテキストは将来の人類学者のためにここにあります:

元のテキスト

次の単純なクエリを検討してください(3つのテーブルのみが関係します)。

    SELECT

        l.sku_id AS ProductId,
        l.is_primary AS IsPrimary,
        v1.category_name AS Category1,
        v2.category_name AS Category2,
        v3.category_name AS Category3,
        v4.category_name AS Category4,
        v5.category_name AS Category5

    FROM category c4
    JOIN category_voc v4 ON v4.category_id = c4.category_id and v4.language_code = 'en'

    JOIN category c3 ON c3.category_id = c4.parent_category_id
    JOIN category_voc v3 ON v3.category_id = c3.category_id and v3.language_code = 'en'

    JOIN category c2 ON c2.category_id = c3.category_id
    JOIN category_voc v2 ON v2.category_id = c2.category_id and v2.language_code = 'en'

    JOIN category c1 ON c1.category_id = c2.parent_category_id
    JOIN category_voc v1 ON v1.category_id = c1.category_id and v1.language_code = 'en'

    LEFT OUTER JOIN category c5 ON c5.parent_category_id = c4.category_id
    LEFT OUTER JOIN category_voc v5 ON v5.category_id = c5.category_id and v5.language_code = @lang

    JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
    (
        l.category_id = c4.category_id OR
        l.category_id = c5.category_id
    )

    WHERE c4.[level] = 4 AND c4.version_id = 5

これは非常に単純なクエリであり、唯一混乱する部分は最後のカテゴリ結合です。これは、カテゴリレベル5が存在する場合と存在しない場合があるためです。クエリの最後で、製品ID(SKU ID)ごとのカテゴリ情報を探しています。そこに非常に大きなテーブルcategory_linkがあります。最後に、テーブル#Idsは10'000のIDを含む一時テーブルです。

実行すると、次の実際の実行プランが表示されます。

Actual Execution Plan

ご覧のとおり、時間のほぼ90%がネストされたループ(内部結合)に費やされています。これらのネストされたループに関する追加情報を次に示します。

Nested Loops (Inner Join)

読みやすくするためにクエリテーブル名を編集したため、テーブル名は完全には一致しませんが、一致させるのは簡単です(ads_alt_category = category)。このクエリを最適化する方法はありますか?また、本番環境では、一時テーブル#Idsは存在しません。これは、ストアドプロシージャに渡される同じ10'000 IDのテーブル値パラメーターです。

追加情報:

  • category_idとparent_category_idのカテゴリインデックス
  • category_id、language_codeのcategory_vocインデックス
  • sku_id、category_idのcategory_linkインデックス

編集(解決済み)

承認された回答で指摘されているように、問題はcategory_link JOINのOR句です。しかし、承認された回答で提案されたコードは非常に遅く、元のコードよりも遅くなります。多くのより速く、よりクリーンな解決策は、現在のJOIN条件を次のものに置き換えるだけです:

JOIN category_link l on l.sku_id IN (SELECT value FROM @p1) AND l.category_id = COALESCE(c5.category_id, c4.category_id)

この分Tweakは最も速いソリューションであり、受け入れられた回答からの二重結合に対してテストされ、valverrijによって提案されたCROSS APPLYに対してもテストされました。

40
Luis Ferrao

問題はコードのこの部分にあるようです:

_JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)
_

結合条件のorは常に疑わしいです。 1つの提案は、これを2つの結合に分割することです。

_JOIN category_link l1 on l1.sku_id in (SELECT value FROM #Ids) and l1.category_id = cr.category_id
left outer join
category_link l1 on l2.sku_id in (SELECT value FROM #Ids) and l2.category_id = cr.category_id
_

次に、これを処理するために残りのクエリを変更する必要があります。 。 。 coalesce(l1.sku_id, l2.sku_id)は、たとえばselect句で使用します。

17
Gordon Linoff

別のユーザーが述べたように、この結合が原因である可能性があります:

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

これらを複数の結合に分割する以外に、 CROSS APPLY を試すこともできます

CROSS APPLY (
    SELECT [some column(s)]
    FROM category_link x
    WHERE EXISTS(SELECT value FROM #Ids WHERE value = x.sku_id)
    AND (x.category_id = c4.category_id OR x.category_id = c5.category_id)        
) l

上記のMSDNリンクから:

テーブル値関数は右入力として機能し、外側のテーブル式は左入力として機能します。 右入力は左入力からの各行に対して評価され、生成された行は最終出力に結合されます。

基本的に、APPLYは、最初に右側のレコードを除外するサブクエリのようなものであり、、次に残りのクエリにそれらを適用します。

この記事は、それが何であり、いつ使用するかを説明するのに非常に優れています。 http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

ただし、CROSS APPLYは常にINNER JOINよりも高速に動作するとは限らないことに注意することが重要です。多くの場合、それはおそらくほぼ同じです。ただし、まれに、実際にそれが遅くなるのを見たことがあります(これもすべて、テーブル構造とクエリ自体に依存します)。

一般的な経験則として、条件付きステートメントが多すぎるテーブルに参加している場合、APPLYに傾く傾向があります。

また、楽しいメモ:OUTER APPLYLEFT JOINのように機能します

また、EXISTSではなくINを使用するという私の選択に注意してください。サブクエリでINを実行する場合、値が見つかった後でも、結果セット全体が返されることに注意してください。ただし、EXISTSを使用すると、一致が見つかるとすぐにサブクエリが停止します。

9
valverij