如何使用存储过程为日期范围内的每一天创建一行?


11

我想创建一个存储过程,该过程将在给定日期范围内的每一天在表中创建一行。存储过程接受两个输入-用户所需日期范围的开始日期和结束日期。

因此,假设我有一个像这样的表:

SELECT Day, Currency
FROM ConversionTable

Day是DateTime,而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年引入了新功能,可以简化此任务,则可以升级测试计算机。

Answers:


15

一种选择是递归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

1
请记住-当您未指定MAXRECURSION时,递归的默认限制为100-我讨厌当传递更宽的日期范围时SP会爆炸:)
Scott Hodgin

2
@Scott Hodgin你是对的。MAXRECURSION向查询添加了提示以解决。
RDFozz

此方法具有包括第二天的EndDate值包括时间在内的有意或无意行为。如果此行为不受欢迎,请将WHERE更改为DATEADD(DAY,1,theDate)<@EndDate
Chris Porter

1
@ChrisPorter-非常好!但是,如果DATEADD(DAY, 1, theDate) < @EndDate这样做,则当两个datetime值具有相同的时间分量时,您不会到达范围的终点。我适当地修改了答案,但使用<= @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非常正确。很久以前了解到,从来没有一个真正的答案。
John Cappelletti

谢谢回复。看起来有几种方法可以执行此操作:)另一个人以精确解决所需的期望输出的方式回答了我的问题,但是您说的对,这似乎更灵活。
罗布五世

@JohnCappelletti这对我需要做的事情来说是完美的,但是我需要它来删除“周末和假期” ...我是通过将最后一行更改为以下内容来实现的,其中D <= @ R2 AND DATEPART(Weekday,D)不在(1,7)和D NOT IN(从Holidays中选择Holiday_Date)。假期是包含假期日期的表。
MattE

@MattE干得好!要排除周末和节假日,我会做同样的事情。:)
John Cappelletti

3

以亚伦·伯特兰德(Aaron Bertrand)关于如何创建日期维度表的帖子 为例,我想到了:

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上解决一个类似的问题,我只具有读取访问权限,因此需要一个纯粹基于SQL的解决方案(无存储过程)来在日期范围内每小时获取一行作为结果集的起点。我敢肯定,其他人可以使其更优雅,并针对其目的对其进行修改,但是对于有需要的人,这是我的hacked解决方案:

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;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.