COALESCE(…)でサブクエリの選択を最適化する


8

アプリケーション内から使用する大きなビューがあります。私はパフォーマンスの問題を絞り込んだと思いますが、それを修正する方法がわかりません。ビューの簡略版は次のようになります。

SELECT ISNULL(SEId + '-' + PEId, '0-0') AS Id,
   *,
   DATEADD(minute, Duration, EventTime) AS EventEndTime
FROM (
    SELECT se.SEId, pe.PEId,
        COALESCE(pe.StaffName, se.StaffName) AS StaffName, -- << Problem!
        COALESCE(pe.EventTime, se.EventTime) AS EventTime,
        COALESCE(pe.EventType, se.EventType) AS EventType,
        COALESCE(pe.Duration, se.Duration) AS Duration,
        COALESCE(pe.Data, se.Data) AS Data,
        COALESCE(pe.Field, se.Field) AS Field,
        pe.ThisThing, se.OtherThing
    FROM PE pe FULL OUTER JOIN SE se 
      ON pe.StaffName = se.StaffName
     AND pe.Duration = se.Duration
     AND pe.EventTime = se.EventTime
    WHERE NOT(pe.ThisThing = 1 AND se.OtherThing = 0)
) Z

これはおそらくクエリ構造の理由全体を正当化するものではありませんが、おそらくあなたにアイデアを与えてくれます-このビューは、私が制御できない非常に不十分に設計された2つのテーブルを結合し、そこからいくつかの情報を合成しようとします。

したがって、これはアプリケーションから使用されるビューなので、最適化を試みている間、次のように別のSELECTでラップします。

SELECT * FROM (
    -- … above code …
) Q
WHERE StaffName = 'SMITH, JOHN Q'

アプリケーションが結果で特定のスタッフメンバーを検索しているためです。

問題はCOALESCE(pe.StaffName, se.StaffName) AS StaffNameセクションのようで、私はのビューから選択していますStaffName。それをpe.StaffName AS StaffNameまたはに変更するとse.StaffName AS StaffName、パフォーマンスの問題はなくなります(ただし、下記の更新された2を参照してください)。しかし、どちらか一方FULL OUTER JOINが欠落している可能性があるため、これはうまくいきません。一方または他方のフィールドがNULLになる可能性があります。

これをリファクタリングCOALESCE(…)して別のものに置き換えることはできますか?サブクエリに書き直されますか?

その他の注意事項:

  • クエリの残りの部分でのパフォーマンスの問題を修正するために、いくつかのインデックスをすでに追加していますCOALESCE
  • 驚いたことに、ラッピングサブクエリとWHEREステートメントが含まれている場合でも、実行プランを確認してもフラグは発生しません。アナライザーでのサブクエリの合計コストは0.0065736です。ふん。実行には4秒かかります。
  • アプリケーションを変更して別のクエリを実行する(たとえば、を返すpe.StaffName AS PEStaffName, se.StaffName AS SEStaffNameと実行するWHERE PEStaffName = 'X' OR SEStaffName = 'X')ことは機能するかもしれませんが、最後の手段として、アプリケーションに触れずにビューを最適化できることを本当に望んでいます。
  • ストアドプロシージャはおそらくこれにとってより理にかなっていますが、アプリケーションはEntity Frameworkで構築されており、テーブル型(別のトピック全体)を返すSPでうまく機能させる方法を理解できませんでした。

インデックス

これまでに追加したインデックスは、次のようになります。

CREATE NONCLUSTERED INDEX [IX_PE_EventTime]
ON [dbo].[PE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[ThisThing])

CREATE NONCLUSTERED INDEX [IX_SE_EventTime]
ON [dbo].[SE] ([EventTime])
INCLUDE ([StaffName],[Duration],[EventType],[Data],[Field],[OtherThing])

更新

うーん...私は上記の被災した変化をシミュレートしようとしましたが、それは助けにはなりませんでした。つまり、) Z上記の前にを追加しましたAND (pe.StaffName = 'SMITH, JOHN Q' OR se.StaffName = 'SMITH, JOHN Q')が、パフォーマンスは同じです。今、どこから始めればいいのか本当にわからない。

アップデート2

完全な結合が必要であるという@ypercubeのコメントから、合成クエリがおそらく重要なコンポーネントを省略していることがわかりました。はい、私は完全な結合が必要ですが、上記で私がドロップしCOALESCEて結合の片側だけをテストしてnull以外の値をテストすると、完全な結合の反対側は無関係になり、オプティマイザはおそらくこれを使用していましたクエリを高速化するための事実。また、例を更新して、それStaffNameが実際に結合キーの1つであることを示しました-これはおそらく問題に大きく関係しています。私は現在、これを完全結合ではなく3方向の結合に分割することが答えになる可能性があり、COALESCEとにかく行うsの数を単純化するという彼の提案にも寄りかかっています。今それを試してみてください。


追加したインデックスは何ですか?インデックスにStaffNameを含めていますか?
Mark Sinkinson、2014

@MarkSinkinson私は、上の各テーブルに非クラスタ化インデックス持ってKeyField、両方の索引フィールドおよび他のいくつかのフィールドを。質問のインデックス定義を投稿できます。テストサーバーでこれに取り組んでいるので、試してみると役立つと思われるインデックスを追加できます。INCLUDEStaffName
S'pht'Kr

1
あなたは持っているWHERE pe.ThisThing = 1 AND se.OtherThing = 0キャンセル条件FULL OUTER参加し、内部へのクエリと同等の参加になります。FULL参加が必要ですか?
ypercubeᵀᴹ

@ypercube申し訳ありませんが、これは私のエアコーディングが悪かったので、両方のテーブルに条件があるということですが、実際のクエリではどちらの側にもnullが含まれています。2つのテーブルをマージして一致を探していますが、左または右に一致するレコードがない場合は、どちらのテーブルからも使用可能なデータが必要です。つまり、完全な結合が必要です。
S'pht'Kr

1
思考:それはロングショットですが、あなたは(三つの部分に内部クエリを破るしようとすることができINNER JOINLEFT JOINWHERE IS NULL、チェックRIGHT IS NULLで登録しよう)し、その後、UNION ALL三つの部分。これにより、使用する必要がなくなりCOALESCE()、オプティマイザが書き換えを理解するのに役立つ場合があります(そうなる場合もあります)。
ypercubeᵀᴹ

回答:


4

これはかなり長続きしましたが、OPはそれが機能したと言っているので、私はそれを回答として追加します(何か問題があった場合は自由に修正してください)。

(三つの部分に内部クエリを破るようにしてくださいINNER JOINLEFT JOINWHERE IS NULL、チェックRIGHT JOINIS NULLチェック)し、その後、UNION ALL三つの部分。これには次の利点があります。

  • オプティマイザは、FULL結合(より一般的)INNERおよびLEFT結合よりも、結合で使用できる変換オプションが少なくなっています。

  • Z派生テーブルを除去することができるビュー定義から(あなたはとにかくそれを行うことができます)。

  • NOT(pe.ThisThing = 1 AND se.OtherThing = 0)のみに必要とされるであろうINNER参加する部分。

  • マイナーな改善、使用COALESCE()は少しでも最小限に抑えられます(私はnull可能se.SEIdpe.PEIdはないと仮定しました。null可能でない列が多い場合、より多くのCOALESCE()呼び出しを削除できるようになります)。
    さらに重要なことに、オプティマイザは、これらの列を含むクエリ(これCOALESCE()はプッシュをブロックしていません)

  • 上記のすべてにより、オプティマイザは、ビューを使用するすべてのクエリを変換/書き換えするためのより多くのオプションを提供するため、基になるテーブルのインデックスを使用できる実行プランを見つけることができます。

全体として、ビューは次のように記述できます。

SELECT 
    se.SEId + '-' + pe.PEId AS Id,
    se.SEId, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    COALESCE(pe.EventType, se.EventType) AS EventType,
    pe.Duration,
    COALESCE(pe.Data, se.Data) AS Data,
    COALESCE(pe.Field, se.Field) AS Field,
    pe.ThisThing, se.OtherThing,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe INNER JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1 AND se.OtherThing = 0) 

UNION ALL

SELECT 
    '0-0',
    NULL, pe.PEId,
    pe.StaffName, 
    pe.EventTime,
    pe.EventType,
    pe.Duration,
    pe.Data,
    pe.Field,
    pe.ThisThing, NULL,
    DATEADD(minute, pe.Duration, pe.EventTime) AS EventEndTime
FROM PE pe LEFT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (pe.ThisThing = 1)
  AND se.StaffName IS NULL

UNION ALL

SELECT 
    '0-0',
    se.SEId, NULL,
    se.StaffName, 
    se.EventTime,
    se.EventType,
    se.Duration,
    se.Data,
    se.Field,
    NULL, se.OtherThing, 
    DATEADD(minute, se.Duration, se.EventTime) AS EventEndTime
FROM PE pe RIGHT JOIN SE se 
  ON pe.StaffName = se.StaffName
 AND pe.Duration = se.Duration
 AND pe.EventTime = se.EventTime
WHERE NOT (se.OtherThing = 0)
  AND pe.StaffName IS NULL ;

0

私の直感はCOALESCE(pe.StaffName, se.StaffName) AS StaffName、2つのソースからのすべての行がすでに引き込まれ、一致しているはずなので、これは問題にならないはずなので、関数呼び出しは単純なメモリ内のnullとの比較です-ピック。明らかにこれはそうではないので、おそらくソースの1つ(ビューまたはインライン派生テーブルの場合)またはベーステーブル(つまり、インデックスがない場合)の何かが、これらの列を個別にスキャンする必要があるとクエリプランナーに考えさせています。

実行している正確なクエリ、サポート構造、および生成されたクエリプランの詳細がなければ、提案するものはすべて推測です。

結局のところ比較を強制的に実行するには、記述されたテーブル(pe.StaffName AS pe.StaffName, se.StaffName AS seStaffName)で両方の値を選択してから、外部クエリで選択(COALESCE(peStaffName, seStaffName) AS StaffName)するか、内部クエリのデータを一時テーブルは、それから選択して外部クエリを実行します(ただし、ストアドプロシージャが必要であり、行の数によっては、このtempdbへのダンプはコストがかかるため、それ自体に問題があります)。


Davidに感謝します。構造まで(pe => PatientEvent、そう…)まで、これについてどれだけ開示すべきかについて、妄想的な側面を誤解してきましたが、それが難しくなることはわかっています。実際には、インデックスに基づいて結合を行ってから、「単純なメモリ内比較」を行ってフィルター処理を行っていると思いますが、Z現在、フィルター処理されていない派生テーブルには約150万行が返されます。私がしたいことは、その述語をクエリに書き直してZ、インデックスを使用することです...しかし、述語を手動でそこに置いたときに、インデックスがまだ使用されていないため、今も混乱しています…よく分かりません。
S'pht'Kr 14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.