14

最終的に同じ結果を返す 3 つのバージョンのクエリがあります。

そのうちの 1 つは、追加の内部結合を比較的小さなテーブルに追加し、かつ where パラメータ変数が where 句内で使用されている場合に劇的に遅くなります。

実行計画は、高速クエリと低速クエリで大きく異なります (各クエリの下に含まれています)。

これがなぜ起こったのか、そしてそれを防ぐ方法を理解したいと思います。

このクエリの所要時間は 1 秒未満です。追加の内部結合はありませんが、where 句でパラメーター変数を使用します。

    declare @start datetime = '20120115'
    declare @end datetime = '20120116'

    select distinct sups.campaignid 
    from tblSupporterMainDetails sups
    inner join tblCallLogs calls on sups.supporterid = calls.supporterid
    where calls.callEnd between @start and @end

  |--Parallelism(Gather Streams)
       |--Sort(DISTINCT ORDER BY:([sups].[campaignID] ASC))
            |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([sups].[campaignID]))
                 |--Hash Match(Partial Aggregate, HASH:([sups].[campaignID]))
                      |--Hash Match(Inner Join, HASH:([calls].[supporterID])=([sups].[supporterID]))
                           |--Bitmap(HASH:([calls].[supporterID]), DEFINE:([Bitmap1004]))
                           |    |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([calls].[supporterID]))
                           |         |--Index Seek(OBJECT:([GOGEN].[dbo].[tblCallLogs].[IX_tblCallLogs_callend_supporterid] AS [calls]), SEEK:([calls].[callEnd] >= '2012-01-15 00:00:00.000' AND [calls].[callEnd] <= '2012-01-16 00:00:00.000') ORDERED FORWARD)
                           |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([sups].[supporterID]))
                                |--Index Scan(OBJECT:([GOGEN].[dbo].[tblSupporterMainDetails].[AUTOGEN_IX_tblSupporterMainDetails_campaignID] AS [sups]),  WHERE:(PROBE([Bitmap1004],[GOGEN].[dbo].[tblSupporterMainDetails].[supporterID] as [sups].[supporterID],N'[IN ROW]')))

このクエリの所要時間は 1 秒未満です。追加の内部結合がありますが、where 句でパラメーター定数を使用します。

    select distinct camps.campaignid 
    from tblCampaigns camps
    inner join tblSupporterMainDetails sups on camps.campaignid = sups.campaignid
    inner join tblCallLogs calls on sups.supporterid = calls.supporterid
    where calls.callEnd between '20120115' and '20120116'

  |--Parallelism(Gather Streams)
       |--Hash Match(Right Semi Join, HASH:([sups].[campaignID])=([camps].[campaignID]))
            |--Bitmap(HASH:([sups].[campaignID]), DEFINE:([Bitmap1007]))
            |    |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([sups].[campaignID]))
            |         |--Hash Match(Partial Aggregate, HASH:([sups].[campaignID]))
            |              |--Hash Match(Inner Join, HASH:([calls].[supporterID])=([sups].[supporterID]))
            |                   |--Bitmap(HASH:([calls].[supporterID]), DEFINE:([Bitmap1006]))
            |                   |    |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([calls].[supporterID]))
            |                   |         |--Index Seek(OBJECT:([GOGEN].[dbo].[tblCallLogs].[IX_tblCallLogs_callend_supporterid] AS [calls]), SEEK:([calls].[callEnd] >= '2012-01-15 00:00:00.000' AND [calls].[callEnd] <= '2012-01-16 00:00:00.000') ORDERED FORWARD)
            |                   |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([sups].[supporterID]))
            |                        |--Index Scan(OBJECT:([GOGEN].[dbo].[tblSupporterMainDetails].[AUTOGEN_IX_tblSupporterMainDetails_campaignID] AS [sups]),  WHERE:(PROBE([Bitmap1006],[GOGEN].[dbo].[tblSupporterMainDetails].[supporterID] as [sups].[supporterID],N'[IN ROW]')))
            |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([camps].[campaignID]))
                 |--Index Scan(OBJECT:([GOGEN].[dbo].[tblCampaigns].[IX_tblCampaigns_isActive] AS [camps]),  WHERE:(PROBE([Bitmap1007],[GOGEN].[dbo].[tblCampaigns].[campaignID] as [camps].[campaignID],N'[IN ROW]')))

このクエリには 2 分かかります。追加の内部結合があり、WHERE 句でパラメーター変数を使用します。

    declare @start datetime = '20120115'
    declare @end datetime = '20120116'

    select distinct camps.campaignid 
    from tblCampaigns camps
    inner join tblSupporterMainDetails sups on camps.campaignid = sups.campaignid
    inner join tblCallLogs calls on sups.supporterid = calls.supporterid
    where calls.callEnd between @start and @end

  |--Nested Loops(Inner Join, OUTER REFERENCES:([camps].[campaignID]))
       |--Index Scan(OBJECT:([GOGEN].[dbo].[tblCampaigns].[IX_tblCampaigns_isActive] AS [camps]))
       |--Top(TOP EXPRESSION:((1)))
            |--Nested Loops(Inner Join, OUTER REFERENCES:([calls].[callID], [Expr1007]) OPTIMIZED WITH UNORDERED PREFETCH)
                 |--Nested Loops(Inner Join, OUTER REFERENCES:([sups].[supporterID], [Expr1006]) WITH UNORDERED PREFETCH)
                 |    |--Index Seek(OBJECT:([GOGEN].[dbo].[tblSupporterMainDetails].[AUTOGEN_IX_tblSupporterMainDetails_campaignID] AS [sups]), SEEK:([sups].[campaignID]=[GOGEN].[dbo].[tblCampaigns].[campaignID] as [camps].[campaignID]) ORDERED FORWARD)
                 |    |--Index Seek(OBJECT:([GOGEN].[dbo].[tblCallLogs].[IX_tblCallLogs_supporterID_closingCall] AS [calls]), SEEK:([calls].[supporterID]=[GOGEN].[dbo].[tblSupporterMainDetails].[supporterID] as [sups].[supporterID]) ORDERED FORWARD)
                 |--Clustered Index Seek(OBJECT:([GOGEN].[dbo].[tblCallLogs].[AUTOGEN_PK_tblCallLogs] AS [calls]), SEEK:([calls].[callID]=[GOGEN].[dbo].[tblCallLogs].[callID] as [calls].[callID]),  WHERE:([GOGEN].[dbo].[tblCallLogs].[callEnd] as [calls].[callEnd]>=[@s2] AND [GOGEN].[dbo].[tblCallLogs].[callEnd] as [calls].[callEnd]<=[@e2]) LOOKUP ORDERED FORWARD)

ノート:

  • 速度低下の原因は の Clustered Index Seek によるものだtblCallLogsと思いますが、SQL Server がこの実行プランを選択する理由はわかりません。
  • クエリ オプティマイザー ヒントを使用する必要がありますか? SQL Serverにその仕事のやり方を伝える必要があり、気が進まない...
  • この問題は、複数の要因の組み合わせ (余分な結合 AND 変数) によって引き起こされているようです。
  • 実行計画は、クエリの変数を見つけたときに「不適切な」計画を再利用しようとしている可能性がありますか?
  • 実際には、パラメーター変数を使用する必要があります。定数はダメ!したがって、この問題は多くのクエリ/ストアド プロシージャに存在する可能性があります。
  • tblCampaignsと のインデックスを再構築し、統計を更新しましたtblSupporterMainDetails。これは効果がありませんでした。
  • どちらのテーブルにも、主キー (ID 整数) にクラスター化インデックスがあります。
  • 外部キー列campaignidは索引付けされます。
  • すべてのクエリは同じパラメーター値を使用します。変数または定数として使用されることはありません。

テーブル内のレコード数:

  • tblSupporterMainDetails = 12,561,900
  • tblCallLogs = 27,242,224
  • tblCampaigns = 756

アップデート:

  • また、 のインデックスを再構築し、統計を更新しましたtblcalllogs。無効。
  • を使用して実行計画のキャッシュをクリアしましたDBCC FREEPROCCACHE
  • tblCallLogs.callEnd は日時です。

関連する列のスキーマ:

tblCampaign.campaignid int not null
tblSupporterMainDetails.campaignid int not null
tblSupporterMainDetails.supporterid int not null
tblCallLogs.supporterid int not null
tblCallLogs.callEnd datetime not null

インデックス:

インデックス

更新 2: tblCallLogs.supporterId にインデックスを追加した後 - インクルード列: callEnd
「遅い」クエリは 40 秒まで高速化されました。更新された実行計画:

  |--Nested Loops(Inner Join, OUTER REFERENCES:([camps].[campaignID]))
   |--Index Scan(OBJECT:([GOGEN].[dbo].[tblCampaigns].[IX_tblCampaigns_isActive] AS [camps]))
   |--Top(TOP EXPRESSION:((1)))
        |--Nested Loops(Inner Join, OUTER REFERENCES:([sups].[supporterID], [Expr1006]) WITH UNORDERED PREFETCH)
             |--Index Seek(OBJECT:([GOGEN].[dbo].[tblSupporterMainDetails].[AUTOGEN_IX_tblSupporterMainDetails_campaignID] AS [sups]), SEEK:([sups].[campaignID]=[GOGEN].[dbo].[tblCampaigns].[campaignID] as [camps].[campaignID]) ORDERED FORWARD)
             |--Index Seek(OBJECT:([GOGEN].[dbo].[tblCallLogs].[IX_tblCallLogs_supporterid_callend] AS [calls]), SEEK:([calls].[supporterID]=[GOGEN].[dbo].[tblSupporterMainDetails].[supporterID] as [sups].[supporterID]),  WHERE:([GOGEN].[dbo].[tblCallLogs].[callEnd] as [calls].[callEnd]>=[@s2] AND [GOGEN].[dbo].[tblCallLogs].[callEnd] as [calls].[callEnd]<=[@e2]) ORDERED FORWARD)

解決:

余分な結合が実際に問題を直接引き起こしているわけではありませんが、明らかにステートメントが変更されたため、SQL Server は別の実行計画を保持していました。
遅いステートメントの最後に OPTION(RECOMPILE) を追加することで、期待どおりの高速なパフォーマンスを得ることができました。つまり、1 秒未満です。この解決策が正確に機能したかどうかはまだわかりません-すべての計画作業をフラッシュしなかったのはなぜですか? これはパラメータ スニッフィングの典型的なケースですか? 正確な答えがわかったら、または誰かが明確な答えを出せるようになるまで、この投稿を更新します。これまで助けてくれた @LievenKeersmaekers と @JNK の両方に感謝します...

4

1 に答える 1

1

解決策の概要:

にカバリング インデックスを追加しsupporterid, callEndます。

ここでの前提は、オプティマイザーこのインデックスを (callEnd、supporterid とは対照的に)使用して、

  • 最初に参加tblSupporterMainDetailsし、tblCallLogs
  • さらに、selected のwhere節でそれを使用します。callEnd

オプションを追加するOPTION(RECOMPILE)

ハードコードされた定数または変数を使用するオプティマイザとの違いを説明してくれた TiborK と Hunchback に感謝します。

パフォーマンスへの影響 - 定数値 -vs- 変数

定数を使用すると、値がオプティマイザーに認識されるため、それに基づいて選択性 (および可能なインデックスの使用) を決定できます。変数を使用する場合、その値はオプティマイザには不明です (そのため、何らかの固定値または密度情報を使用する必要があります)。したがって、技術的には、これはパラメーター スニッフィングではありませんが、そのテーマに関する記事を見つけた場合は、定数と変数の違いについても説明する必要があります。OPTION(RECOMPILE) を使用すると、実際に変数がパラメータ スニッフィング状態になります。

本質的に、定数、変数、およびパラメーターには大きな違いがあります (盗聴することができます)。

于 2013-03-28T18:12:44.383 に答える