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


39

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を含む一時テーブルです。

実行すると、次の実際の実行計画が得られます。

実際の実行計画

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

ネストされたループ(内部結合)

読みやすいようにクエリテーブル名を編集したため、テーブル名は完全には一致しませんが、一致させるのは非常に簡単です(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)

この微調整は最速のソリューションであり、承認された回答からの二重結合に対してテストされ、valverijが提案するCROSS APPLYに対してもテストされています。


クエリプランの残りを確認する必要があります。
–RBarryYoung

ほんの一言:依存結合の多くがカーディナリティの推定エラーを引き起こす可能性があります。ほとんどの場合、クエリのパフォーマンスは、カーディナリティの過小評価によって低下します。
usr

実行計画はインデックスの提案をしますか?また、一時テーブルに主キーとインデックスを設定できることを忘れないでください(詳細はこちら

私は何を取得、現在のソリューションを試した後、私は疑問に改善するだろう場合@rbarry

1
UNIONを使用してクエリを複製し、ORを取り除くことについてはどうですか

回答:


17

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

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句内。


その特定に参加して行われているフィルタリングの量で、私はまた、変更をテストしたいJOINCROSS APPLYINに切り替えることEXISTSAPPLYWHERE句。

ゴードンのおかげで、私はこの最初のことを午前中にテストします。@Valverij、私はクロスアプライに精通していません、あなたはあなたのソリューションをもっと適切な答えで説明してもらえますか?

3
問題を指摘した最初の回答だったので、この回答を受け入れています。ただし、推奨される解決策は非常に遅く、元のコードよりも遅くなります。しかし、OR句が問題であることを知って、単にそれを置き換えるだけでON l.category_id = ISNULL(c5.category_id, c4.category_id、トリックができました。
ルイスフェラオ

1
@LuisFerrao。。。追加情報をありがとうございます。coalesce()オプティマイザが正しい方向にプッシュされることを知っておくと役立ちます。
ゴードンリノフ

9

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

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 APPLYように振る舞いますLEFT JOIN

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


このソリューションを徹底的にテストしました。あなたが書いたように、それはかなり遅いですが、あなたがあなたのメッセージを始めたアドバイスを適用するのを忘れました。IN句で置き換えて削除AND x.cat = c4.cat OR x.cat = c5.catするx.cat = ISNULL(c5.cat, c4.cat)ことは、これが2番目に速いソリューションであり、かなり有益なため、賛成票に値します。
ルイスフェラオ

ありがとう。IN行は実際には存在しないはずでした(INを使用するかORに固執するかを決定できませんでした)。削除します。
valverij
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.