Как создать строку для каждого дня в диапазоне дат, используя хранимую процедуру?

11

Я хотел бы создать хранимую процедуру, которая будет создавать строки в таблице для каждого дня в заданном диапазоне дат. Хранимая процедура принимает два входа: дату начала и дату окончания желаемого пользователем диапазона дат.

Итак, допустим, у меня есть таблица примерно так:

SELECT Day, Currency
FROM ConversionTable

Day - это DateTime, а Currency - целое число.

Для простоты, скажем так, я всегда хочу, чтобы столбец Currency был равен 1 для каждой из этих вставленных строк. Итак, если кто-то введет «5 марта 2017 года» в качестве даты начала и «11 апреля 2017 года» в качестве даты окончания, я хотел бы создать следующие строки:

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 году были введены новые функциональные возможности, облегчающие эту задачу.

Роб V
источник

Ответы:

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подсказка добавлена ​​благодаря комментарию Скотта Ходжина ниже.)

RDFozz
источник
О да! Я подумал о подходе к таблице «чисел» (ответ Скотта Ходжина). В течение очень большого количества дней это могло бы работать лучше. И хороший подход многократного использования (ответ Джона С.) всегда хорош. Это был просто самый простой и прямой ответ, который я мог придумать.
RDFozz
1
Просто помните - рекурсия имеет предельное значение по умолчанию 100, когда вы не указываете MAXRECURSION - я ненавижу, когда ваш SP срабатывает, когда передаются более широкие диапазоны дат :)
Скотт Ходжин
2
@ Скотт Ходжин Ты прав. Добавлена MAXRECURSIONподсказка в запрос для разрешения.
RDFozz
Этот метод имеет намеренное или непреднамеренное поведение, включающее включение в следующий день значения EndDate, включающего время. Если это поведение нежелательно, замените WHERE на DATEADD (DAY, 1, theDate) <@EndDate
Крис Портер,
1
@ChrisPorter - Отличный момент! Однако, если вы сделаете это DATEADD(DAY, 1, theDate) < @EndDate, вы не получите конец диапазона, когда оба значения даты и времени имеют одинаковый компонент времени. Я изменил ответ соответствующим образом, но с помощью <= @EndDate. Если вы не хотите, чтобы значение конца диапазона включалось независимо, тогда < @EndDateдействительно будет правильно.
RDFozz
6

Другой вариант - использовать функцию с табличным значением. Этот подход очень быстрый и предлагает немного больше гибкости. Вы вводите диапазон даты / времени, дату и время. Также предлагает преимущество включения его в 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 Очень верно. Узнал давным-давно, никогда не бывает один верный ответ.
Джон Каппеллетти
Спасибо за ответ. Похоже, есть несколько способов сделать это :) Другой парень ответил на мой вопрос так, чтобы точно решить поставленный желаемый результат, но вы правы, ваш кажется более гибким.
Роб V
@JohnCappelletti Это идеально подходит для того, что мне нужно сделать, но мне нужно было, чтобы убрать выходные и праздничные дни ... Я сделал это, изменив последнюю строку на следующую: Где D <= @ R2 AND DATEPART (Weekday, D), а не в (1,7) И НЕ В (ВЫБЕРИТЕ Holiday_Date ОТ ОТДЫХА). Праздники - это таблица с датами праздников.
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, где у меня был только доступ на чтение, и поэтому мне требовалось решение, основанное исключительно на 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;
Натан
источник