web-dev-qa-db-ja.com

一時テーブルが原因で統計更新後の実行プランが不良

ストアドプロシージャクエリは、テーブルの1つで統計が更新された後、不適切なプランを取得することがありますが、すぐに適切なプランに再コンパイルできます。同じコンパイル済みパラメーター。

問題は、SPで作成されてから結合された小さな一時テーブルにあると思われます。悪い計画では、一時テーブルに対して、結合列に統計がないという警告があります。何が原因ですか?

SQL Server 2016 SP1 CU4、2014互換性レベル

悪い計画:

Bad plan screenshot

良い計画:

Good plan screenshot

ストアドプロシージャ

USE AppDB
GO
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE PROCEDURE [MySchema].[MySP]
    @MyId VARCHAR(50),
    @Months INT
AS
BEGIN

    SET NOCOUNT ON

    SELECT * 
    INTO #MyTemp
    FROM AppDB.MySchema.View_Feeder vf WITH (NOLOCK)
    WHERE vf.MyId = @MyId AND vf.Status IS NOT NULL

    SELECT wd.Col1
         , vp.Col2
         , vp.Col3 
    FROM AppDB.MySchema.View_VP vp WITH (FORCESEEK)
    INNER JOIN #MyTemp wd ON wd.Col1 = vp.Col1
    WHERE vp.Col3 > DATEADD(MONTH, @Months * -1, GETDATE())

END

内観

USE AppDB
GO
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE VIEW [MySchema].[View_VP]
AS

    SELECT pp.Col1,
           pd.Col2 AS Col2, 
           MAX(pp.Col4) AS Col3
    FROM P_DB..LargeTable pp WITH (NOLOCK)
    INNER JOIN P_DB..SmallTable pd WITH (NOLOCK) ON pp.P_Id = pd.P_Id
    WHERE pp.[Status] IN (3, 4)
    GROUP BY pp.Col1, pd.Col2

予定

編集済み 良い計画 および 悪い計画

追加情報

FORCESEEKヒントは、この非常に同じ問題を処理し、計画を安定させるために追加されました。とにかく、それがあろうとなかろうと、ここで何が起こっているのかを本当に理解したいと思います。

私は問題を自由に再現することができないので、SELECT INTOを明示的なテーブルに置き換えることが違いを生むかどうかは言いがたいです。ただし、同じように動作するはずです。

SELECT
    database_id, 
    is_auto_create_stats_on, 
    is_auto_update_stats_on, 
    is_auto_update_stats_async_on
FROM sys.databases
WHERE
    database_id IN (2, <relevant user databases>)

戻り値:

  database_id   is_auto_create_stats_on   is_auto_update_stats_on   is_auto_update_stats_async_on  
 ------------- ------------------------- ------------------------- ------------------------------- 
  2             1                         1                         0                              
  7             1                         1                         1                              
  37            1                         1                         1                              

このシークがひどいことは明らかですが、問題はそもそもなぜ良いシークを行わないのかです。

クエリは100万行を返しません。見積もりが間違っています。出力に若干の変更がある可能性がありますが、行の数は常にかなり少ないです(おそらく最大で数百)。

比較的多くの行を返すものでさえ、Idがシークするプランを生成し、statusはシークしません(これは、見てわかるように選択的ではありません)。どのような値がまとめられていても、ステータス探索計画を再現できないようです。一時テーブルの作成と2番目のクエリの間にwaitfor delayを追加して、統計を更新するか、2番目のセッションで再コンパイルしても、効果はありませんでした。

8
Alex Friedman

悪い計画では、一時テーブルに対して、結合列に統計がないという警告があります。何ができますか?

これにはより難解な理由があるかもしれませんが、それは単純な統計作成の失敗である可能性が高いです。これは、たとえば、タスクが必要なメモリリソースを取得できない場合、または統計の作成が抑制されている場合(同時コンパイルが多すぎる場合)に発生する可能性があります。 Microsoftホワイトペーパー Microsoft SQL Server 2008のクエリオプティマイザーで使用される統計 を参照してください。 auto statsプロファイラーまたは拡張イベントと他のイベントを同時に見て、これをさらにデバッグできる場合があります。

とはいえ、プランの選択のせいで、欠落している一時テーブルの統計情報のドアを開くには、かなりのより多くの情報と調査が必要になります。詳細な統計がなくても、オプティマイザは一時テーブルの合計カーディナリティを確認できます。これは、ここでは重要な要素のようです。

...しかし、すぐに良い計画に再コンパイルできます。同じコンパイル済みパラメーター。

_@Months_パラメーターは同じである可能性がありますが、(不明なビュー_View_Feeder_からの)一時テーブルの行数が異なり、提供されたプランには_@MyId_の値が表示されません。

利用可能な情報から:「良い」計画(推定のみ、パフォーマンスデータは表示されません)は、4行を含む一時テーブルに基づいています。 「悪い計画」は、114行の一時テーブルに基づいています。密度とヒストグラムの情報がないことは確かに役に立たないかもしれませんが、密度と分布が不明なものであるにもかかわらず、オプティマイザが4行と114行に対して異なるプランをどのように選択するかは簡単にわかります。

一時テーブルに依存しない計画演算子の推定が大幅にずれている場合、これは、現在のメインテーブル統計が基礎となるデータを表していないことを示す強力なシグナルです。問題に情報がないため、これを評価することは不可能です。

それにもかかわらず、オプティマイザがここで準最適の選択肢から選択するように求められているのを見ることができます。 lookups(「カバリング」インデックスがない)とレイトフィルタリング(次を参照)が関係するため、どちらのプランも「明らかに良い」選択を表すものではありません。特にルックアップにはコストがかかります。これは、カーディナリティの見積もりに敏感に依存します。

viewを使用すると、オプティマイザとヒントの選択が制限されます。

  • この非常に特殊なケースでは変換が有効であっても、ビューには_GROUP BY_が含まれているため、述語vp.Col3 > DATEADD(MONTH, @Months * -1, GETDATE())がプッシュダウンされません。
    • ビューをクエリにインライン化することで、日付/時刻列をより早くフィルタリングする自然な方法が提供されます(ただし、質問では、クエリのリファクタリングがオプションかどうかは述べられていません)。
  • ビューのインデックスにヒントを与えることは不可能であり、FORCESEEKはオプティマイザにanyインデックス検索プランを見つけるように要求するだけです(必ずしもインデックスを使用する必要はありません)好む)。ビューを削除すると、同様にこの制限が削除されます。

述語をプッシュダウンできるようにすると、大きなテーブルでもインデックス作成の機会が開かれます。例えば:

_CREATE INDEX give_me_a_good_name
ON dbo.LargeTable (Col1, [Status], Col4) 
INCLUDE (P_Id);
_

...書き換えられたクエリに適切なアクセスパスを提供します。

_DECLARE @Date datetime = DATEADD(MONTH, @Months * -1, GETDATE());

SELECT
    MT.Col1,
    ST.Col2,
    MAX(LT.Col4)
FROM #MyTemp AS MT
JOIN dbo.LargeTable AS LT
    ON LT.Col1 = MT.Col1
JOIN dbo.SmallTable AS ST
    ON ST.P_id = LT.P_Id
WHERE
    LT.[Status] IN (3, 4)
    AND LT.Col4 > @Date
GROUP BY
    MT.Col1,
    ST.Col2
OPTION (RECOMPILE);
_

Example plan

もう1つの考慮事項は、私の記事 ストアドプロシージャの一時テーブル および 一時テーブルキャッシュの説明 で説明されている一時テーブルと統計キャッシュの影響です。適切な計画が一時オブジェクトのcurrentコンテンツ、メインクエリの前の明示的な_UPDATE STATISTICS #MyTemp;_、およびOPTION (RECOMPILE)メインクエリへの良い解決策かもしれません。

または、特定のプラン形状がこのクエリに対してalways最適である場合、さまざまなヒント、プランガイド、クエリストアなど、多くのオプションが利用可能です強制を計画します。カーディナリティが低い場合に有利であり、統計情報を提供しない(または依存しない)ため、一時テーブルの代わりにテーブル変数を使用することをお勧めします。

要約すると、一時テーブルで統計が欠落する(その影響の)理由を心配する前に、いくつかの一般的な改善を行う必要があります。

  • 統計がオプティマイザにとって代表的で有用であることを確認します
  • パラメータ値の範囲について、実績と推定値を確認します
  • 既存のインデックスを改善して、クエリに適切なデータアクセスパスを提供する
  • 可能であればビューを削除します。または、日付/時刻パラメーターの明示的な述語を持つ「パラメーター化されたビュー」(インラインテーブル値関数)を検討してください。
  • 自動統計作成が不必要に抑制されていないことを確認します
  • タスクに適切な種類の一時オブジェクトを使用する(テーブルと変数)
  • プランの選択がパラメーター値に非常に敏感な場合は、RECOMPILEを検討してください
  • キャッシュされた統計情報に問題がある場合は、_UPDATE STATISTICS_およびRECOMPILEを追加します
  • オプティマイザに有用な情報を提供する場合は、_SELECT INTO_ではなく主キーを持つ一時テーブルを検討してください。
  • スキーマを見直して、オプティマイザが可能な限り多くの情報(外部キー、その他の制約など)を持っていることを確認します
  • データの知識に基づいて、フィルターされたインデックス/統計の適合性を検討します
  • パフォーマンスを向上させるためにNOLOCKヒントをふりかけないでください

再現

以下は、提供された編集済みの実行プランで利用可能な限られた情報から作成されました。

_DROP VIEW IF EXISTS dbo.View_VP;
DROP TABLE IF EXISTS dbo.SmallTable, dbo.LargeTable, #MyTemp;
GO
CREATE TABLE LargeTable (P_Id integer NOT NULL, Status integer NOT NULL, Col1 integer NOT NULL, Col4 datetime NOT NULL);
CREATE TABLE SmallTable (P_id integer NOT NULL, Col2 integer NOT NULL)
CREATE TABLE #MyTemp (Col1 integer NOT NULL);
GO
CREATE VIEW dbo.View_VP 
AS
    SELECT
        pp.Col1,
        pd.Col2 AS Col2,
        MAX(pp.Col4) AS Col3
    FROM LargeTable pp
    JOIN SmallTable pd
        ON pd.P_id = pp.P_Id
    WHERE 
        pp.[Status] IN (3, 4)
    GROUP BY 
        pp.Col1, pd.Col2;
GO
CREATE UNIQUE CLUSTERED INDEX PK_SmallTable ON dbo.SmallTable (P_id)
CREATE CLUSTERED INDEX ix_P_id ON dbo.LargeTable (P_Id)
CREATE INDEX ix_Col1 ON dbo.LargeTable (Col1)
CREATE INDEX ix_Status ON dbo.LargeTable ([Status])
GO
UPDATE STATISTICS dbo.LargeTable WITH ROWCOUNT = 32268200, PAGECOUNT = 322682;
UPDATE STATISTICS dbo.SmallTable WITH ROWCOUNT = 6349, PAGECOUNT = 63;
UPDATE STATISTICS #MyTemp WITH ROWCOUNT = 4;
_

クエリは次のとおりです。

_DECLARE @Months integer = 6;

SELECT wd.Col1
         , vp.Col2
         , vp.Col3 
    FROM dbo.View_VP vp WITH (FORCESEEK)
    INNER JOIN #MyTemp wd ON wd.Col1 = vp.Col1
    WHERE vp.Col3 > DATEADD(MONTH, @Months * -1, GETDATE())
_

ベーステーブルの実際の統計がない場合、これは「悪い計画」の例に近い計画を優先します(_ix_Status_を使用):

Demo plan

これは、_Col1_の選択性に関する情報がオプティマイザの選択における重要な要素であることを示唆しています。

12
Paul White 9