web-dev-qa-db-ja.com

日付やその間の内部結合のパフォーマンスが悪い

日付範囲を使用して内部結合を行うクエリの最適化に問題があります。クエリの目的は、毎日のデータを取得し、週ごとに要約することです。

Select  pcw.EndDate WeekEndDate, h.Store, SUM(h.DeliveryChargesTotal) DeliveryChargesTotal 
from Daily_GC_Headers h
inner join PeriodCalendar_Weeks pcw
on h.SalesDate between pcw.StartDate and pcw.EndDate
where SalesDate between @StartDate and @EndDate and isCanceled = 0 
group by pcw.EndDate, h.Store

Daily_GC_Headersテーブルの簡略化されたスキーマ(1380万行、WHERE句の約540万一致基準):

Store - Varchar(10) (PK)
SalesDate - Date (PK)
TicketNumber - SmallInt (PK; starts over 1 each day at each store.)
IsCanceled - Bit
DeliveryChargesTotal - Decimal(9,2)

PeriodCalendar_Weeksテーブルの簡略化されたスキーマ(570行、53が基準に一致):

Year - smallint (PK)
Period - tinyint (PK)
Week - tinyint (PK)
StartDate - Date    
EndDate - Date

このクエリは、SSMSで約15秒かかります。 Daily_GC_Headersを単独で(およびStoreでグループ化するだけで)クエリすると、2秒かかります。 PeriodCalendar_Weeksに対するクエリは「インスタント」です。

DBCC SHOW_STATISTICSは、統計が両方とも最新であることを示します(更新するために毎週ジョブを実行します)。プランのキャッシュをクリアしてみました。

実行計画がおかしい。たとえば、PeriodCalendar_WeeksでEager Spoolを実行しています。推定行は156.6ですが、実際の行は153,971です。次に、その最初のスプールの結果をフィルタリングし、レイジースプールを実行します。その2番目のスプールの推定/実際の行は5.4ミリオンです。ただし、基になるテーブルに600行未満の行があります

これを最適化するために何を探したり、何をすべきですか?

追加情報

明確にするために、私は最初、週の表で過度に単純化したPKについて説明しました。上記のスキーマを更新して、完全なキーを表示します。ヘッダーに記載されているPKは、完全なキーです(かつていました)。

Weeksテーブルの一部の行のスクリーンショット: enter image description here

週テーブルの統計: enter image description here

ヘッダーテーブルの一部の統計。表の履歴全体(3年)について、約5〜10日ごとのヒストグラムレコードがあるようです。 enter image description here

2
poke

これを行うには、前に戻って期間テーブルに結合する必要がないため、はるかに効率的です。

DECLARE @StartDate DATE, @EndDate DATE;

Select @StartDate = Min(StartDate), @EndDate = MAX(EndDate) 
from dbo.PeriodCalendar_Weeks pcw
where (pcw.Year = @Year and pcw.Period < @Period) 
  or  (pcw.Year = @Year and pcw.Period = @Period and pcw.Week <= @Week) 
  or (pcw.Year = @Year -1 and pcw.Period >= @Period);

SELECT 
  WeekEndDate = DATEADD(DAY, 6, DATEADD(WEEK, SalesWeek, @StartDate)), 
  Store, 
  DeliveryChargesTotal = dct
FROM 
(
  SELECT DATEDIFF(DAY, @StartDate, SalesDate)/7, Store, SUM(DeliveryChargesTotal)
  FROM dbo.Daily_GC_Headers
  WHERE SalesDate BETWEEN @StartDate AND @EndDate AND isCanceled = 0
  GROUP BY DATEDIFF(DAY, @StartDate, SalesDate)/7, Store
) AS x (SalesWeek, Store, dct)
ORDER BY WeekEndDate, Store;

フィルターされたインデックスは、isCanceled = 1Storeのカーディナリティに応じて、これらは考えられる提案であり、最適ではない場合があります):

CREATE INDEX x ON dbo.Daily_GC_Headers
  (SalesDate) INCLUDE (Store, DeliveryChargesTotal)
  WHERE isCanceled = 0;

行が非常に少ない場合、isCanceled = 1、これは良いかもしれません:

CREATE INDEX x ON dbo.Daily_GC_Headers
  (SalesDate, IsCanceled) INCLUDE (Store, DeliveryChargesTotal);

どちらもテストシステムで試してみる価値があり、どちらの場合もStoreをキーに移動するか、後者の場合はIsCanceledINCLUDEリストに移動します。私のシステムでは、INCLUDEリストの日付を除くすべてで最良の結果が見つかりました。

CREATE INDEX x ON dbo.Daily_GC_Headers
  (SalesDate) INCLUDE (Store, IsCanceled, DeliveryChargesTotal);

繰り返しますが、これらのいずれかが機能するかどうか、または上記のクエリがSQL Serverから直接異なる/より良い推奨を与えるかどうかをテストする必要があります。

2
Aaron Bertrand