Jak utworzyć wiersz dla każdego dnia w zakresie dat przy użyciu procedury składowanej?

11

Chciałbym utworzyć procedurę składowaną, która utworzy wiersz w tabeli dla każdego dnia w danym zakresie dat. Procedura przechowywana akceptuje dwa dane wejściowe - datę początkową i końcową zakresu dat żądanego przez użytkownika.

Powiedzmy, że mam taki stół:

SELECT Day, Currency
FROM ConversionTable

Dzień to data i godzina, a waluta to tylko liczba całkowita.

Aby uprościć sprawę, powiedzmy, że zawsze chcę, aby kolumna Waluta wynosiła 1 dla każdego z tych wstawionych wierszy. Jeśli więc ktoś wprowadzi „5 marca 2017 r.” Jako datę początkową i „11 kwietnia 2017 r.” Jako datę końcową, chciałbym utworzyć następujące wiersze:

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

Jaki jest najlepszy sposób na zakodowanie procedury przechowywanej, aby to zrobić? Używam SQL Server 2008 R2 w moim środowisku testowym, ale nasze prawdziwe środowisko korzysta z SQL Server 2012, więc mogę uaktualnić moją maszynę testową, jeśli w 2012 r. Wprowadzono nową funkcjonalność, która ułatwia to zadanie.

Rob V.
źródło

Odpowiedzi:

15

Jedną z opcji jest rekurencyjna 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)
;

( MAXRECURSIONpodpowiedź dodana dzięki komentarzowi Scotta Hodgina poniżej).

RDFozz
źródło
O tak! Zastanawiałem się nad podejściem do tabeli liczb (odpowiedź Scotta Hodgina). Przez bardzo dużą liczbę dni może działać lepiej. A dobre podejście wielokrotnego użytku (odpowiedź Johna C.) jest zawsze dobre. To była najprostsza i najprostsza odpowiedź, jaką mogłem wymyślić.
RDFozz
1
Pamiętaj tylko - rekursja ma domyślny limit 100, jeśli nie określisz MAKSYMALNEGO ROZWOJU - Nienawidzę, aby twój SP wybuchł, gdy zostaną wprowadzone szersze zakresy dat :)
Scott Hodgin
2
@Scott Hodgin Masz rację. Dodano MAXRECURSIONwskazówkę do zapytania, aby rozwiązać.
RDFozz
Ta metoda ma celowe lub niezamierzone zachowanie obejmujące uwzględnienie następnego dnia wartości EndDate. Jeśli takie zachowanie jest niepożądane, zmień WHERE na DATEADD (DZIEŃ, 1, theDate) <@ EndDate
Chris Porter
1
@ChrisPorter - Doskonały punkt! Jednak jeśli to zrobisz DATEADD(DAY, 1, theDate) < @EndDate, nie otrzymasz końca zakresu, gdy obie wartości daty i godziny mają ten sam składnik czasu. Odpowiednio zmodyfikowałem odpowiedź, ale używając <= @EndDate. Jeśli nie chcesz, aby wartość końca zakresu była uwzględniana niezależnie, to < @EndDaterzeczywiście byłaby poprawna.
RDFozz
6

Inną opcją jest użycie funkcji wycenianej w tabeli. To podejście jest bardzo szybkie i oferuje nieco większą elastyczność. Podajesz zakres daty / godziny, datę i część oraz przyrost. Zaletą jest także włączenie go w APLIKACJĘ KRZYŻOWĄ

Na przykład

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

Zwroty

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, jeśli jest zainteresowany

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) 
*/
John Cappelletti
źródło
@RobV Bardzo prawda. Nauczyłem się dawno temu NIGDY nie ma jednej prawdziwej odpowiedzi.
John Cappelletti,
Dziękuję za odpowiedź. Wygląda na to, że jest na to kilka sposobów :) Drugi facet odpowiedział na moje pytanie w sposób, który dokładnie rozwiązał pożądany efekt, ale masz rację, wydaje się, że jest bardziej elastyczny.
Rob V
@JohnCappelletti Jest to idealne rozwiązanie do tego, co muszę zrobić, ale potrzebowałem go, aby usunąć weekendy i święta ... Zrobiłem to, zmieniając ostatnią linię na następującą Gdzie D <= @ R2 I DATEPART (Dzień tygodnia, D) nie ma (1,7) I NIE DOTYCZY (WYBIERZ Holiday_Date FROM Holidays). Święta to tabela zawierająca daty świąt.
MattE
@MattE Dobra robota! Aby wykluczyć weekendy i święta, zrobiłbym to samo. :)
John Cappelletti
3

Korzystając z postu Aarona Bertranda na temat tworzenia tabeli wymiarów daty jako przykładu, wymyśliłem to:

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]

Powinieneś być w stanie umieścić ten typ logiki w procedurze przechowywanej i dodać wszystko, czego potrzebujesz.

Scott Hodgin
źródło
2

Ostatnio musiałem rozwiązać podobny problem na Redshift, gdzie miałem tylko dostęp do odczytu i dlatego potrzebowałem rozwiązania opartego wyłącznie na SQL (bez procedur składowanych), aby uzyskać wiersz dla każdej godziny w zakresie dat jako punkt wyjścia dla mojego zestawu wyników. Jestem pewien, że inni mogą uczynić to bardziej eleganckim i zmodyfikować do swoich celów, ale dla potrzebujących, oto moje zhakowane rozwiązanie:

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;
Nathan
źródło