Come creare una riga per ogni giorno in un intervallo di date utilizzando una procedura memorizzata?


11

Vorrei creare una procedura memorizzata che creerà una riga in una tabella per ogni giorno in un determinato intervallo di date. La Stored Procedure accetta due input: una data di inizio e una data di fine dell'intervallo di date desiderato dall'utente.

Quindi, diciamo che ho un tavolo così:

SELECT Day, Currency
FROM ConversionTable

Day è un DateTime e Currency è solo un numero intero.

Per semplificare le cose, diciamo solo che voglio sempre che la colonna Valuta sia 1 per ciascuna di queste righe inserite. Quindi, se qualcuno inserisce '5 marzo 2017' come data di inizio e '11 aprile 2017' come data di fine, vorrei che fossero create le seguenti righe:

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

Qual è il modo migliore per codificare la procedura memorizzata per farlo? Sto usando SQL Server 2008 R2 nel mio ambiente di prova, ma il nostro ambiente reale utilizza SQL Server 2012, quindi posso aggiornare la mia macchina di prova se nel 2012 sono state introdotte nuove funzionalità che semplificano questa attività.

Risposte:


15

Un'opzione è un CTE ricorsivo:

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)
;

( MAXRECURSIONsuggerimento aggiunto grazie al commento di Scott Hodgin, sotto.)


Oh si! Avevo pensato a un approccio al tavolo dei "numeri" (la risposta di Scott Hodgin). Per un numero molto elevato di giorni, potrebbe funzionare meglio. E un buon approccio riutilizzabile (la risposta di John C) è sempre buono. Questa era solo la risposta più semplice e diretta a cui potevo pensare.
RDFozz

1
Ricorda: la ricorsione ha un limite predefinito di 100 quando non specifichi una MAXRECURSION - Odio che il tuo SP salti quando passano intervalli di date più ampi :)
Scott Hodgin

2
@Scott Hodgin Hai ragione. Aggiunto il MAXRECURSIONsuggerimento alla query da risolvere.
RDFozz

Questo metodo ha un comportamento intenzionale o non intenzionale di includere il giorno successivo del valore EndDate include un orario. Se questo comportamento non è auspicabile, modifica WHERE in DATA (GIORNO, 1, data) <@EndDate
Chris Porter,

1
@ChrisPorter - Ottimo punto! Tuttavia, se lo fai DATEADD(DAY, 1, theDate) < @EndDate, non ottieni la fine dell'intervallo quando entrambi i valori di datetime hanno lo stesso componente temporale. Ho modificato la risposta in modo appropriato, ma utilizzando <= @EndDate. Se non si desidera includere il valore di fine intervallo indipendentemente, < @EndDatesarebbe effettivamente corretto.
RDFozz,

6

Un'altra opzione è quella di utilizzare una funzione con valori di tabella. Questo approccio è molto veloce e offre un po 'più di flessibilità. Fornisci l'intervallo data / ora, la parte della data e l'incremento. Offre anche il vantaggio di includerlo in una CROSS APPLY

Per esempio

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

ritorna

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

L'UDF, se interessato

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 Molto vero. Imparato molto tempo fa non c'è MAI una vera risposta.
John Cappelletti,

Grazie per la risposta. Sembra che ci siano diversi modi per farlo :) L'altro ragazzo ha risposto alla mia domanda in un modo che ha risolto con precisione l'output desiderato proposto, ma hai ragione il tuo sembra che sia più flessibile.
Rob V

@JohnCappelletti Questo è perfetto per quello che devo fare, ma ne avevo bisogno per rimuovere Week-end e festivi ... L'ho fatto cambiando l'ultima riga nella seguente dove D <= @ R2 AND DATEPART (Weekday, D) non in (1,7) AND D IN IN (SELEZIONA Holiday_Date DA Vacanze). Vacanze è una tabella che contiene le date delle vacanze.
MattE

@MattE Ben fatto! Per escludere i fine settimana e le vacanze, avrei fatto lo stesso. :)
John Cappelletti,

3

Usando il post di Aaron Bertrand su come creare una tabella dimensionale della data come esempio, ho trovato questo:

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]

Dovresti essere in grado di inserire questo tipo di logica nella procedura memorizzata e aggiungere tutto il necessario.


2

Di recente ho dovuto risolvere un problema simile su Redshift in cui avevo solo accesso in lettura e quindi avevo bisogno di una soluzione puramente basata su SQL (nessuna procedura memorizzata) per ottenere una riga per ogni ora in un intervallo di date come punto di partenza per il mio set di risultati. Sono sicuro che altri possono renderlo più elegante e modificarlo per i loro scopi, ma per chi è nel bisogno, ecco la mia soluzione compromessa:

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;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.