ストアドプロシージャを使用して日付範囲の毎日の行を作成する方法


11

特定の日付範囲の毎日のテーブルに行を作成するストアドプロシージャを作成したいと思います。ストアドプロシージャは、2つの入力(ユーザーが希望する日付範囲の開始日と終了日)を受け入れます。

それで、私がそのようなテーブルを持っているとしましょう:

SELECT Day, Currency
FROM ConversionTable

DayはDateTimeであり、Currencyは単なる整数です。

話を簡単にするために、これらの挿入された行ごとにCurrency列を常に1にしたいとします。したがって、誰かが「2017年3月5日」を開始日として入力し、「2017年4月11日」を終了日として入力した場合、次の行を作成します。

2017-03-05 00:00:00, 1
2017-03-06 00:00:00, 1
...
2017-04-11 00:00:00, 1

これを行うためにストアドプロシージャをコーディングする最良の方法は何ですか?私のテスト環境ではSQL Server 2008 R2を使用していますが、実際の環境ではSQL Server 2012を使用しているため、このタスクを容易にする新しい機能が2012年に導入された場合、テストマシンをアップグレードできます。

回答:


15

1つのオプションは再帰CTEです。

DECLARE @StartDate datetime = '2017-03-05'
       ,@EndDate   datetime = '2017-04-11'
;

WITH theDates AS
     (SELECT @StartDate as theDate
      UNION ALL
      SELECT DATEADD(day, 1, theDate)
        FROM theDates
       WHERE DATEADD(day, 1, theDate) <= @EndDate
     )
SELECT theDate, 1 as theValue
  FROM theDates
OPTION (MAXRECURSION 0)
;

MAXRECURSIONヒントは、以下のScott Hodginのコメントのおかげで追加されました。)


ああ、そう!「数値」テーブルアプローチを検討しました(Scott Hodginの回答)。非常に長い日数の場合、パフォーマンスが向上する可能性があります。そして、再利用可能な優れたアプローチ(John Cの答え)は常に優れています。これは私が考えることができる最も単純で最も簡単な答えでした。
RDFozz 2017

1
覚えておいてください-MAXRECURSIONを指定しない場合、再帰はデフォルトで100に制限されます-より広い日付範囲が渡されたときにSPが暴走するのは嫌です:)
Scott Hodgin

2
@Scott Hodginそうですね。MAXRECURSION解決するヒントをクエリに追加しました。
RDFozz 2017

このメソッドには、EndDate値の次の日に時間を含めるという意図的または意図的でない動作があります。この動作が望ましくない場合は、DATEADDに(DAY、1、theDate)<@EndDate変更
クリス・ポーター

1
@ChrisPorter-素晴らしい点!ただし、これを行うとDATEADD(DAY, 1, theDate) < @EndDate、両方の日時値が同じ時間コンポーネントを持つ場合、範囲の終わりを取得できません。私は答えを適切に修正しましたが、を使用しました<= @EndDate。とにかく範囲の終わりの値を含めたくない場合< @EndDateは、実際に正しいでしょう。
RDFozz

6

別のオプションは、テーブル値関数を使用することです。このアプローチは非常に高速で、もう少し柔軟性があります。日付/時間範囲、DatePart、および増分を指定します。また、CROSS APPLYに含める利点もあります。

例えば

Select * from [dbo].[udf-Range-Date]('2017-03-05','2017-04-11','DD',1) 

戻り値

RetSeq  RetVal
1   2017-03-05 00:00:00.000
2   2017-03-06 00:00:00.000
3   2017-03-07 00:00:00.000
4   2017-03-08 00:00:00.000
5   2017-03-09 00:00:00.000
...
36  2017-04-09 00:00:00.000
37  2017-04-10 00:00:00.000
38  2017-04-11 00:00:00.000

興味があればUDF

CREATE FUNCTION [dbo].[udf-Range-Date] (@R1 datetime,@R2 datetime,@Part varchar(10),@Incr int)
Returns Table
Return (
    with cte0(M)   As (Select 1+Case @Part When 'YY' then DateDiff(YY,@R1,@R2)/@Incr When 'QQ' then DateDiff(QQ,@R1,@R2)/@Incr When 'MM' then DateDiff(MM,@R1,@R2)/@Incr When 'WK' then DateDiff(WK,@R1,@R2)/@Incr When 'DD' then DateDiff(DD,@R1,@R2)/@Incr When 'HH' then DateDiff(HH,@R1,@R2)/@Incr When 'MI' then DateDiff(MI,@R1,@R2)/@Incr When 'SS' then DateDiff(SS,@R1,@R2)/@Incr End),
         cte1(N)   As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
         cte2(N)   As (Select Top (Select M from cte0) Row_Number() over (Order By (Select NULL)) From cte1 a, cte1 b, cte1 c, cte1 d, cte1 e, cte1 f, cte1 g, cte1 h ),
         cte3(N,D) As (Select 0,@R1 Union All Select N,Case @Part When 'YY' then DateAdd(YY, N*@Incr, @R1) When 'QQ' then DateAdd(QQ, N*@Incr, @R1) When 'MM' then DateAdd(MM, N*@Incr, @R1) When 'WK' then DateAdd(WK, N*@Incr, @R1) When 'DD' then DateAdd(DD, N*@Incr, @R1) When 'HH' then DateAdd(HH, N*@Incr, @R1) When 'MI' then DateAdd(MI, N*@Incr, @R1) When 'SS' then DateAdd(SS, N*@Incr, @R1) End From cte2 )

    Select RetSeq = N+1
          ,RetVal = D 
     From  cte3,cte0 
     Where D<=@R2
)
/*
Max 100 million observations -- Date Parts YY QQ MM WK DD HH MI SS
Syntax:
Select * from [dbo].[udf-Range-Date]('2016-10-01','2020-10-01','YY',1) 
Select * from [dbo].[udf-Range-Date]('2016-01-01','2017-01-01','MM',1) 
*/

@RobV非常に真実です。ずっと前に学びましたが、本当の答えは決してありません。
ジョンカペレッティ2017

返信いただきありがとうございます。これを行うにはいくつかの方法があるように見えます:)他の男は、提起された望ましい出力を正確に解決する方法で私の質問に答えましたが、あなたの言うとおり、より柔軟なようです。
Rob V

@JohnCappellettiこれは私がする必要があることには完璧ですが、週末と休日を削除するためにそれが必要でした...私は最後の行を次のように変更してこれを行いましたD <= @ R2 AND DATEPART(Weekday、D)not in (1,7)AND D NOT IN(SELECT Holiday_Date FROM Holidays)。Holidaysは、休日の日付を含むテーブルです。
MattE

@MattEよくやった!週末と休日を除外するために、私は同じことをしたでしょう。:)
ジョンカッペレッティ

3

例として日付ディメンションテーブルを作成する方法に関するアーロンバートランドの投稿を使用して、私はこれを思いつきました:

DECLARE @StartDate DATE ='2017-03-05 00:00:00'
DECLARE @EndDate DATE ='2017-04-11 00:00:00'

Declare @DateTable table ([date]       DATE PRIMARY KEY);

-- use the catalog views to generate as many rows as we need

INSERT @DateTable ([date])
SELECT d
FROM (
    SELECT d = DATEADD(DAY, rn - 1, @StartDate)
    FROM (
        SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate)) rn = ROW_NUMBER() OVER (
                ORDER BY s1.[object_id]
                )
        FROM sys.all_objects AS s1
        CROSS JOIN sys.all_objects AS s2
        -- on my system this would support > 5 million days
        ORDER BY s1.[object_id]
        ) AS x
    ) AS y;

SELECT *
FROM @DateTable
ORDER BY [date]

このタイプのロジックをストアード・プロシージャーに入れて、必要なものを何でも追加できるはずです。


2

Redshiftでは最近、同様の問題を解決する必要がありました。読み取りアクセスしかできなかったため、結果セットの開始点として、日付範囲の1時間ごとに行を取得する純粋なSQLベースのソリューション(ストアドプロシージャなし)が必要でした。他の人がこれをよりエレガントにして目的に合わせて変更できると確信していますが、必要な人のために、これが私のハッキングされた解決策です:

with hours as
   (select 0 clockhour union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9 union select 10 union select 11 union select 12 
    union select 13 union select 14 union select 15 union select 16 union select 17 union select 18 union select 19 union select 20 union select 21 union select 22 union select 23)
, days as
   (select *
    from 
       (select to_number(n0.number || n1.number, '99') daynum
        from
           (select 0 as number union select 1 union select 2 union select 3) as n0
           cross join
           (select 1 as number union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9 union select 0) as n1)
    where daynum between 1 and 31)
, months as
   (select 1 as monthnum, 'jan' as themonth, 31 as numdays union select 2, 'feb', 28 union select 3, 'mar', 31 union select 4, 'apr', 30 union select 5, 'may', 31 union select 6, 'jun', 30 
    union select 7, 'jul', 31 union select 8, 'aug', 31 union select 9, 'sep', 30 union select 10, 'oct', 31 union select 11, 'nov', 30 union select 12, 'dec', 31)
, years as
   (select century || decade || yr as yr
    from 
       (select 19 century union select 20) 
    cross join
       (select 0 decade union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) 
    cross join
       (select 0 yr union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9))
select cast(daynum || '-' || themonth || '-' || yr || ' ' || clockhour || ':00:00' as timestamp) dayhour
from hours
cross join days
cross join months
cross join years
where cast(daynum || '-' || themonth || '-' || yr || ' ' || clockhour || ':00:00' as timestamp) 
between date_trunc('month', dateadd('month', -$MONTHS_AGO, getdate()))
and     date_trunc('month', dateadd('month', $MONTHS_AHEAD, getdate()))
and   daynum <= numdays
order by yr, monthnum, daynum, clockhour;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.