このシリーズの前半(パート1 |パート2)では、さまざまな手法を使用して一連の数値を生成する方法について説明しました。興味深く、いくつかのシナリオでは便利ですが、より実用的なアプリケーションは、一連の連続した日付を生成することです。たとえば、一部の日にはトランザクションがなかった場合でも、月のすべての日を表示する必要があるレポート。
以前の投稿で、一連の数値から一連の日数を導き出すのは簡単だと述べました。一連の数値を導出するための複数の方法をすでに確立しているので、次のステップがどのように見えるかを見てみましょう。非常に簡単に始めて、1月1日から1月3日までの3日間レポートを実行し、毎日の行を含めたいとしましょう。昔ながらの方法は、#tempテーブルを作成し、ループを作成し、現在の日を保持する変数を作成し、ループ内で範囲の最後まで#tempテーブルに行を挿入してから#を使用することです。ソースデータへの外部結合への一時テーブル。これは、私がここで提示したいよりも多くのコードです。本番環境に移行し、保守し、同僚に学んでもらうことを気にしないでください。
簡単に始める
(選択した方法に関係なく)確立された数列を使用すると、このタスクははるかに簡単になります。この例では、3日しか必要ないため、複雑なシーケンスジェネレーターを非常に単純なユニオンに置き換えることができます。このセットに4行を含めるので、必要なシリーズに正確に切り取る方法を簡単に示すことができます。
まず、関心のある範囲の開始と終了を保持するための変数がいくつかあります。
DECLARE @s DATE = '2012-01-01', @e DATE = '2012-01-03';
さて、単純な直列ジェネレーターだけから始めると、次のようになります。 ORDER BY
を追加します ここでも、安全のために、注文についての仮定に頼ることはできません。
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT n FROM n ORDER BY n; -- result: n ---- 1 2 3 4
これを一連の日付に変換するには、DATEADD()
を適用するだけです。 開始日から:
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT DATEADD(DAY, n, @s) FROM n ORDER BY n; -- result: ---- 2012-01-02 2012-01-03 2012-01-04 2012-01-05
私たちの範囲は1日ではなく2日から始まるので、これはまだ完全には正しくありません。したがって、開始日をベースとして使用するには、セットを1ベースから0ベースに変換する必要があります。 1を引くことでそれを行うことができます:
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT DATEADD(DAY, n-1, @s) FROM n ORDER BY n; -- result: ---- 2012-01-01 2012-01-02 2012-01-03 2012-01-04
もうすぐです!より大きなシリーズソースからの結果を制限する必要があります。これは、DATEDIFF
にフィードすることで実行できます。 、日数で、範囲の開始と終了の間、TOP
演算子–次に1を追加します(DATEDIFF
以降) 基本的に、制限のない範囲を報告します。
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM n ORDER BY n; -- result: ---- 2012-01-01 2012-01-02 2012-01-03
実際のデータの追加
これで、別のテーブルに対して結合してレポートを導出する方法を確認するために、新しいクエリと外部結合をソースデータに対して使用することができます。
;WITH n(n) AS ( SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 ), d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM n ORDER BY n ) SELECT d.OrderDate, OrderCount = COUNT(o.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeader AS o ON o.OrderDate >= d.OrderDate AND o.OrderDate < DATEADD(DAY, 1, d.OrderDate) GROUP BY d.OrderDate ORDER BY d.OrderDate;
(COUNT(*)
とは言えなくなったことに注意してください 、これは左側をカウントするため、常に1になります。)
これを書く別の方法は次のようになります:
;WITH d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM ( SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 ) AS n(n) ORDER BY n ) SELECT d.OrderDate, OrderCount = COUNT(o.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeader AS o ON o.OrderDate >= d.OrderDate AND o.OrderDate < DATEADD(DAY, 1, d.OrderDate) GROUP BY d.OrderDate ORDER BY d.OrderDate;
これにより、主要なCTEを、選択した任意のソースからの日付シーケンスの生成に置き換える方法を簡単に想像できるようになります。 AdventureWorks2012を使用して、これらを(グラフを歪めるためだけに機能する再帰CTEアプローチを除いて)実行しますが、SalesOrderHeaderEnlarged
を使用します。 このスクリプトからJonathanKehayiasが作成したテーブル。この特定のクエリに役立つインデックスを追加しました:
CREATE INDEX d_so ON Sales.SalesOrderHeaderEnlarged(OrderDate);
また、テーブルに存在することがわかっている任意の日付範囲を選択していることにも注意してください。
数値表
;WITH d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM dbo.Numbers ORDER BY n ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND CONVERT(DATE, s.OrderDate) = d.OrderDate WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
計画(クリックして拡大):
spt_values
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; ;WITH d(OrderDate) AS ( SELECT DATEADD(DAY, n-1, @s) FROM (SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) ROW_NUMBER() OVER (ORDER BY Number) FROM master..spt_values) AS x(n) ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND CONVERT(DATE, s.OrderDate) = d.OrderDate WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
計画(クリックして拡大):
sys.all_objects
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; ;WITH d(OrderDate) AS ( SELECT DATEADD(DAY, n-1, @s) FROM (SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) ROW_NUMBER() OVER (ORDER BY [object_id]) FROM sys.all_objects) AS x(n) ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND CONVERT(DATE, s.OrderDate) = d.OrderDate WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
計画(クリックして拡大):
スタックCTE
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; ;WITH e1(n) AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ), e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b), d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY n)-1, @s) FROM e2 ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND d.OrderDate = CONVERT(DATE, s.OrderDate) WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
計画(クリックして拡大):
さて、1年間の長距離では、100行しか生成されないため、これで削減されることはありません。 1年間、(うるう年の可能性を考慮して)366行をカバーする必要があるため、次のようになります。
DECLARE @s DATE = '2006-10-23', @e DATE = '2007-10-22'; ;WITH e1(n) AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ), e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b), e3(n) AS (SELECT 1 FROM e2 CROSS JOIN (SELECT TOP (37) n FROM e2) AS b), d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY N)-1, @s) FROM e3 ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND d.OrderDate = CONVERT(DATE, s.OrderDate) WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
計画(クリックして拡大):
カレンダーテーブル
これは、前の2つの投稿ではあまり話さなかった新しいものです。多くのクエリに日付系列を使用している場合は、数値テーブルとカレンダーテーブルの両方を使用することを検討する必要があります。同じ議論は、実際に必要なスペースの量と、テーブルが頻繁に照会されたときにアクセスがどれだけ速くなるかについても当てはまります。たとえば、30年の日付を格納するには、11,000行未満(正確な数はうるう年の数によって異なります)が必要であり、使用するのはわずか200KBです。はい、あなたはその権利を読んでいます:200キロバイト 。 (圧縮すると、わずか136 KBです。)
30年のデータを含むカレンダーテーブルを生成するには、数値テーブルを持つことが良いことであるとすでに確信していると仮定して、これを行うことができます。
DECLARE @s DATE = '2005-07-01'; -- earliest year in SalesOrderHeader DECLARE @e DATE = DATEADD(DAY, -1, DATEADD(YEAR, 30, @s)); SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) d = CONVERT(DATE, DATEADD(DAY, n-1, @s)) INTO dbo.Calendar FROM dbo.Numbers ORDER BY n; CREATE UNIQUE CLUSTERED INDEX d ON dbo.Calendar(d);に作成します。
これで、売上レポートクエリでそのカレンダーテーブルを使用するために、はるかに簡単なクエリを作成できます。
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; SELECT OrderDate = c.d, OrderCount = COUNT(s.SalesOrderID) FROM dbo.Calendar AS c LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND c.d = CONVERT(DATE, s.OrderDate) WHERE c.d >= @s AND c.d <= @e GROUP BY c.d ORDER BY c.d;
計画(クリックして拡大):
パフォーマンス
NumbersテーブルとCalendarテーブルの圧縮コピーと非圧縮コピーの両方を作成し、1週間の範囲、1か月の範囲、および1年の範囲をテストしました。コールドキャッシュとウォームキャッシュを使用してクエリも実行しましたが、それはほとんど重要ではないことがわかりました。
1週間の範囲を生成するためのミリ秒単位の期間
1か月の範囲を生成するためのミリ秒単位の期間
1年間の範囲を生成するためのミリ秒単位の期間
補遺
Paul White(ブログ| @SQL_Kiwi)は、次のクエリを使用して、Numbersテーブルを強制してはるかに効率的な計画を作成できると指摘しました。
SELECT OrderDate = DATEADD(DAY, n, 0), OrderCount = COUNT(s.SalesOrderID) FROM dbo.Numbers AS n LEFT OUTER JOIN Sales.SalesOrderHeader AS s ON s.OrderDate >= CONVERT(DATETIME, @s) AND s.OrderDate < DATEADD(DAY, 1, CONVERT(DATETIME, @e)) AND DATEDIFF(DAY, 0, OrderDate) = n WHERE n.n >= DATEDIFF(DAY, 0, @s) AND n.n <= DATEDIFF(DAY, 0, @e) GROUP BY n ORDER BY n;
この時点で、すべてのパフォーマンステストを再実行するつもりはありませんが(読者のために練習してください!)、より良いまたは同様のタイミングが生成されると想定します。それでも、カレンダーテーブルは、厳密には必要ではない場合でも、持っておくと便利だと思います。
結論
結果はそれ自体を物語っています。一連の数値を生成する場合、Numbersテーブルのアプローチが優先されますが、1,000,000行であってもごくわずかです。そして、一連の日付については、下端では、さまざまな手法の間に大きな違いは見られません。ただし、特に大きなソーステーブルを処理している場合は、日付範囲が大きくなるにつれて、カレンダーテーブルは、特にメモリフットプリントが小さいことを考えると、その価値を実際に示していることは明らかです。カナダの奇抜なメートル法でも、ディスク上で200 KBしか発生しなかった場合、60ミリ秒は約10*秒*よりもはるかに優れています。
この小さなシリーズを楽しんでいただけたでしょうか。それは私が何年もの間再訪することを意味していたトピックです。
[パート1|パート2|パート3]