ストアドプロシージャクエリは、テーブルの1つで統計が更新された後、不適切なプランを取得することがありますが、すぐに適切なプランに再コンパイルできます。同じコンパイル済みパラメーター。
問題は、SPで作成されてから結合された小さな一時テーブルにあると思われます。悪い計画では、一時テーブルに対して、結合列に統計がないという警告があります。何が原因ですか?
SQL Server 2016 SP1 CU4、2014互換性レベル
悪い計画:
良い計画:
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番目のセッションで再コンパイルしても、効果はありませんでした。
悪い計画では、一時テーブルに対して、結合列に統計がないという警告があります。何ができますか?
これにはより難解な理由があるかもしれませんが、それは単純な統計作成の失敗である可能性が高いです。これは、たとえば、タスクが必要なメモリリソースを取得できない場合、または統計の作成が抑制されている場合(同時コンパイルが多すぎる場合)に発生する可能性があります。 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);
_
もう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
_を使用):
これは、_Col1
_の選択性に関する情報がオプティマイザの選択における重要な要素であることを示唆しています。