Як створити рядок на кожен день у діапазоні дат, використовуючи збережену процедуру?


11

Я хотів би створити збережену процедуру, яка створюватиме рядки в таблиці за кожен день у заданому діапазоні дат. Збережена процедура приймає два входи - дату початку та кінцеву дату, бажану користувачем.

Отже, скажімо, у мене така таблиця:

SELECT Day, Currency
FROM ConversionTable

День - це дата Date, а валюта - це ціле число.

Щоб зробити це просто, скажімо, що я завжди хочу, щоб стовпець "Валюта" був 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 році буде нова функціональність, що полегшує цю задачу.

Відповіді:


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

1
Пам'ятайте лише, що для рекурсії встановлено обмеження за замовчуванням - 100, коли ви не вказуєте MAXRECURSION - я ненавиджу, щоб ваш SP пройшов, коли ширші діапазони дат передаються :)
Скотт Ходгін

2
@Scott Hodgin Ви маєте рацію. Додано MAXRECURSIONпідказку до запиту для вирішення.
RDFozz

Цей метод має навмисну ​​чи ненавмисну ​​поведінку, включаючи наступний день, значення значення EndDate включає час. Якщо така поведінка небажана, змініть, де БЕЗ ДАТАТА (День, 1, theDate) <@EndDate
Кріс Портер,

1
@ChrisPorter - Відмінний бал! Однак якщо ви це зробите DATEADD(DAY, 1, theDate) < @EndDate, ви не отримаєте кінець діапазону, коли обидва значення дати мають однакову складову часу. Я відповідно змінив відповідь, але використовуючи <= @EndDate. Якщо ви не хочете, щоб кінцеве значення діапазону було включене незалежно, тоді це < @EndDateбуло б правильно.
RDFozz

6

Інший варіант - використовувати функцію «Таблиця з оцінкою». Цей підхід дуже швидкий і пропонує трохи більше гнучкості. Ви постачаєте дату / часовий діапазон, дату і час збільшення. Також пропонується перевага включення його до КРЕСНОГО ЗАЯВКИ

Наприклад

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

АДС, якщо зацікавлено

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 (будній день, D) не в (1,7) І НЕ ВИБІРТЕ (ВИБІРТЬ Holiday_Date ОТ Свята). Свята - це таблиця, яка містить дати відпустки.
МЕТЕ

@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;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.