Wie erstelle ich mit einer gespeicherten Prozedur eine Zeile für jeden Tag in einem Datumsbereich?

10

Ich möchte eine gespeicherte Prozedur erstellen, die für jeden Tag in einem bestimmten Datumsbereich eine Zeile in einer Tabelle erstellt. Die gespeicherte Prozedur akzeptiert zwei Eingaben - Ein Startdatum und ein Enddatum des vom Benutzer gewünschten Datumsbereichs.

Nehmen wir also an, ich habe einen Tisch wie diesen:

SELECT Day, Currency
FROM ConversionTable

Tag ist eine DateTime und Währung ist nur eine ganze Zahl.

Nehmen wir zur Vereinfachung an, ich möchte immer, dass die Spalte Währung für jede dieser eingefügten Zeilen 1 ist. Wenn also jemand "5. März 2017" als Startdatum und "11. April 2017" als Enddatum eingibt, möchte ich die folgenden Zeilen erstellen:

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

Was ist der beste Weg, um die gespeicherte Prozedur zu codieren, um dies zu tun? Ich verwende SQL Server 2008 R2 in meiner Testumgebung, aber unsere reale Umgebung verwendet SQL Server 2012, sodass ich meinen Testcomputer aktualisieren kann, wenn 2012 neue Funktionen eingeführt wurden, die diese Aufgabe erleichtern.

Rob V.
quelle

Antworten:

14

Eine Option ist ein rekursiver 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)
;

( MAXRECURSIONHinweis hinzugefügt dank Scott Hodgins Kommentar unten.)

RDFozz
quelle
Oh ja! Ich hatte über einen "Zahlen" -Tabellenansatz nachgedacht (Scott Hodgins Antwort). Für eine sehr große Anzahl von Tagen kann es eine bessere Leistung bringen. Und ein netter wiederverwendbarer Ansatz (John Cs Antwort) ist immer gut. Dies war nur die einfachste und direkteste Antwort, die ich mir vorstellen konnte.
RDFozz
1
Denken Sie daran - Rekursion hat ein Standardlimit von 100, wenn Sie keine MAXRECURSION angeben - Ich hasse es, wenn Ihr SP durchbrennt, wenn größere Datumsbereiche übergeben werden :)
Scott Hodgin
2
@ Scott Hodgin Du hast recht. Der zu MAXRECURSIONlösende Abfrage wurde der Hinweis hinzugefügt .
RDFozz
Diese Methode hat das absichtliche oder unbeabsichtigte Verhalten, dass der nächste Tag des EndDate-Werts eine Zeit enthält. Wenn dieses Verhalten unerwünscht ist, ändern Sie das WHERE in DATEADD (DAY, 1, theDate) <@EndDate
Chris Porter
1
@ ChrisPorter - Ausgezeichneter Punkt! Wenn Sie es jedoch schaffen DATEADD(DAY, 1, theDate) < @EndDate, erhalten Sie das Ende des Bereichs nicht, wenn beide Datums- / Uhrzeitwerte dieselbe Zeitkomponente haben. Ich habe die Antwort entsprechend geändert, aber mit <= @EndDate. Wenn Sie nicht möchten, dass der Wert für das Ende des Bereichs < @EndDatetrotzdem enthalten ist, ist dies in der Tat korrekt.
RDFozz
6

Eine andere Möglichkeit ist die Verwendung einer Tabellenwertfunktion. Dieser Ansatz ist sehr schnell und bietet etwas mehr Flexibilität. Sie geben den Datums- / Zeitbereich, DatePart und Increment an. Bietet auch den Vorteil, es in eine CROSS APPLY aufzunehmen

Beispielsweise

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

Kehrt zurück

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

Die UDF bei Interesse

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
quelle
@ RobV Sehr wahr. Vor langer Zeit gelernt, gibt es NIE eine wahre Antwort.
John Cappelletti
Danke für die Antwort. Es sieht so aus, als gäbe es mehrere Möglichkeiten, dies zu tun :) Der andere Typ hat meine Frage so beantwortet, dass die gestellte gewünschte Ausgabe genau gelöst wurde, aber Sie haben Recht, Ihre scheint flexibler zu sein.
Rob V
@JohnCappelletti Dies ist perfekt für das, was ich tun muss, aber ich brauchte es, um Wochenenden und Feiertage zu entfernen ... Ich habe dazu die letzte Zeile in die folgende Zeile geändert, wobei D <= @ R2 UND DATEPART (Wochentag, D) nicht in (1,7) UND D NICHT IN (SELECT Holiday_Date FROM Holidays). Feiertage ist eine Tabelle, die Feiertagsdaten enthält.
MattE
@MattE Gut gemacht! Um Wochenenden und Feiertage auszuschließen, hätte ich dasselbe getan. :)
John Cappelletti
3

Am Beispiel von Aaron Bertrands Beitrag zum Erstellen einer Datumsdimensionstabelle habe ich Folgendes gefunden:

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]

Sie sollten in der Lage sein, diese Art von Logik in Ihre gespeicherte Prozedur einzufügen und alles hinzuzufügen, was Sie sonst noch benötigen.

Scott Hodgin
quelle
1

Ich musste kürzlich ein ähnliches Problem bei Redshift lösen, bei dem ich nur Lesezugriff hatte und daher eine rein SQL-basierte Lösung (keine gespeicherten Prozeduren) benötigte, um eine Zeile für jede Stunde in einem Datumsbereich als Ausgangspunkt für meine Ergebnismenge zu erhalten. Ich bin sicher, andere können dies eleganter gestalten und für ihre Zwecke modifizieren, aber für Bedürftige ist hier meine gehackte Lösung:

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
quelle