インデックスに対してDateAdd()によって制約されたビューの1行の推定を改善する方法


8

Microsoft SQL Server 2012(SP3)(KB3072779)-11.0.6020.0(X64)の使用。

与えられたテーブルとインデックス:

create table [User].[Session] 
(
  SessionId int identity(1, 1) not null primary key
  CreatedUtc datetime2(7) not null default sysutcdatetime())
)

create nonclustered index [IX_User_Session_CreatedUtc]
on [User].[Session]([CreatedUtc]) include (SessionId)

次の各クエリの実際の行は310万です。推定行はコメントとして表示されます。

これらのクエリがView内の別のクエリフィードすると、1行の推定値のため、オプティマイザはループ結合を選択します。 親クエリの結合ヒントを上書きしたり、SPに頼ったりしないように、この基本レベルでの見積もりを改善するにはどうすればよいですか

ハードコードされた日付を使用するとうまくいきます:

 select distinct SessionId from [User].Session -- 2.9M (great)
  where CreatedUtc > '04/08/2015'  -- but hardcoded

これらの同等のクエリはビューと互換性がありますが、すべて1行と推定されます。

select distinct SessionId from [User].Session -- 1
 where CreatedUtc > dateadd(day, -365, sysutcdatetime())         

select distinct SessionId from [User].Session  -- 1
 where dateadd(day, 365, CreatedUtc) > sysutcdatetime();          

select distinct SessionId from [User].Session s  -- 1
 inner loop join  (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
    on d.MinCreatedUtc < s.CreatedUtc    
    -- (also tried reversing join order, not shown, no change)

select distinct SessionId from [User].Session s -- 1
 cross apply (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 where d.MinCreatedUtc < s.CreatedUtc
    -- (also tried reversing join order, not shown, no change)

いくつかのヒントを試してみてください(ただし、ビューには該当しません)。

 select distinct SessionId from [User].Session -- 1
  where CreatedUtc > dateadd(day, -365, sysutcdatetime())
 option (recompile);

select distinct SessionId from [User].Session  -- 1
 where CreatedUtc > (select dateadd(day, -365, sysutcdatetime()))
 option (recompile, optimize for unknown);

select distinct SessionId                     -- 1
  from (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 inner loop join [User].Session s    
    on s.CreatedUtc > d.MinCreatedUtc  
option (recompile);

パラメータ/ヒントを使用してみてください(ただし、表示はN / A):

declare
    @minDate datetime2(7) = dateadd(day, -365, sysutcdatetime());

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate;

select distinct SessionId from [User].Session  -- 2.96M (great)
 where CreatedUtc > @minDate
option (recompile);

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate
option (optimize for unknown);

見積もり対実際

統計は最新です。

DBCC SHOW_STATISTICS('user.Session', 'IX_User_Session_CreatedUtc') with histogram;

ヒストグラムの最後の数行(合計189行)が表示されます。

ここに画像の説明を入力してください

回答:


6

Aaronの回答よりも包括的ではありませんが、中心的な問題はDATEADDdatetime2タイプを使用する場合のカーディナリティ推定バグです。

接続:sysdatetimeがdateadd()式にある場合の推定が正しくない

回避策の1つは、GETUTCDATE(日時を返す)を使用することです。

WHERE CreatedUtc > CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()))

バグを回避するには、datetime2への変換がの外でなければならないことに注意してくださいDATEADD

1行のカーディナリティ推定の問題は、70モデルのカーディナリティエスティメータが使用される2016 RC0までのすべてのバージョンのSQL Serverで再現されます。

Aaron Bertrandが、SQLPerformance.comについてこれに関する記事を書いています。


6

一部のシナリオでは、引数が何であるか、および実際のデータがどのように見えるかに応じて、SQL ServerはDATEADD/ に対して非常に乱暴な見積もりをDATEDIFF行うことがあります。これについてDATEDIFFは、月の初めといくつかの回避策を扱うときにここに書きました:

しかし、私の典型的なアドバイスはちょうど停止使用してすることですDATEADD/ DATEDIFF/結合句をどこに。

次のアプローチは、うるう年がフィルターされた範囲にある場合は正確ではありませんが(その場合は追加の日が含まれます)、その日に丸められますが、次のように推定が向上します(ただし、それほど優れていません!)DATEDIFF列アプローチに対して引数なしで、シークを使用できるようにします。

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  DAY(GETUTCDATE())
);

SELECT ... WHERE CreatedUtc >= @start;

入力を操作しDATEFROMPARTSてうるう日の問題を回避しDATETIMEFROMPARTSたり、日付に丸める代わりに精度を高めたりすることができます。これは、使用せずに過去の日付を変数に入力できることを示すだけですDATEADD(それは単なるもう少し作業)、したがって、推定バグのより不自由な部分を回避します(2014年以降に修正されています)。

うるう日のエラーを回避するために、昨年の2月28日から29ではなく、代わりにこれを行うことができます。

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  CASE WHEN DAY(GETUTCDATE()) = 29 AND MONTH(GETUTCDATE()) = 2 
    THEN 28 ELSE DAY(GETUTCDATE()) END
);

また、今年のうるう日を過ぎているかどうかを確認して1日を追加することもできます。1日を過ぎている場合は、最初に1日を追加しDATEADD ます(興味深いことに、ここで使用しも正確な見積もりが可能です)。

DECLARE @base date = GETUTCDATE();
IF GETUTCDATE() >= DATEFROMPARTS(YEAR(GETUTCDATE()),3,1) AND 
  TRY_CONVERT(datetime, DATEFROMPARTS(YEAR(GETUTCDATE()),2,29)) IS NOT NULL
BEGIN
  SET @base = DATEADD(DAY, 1, GETUTCDATE());
END

DECLARE @start date = DATEFROMPARTS
(
  YEAR(@base)-1, 
  MONTH(@base),
  CASE WHEN DAY(@base) = 29 AND MONTH(@base) = 2 
    THEN 28 ELSE DAY(@base) END
);

SELECT ... WHERE CreatedUtc >= @start;

真夜中の日よりも正確である必要がある場合は、selectの前にさらに操作を追加できます。

DECLARE @accurate_start datetime2(7) = DATETIME2FROMPARTS
(
  YEAR(@start), MONTH(@start), DAY(@start),
  DATEPART(HOUR,  SYSUTCDATETIME()), 
  DATEPART(MINUTE,SYSUTCDATETIME()),
  DATEPART(SECOND,SYSUTCDATETIME()), 
  0,0
);

SELECT ... WHERE CreatedUtc >= @accurate_start;

これで、ビュー内でこれをすべて詰め込むことができ、ヒントやトレースフラグを必要とせずに、シークと30%の見積もりを使用できますが、かなり美しくありません。ネストされたCTEはSYSUTCDATETIME()、100回入力したり、再利用された式を繰り返す必要がないようにするためのものです。それでも、複数回評価することができます。

CREATE VIEW dbo.v5 
AS
  WITH d(d) AS ( SELECT SYSUTCDATETIME() ),
  base(d) AS
  (
    SELECT DATEADD(DAY,CASE WHEN d >= DATEFROMPARTS(YEAR(d),3,1) 
      AND TRY_CONVERT(datetime,RTRIM(YEAR(d))+RIGHT('0'+RTRIM(MONTH(d)),2)
      +RIGHT('0'+RTRIM(DAY(d)),2)) IS NOT NULL THEN 1 ELSE 0 END, d)
    FROM d
  ),
  src(d) AS
  (
    SELECT DATETIME2FROMPARTS
    (
      YEAR(d)-1, 
      MONTH(d),
      CASE WHEN MONTH(d) = 2 AND DAY(d) = 29
        THEN 28 ELSE DAY(d) END,
      DATEPART(HOUR,d), 
      DATEPART(MINUTE,d),
      DATEPART(SECOND,d),
      10*DATEPART(MICROSECOND,d),
      7
    ) FROM base
  )
  SELECT DISTINCT SessionId FROM [User].[Session]
    WHERE CreatedUtc >= (SELECT d FROM src);

これは、DATEDIFF列に対してあなたよりもはるかに冗長ですが、コメントで述べたよう、そのアプローチは議論の余地がなく、おそらくほとんどのテーブルを読み取る必要がありますが、おそらく競争的に機能しますが、負担になると思います「昨年」はテーブルの低いパーセンテージになります。

また、参考までに、再現しようとしたときに取得したメトリックの一部を以下に示します。

ここに画像の説明を入力してください

1行の推定値を取得できなかったため、分布を一致させるように一生懸命努力しました(313万行、昨年から289万行)。しかし、あなたは見ることができます:

  • どちらのソリューションもほぼ同等の読み取りを実行します。
  • 日の境界のみを考慮しているため、ソリューションの精度は少し低くなっています(それでも問題ないかもしれませんが、私のビューは一致する精度が低くなる可能性があります)。
  • 4199 +再コンパイルは、見積もり(または計画)を実際に変更しませんでした。

期間の数値からあまり多くを引き出さないでください。現在は近いですが、テーブルが大きくなるにつれて近くにとどまらない場合があります(ここでも、シークでもテーブルのほとんどを読み取る必要があるためです)。

以下は、v4(列に対する日付差分)およびv5(マイバージョン)の計画です。

ここに画像の説明を入力してください

ここに画像の説明を入力してください


要約すると、ブログに記載されています。この回答は、使用可能な見積もりとシークベースの計画を提供します。@PaulWhiteの答えが最良の見積もりです。おそらく、1行の見積もり(1500に対して)は、過去24時間以内にテーブルに行がなかったことが原因である可能性があります。
crokusek

@crokusekあなた>= DATEADD(DAY, -365, SYSDATETIME())がバグを言うなら、推定はに基づいているということ>= SYSDATETIME()です。したがって、技術的には、推定値はテーブルCreatedUtc内の将来の行数に基づいています。これはおそらく0ですが、SQL Serverは推定行に対して常に0を1に切り上げます。
アーロンバートランド

1

dateadd()をdatediff()に置き換えて、適切な概算(30%ish)を取得します。

 select distinct SessionId from [User].Session     -- 1.2M est, 3.0M act.
  where datediff(day, CreatedUtc, sysutcdatetime()) <= 365

これは、MS Connect 630583と同様のバグのようです

オプションを再コンパイルしても違いはありません。

計画統計


2
列にdatediffを適用すると、式が検索不可能になるため、スキャンする必要があります。いずれにしても、テーブルの90 +%を読み取る必要がある場合はおそらく問題ありませんが、テーブルが大きくなると、コストが高くなります。
アーロンバートランド

素晴らしい点。内部で変換できると思っていました。スキャンを実行していることを確認した。
crokusek
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.