Comment créer une ligne pour chaque jour dans une plage de dates à l'aide d'une procédure stockée?

10

Je voudrais créer une procédure stockée qui créera une ligne dans un tableau pour chaque jour dans une plage de dates donnée. La procédure stockée accepte deux entrées: une date de début et une date de fin de la plage de dates souhaitée par l'utilisateur.

Alors, disons que j'ai une table comme ça:

SELECT Day, Currency
FROM ConversionTable

Le jour est un DateTime et la devise n'est qu'un entier.

Pour simplifier les choses, disons simplement que je veux toujours que la colonne Devise soit 1 pour chacune de ces lignes insérées. Donc, si quelqu'un saisit '5 mars 2017' comme date de début et '11 avril 2017' comme date de fin, je voudrais que les lignes suivantes soient créées:

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

Quelle est la meilleure façon de coder la procédure stockée pour ce faire? J'utilise SQL Server 2008 R2 dans mon environnement de test, mais notre environnement réel utilise SQL Server 2012, donc je peux mettre à niveau ma machine de test si de nouvelles fonctionnalités introduites en 2012 facilitent cette tâche.

Rob V
la source

Réponses:

14

Une option est un CTE récursif:

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

( MAXRECURSIONindice ajouté grâce au commentaire de Scott Hodgin, ci-dessous.)

RDFozz
la source
Oh oui! J'avais réfléchi à une approche par tableau «numérique» (réponse de Scott Hodgin). Pour un très grand nombre de jours, il pourrait mieux fonctionner. Et une belle approche réutilisable (réponse de John C) est toujours bonne. C'était la réponse la plus simple et la plus directe à laquelle je pouvais penser.
RDFozz
1
N'oubliez pas - la récursivité a une limite par défaut de 100 lorsque vous ne spécifiez pas de MAXRECURSION - Je déteste que votre SP explose lorsque des plages de dates plus larges sont passées :)
Scott Hodgin
2
@Scott Hodgin Vous avez raison. Ajout de l' MAXRECURSIONindice à la requête à résoudre.
RDFozz
Cette méthode a un comportement intentionnel ou non consistant à inclure le lendemain de la valeur EndDate comprend une heure. Si ce comportement n'est pas souhaitable, remplacez WHERE par DATEADD (DAY, 1, theDate) <@EndDate
Chris Porter
1
@ChrisPorter - Excellent point! Cependant, si vous le faites DATEADD(DAY, 1, theDate) < @EndDate, vous n'obtiendrez pas la fin de la plage lorsque les deux valeurs datetime ont le même composant time. J'ai modifié la réponse de manière appropriée, mais en utilisant <= @EndDate. Si vous ne voulez pas que la valeur de fin de plage soit incluse, ce < @EndDateserait en effet correct.
RDFozz
6

Une autre option consiste à utiliser une fonction de valeur de table. Cette approche est très rapide et offre un peu plus de flexibilité. Vous fournissez la plage de date / heure, la partie de date et l'incrément. Offre également l'avantage de l'inclure dans une APPLICATION CROISÉE

Par exemple

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

Retour

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 si vous êtes intéressé

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
la source
@RobV Très vrai. Appris il y a longtemps, il n'y a JAMAIS de vraie réponse.
John Cappelletti
Merci pour la réponse. On dirait qu'il y a plusieurs façons de le faire :) L'autre gars a répondu à ma question d'une manière qui a précisément résolu la sortie souhaitée posée, mais vous avez raison, la vôtre semble plus flexible.
Rob V
@JohnCappelletti Ceci est parfait pour ce que je dois faire, mais j'en avais besoin pour supprimer les week-ends et les jours fériés ... J'ai fait cela en remplaçant la dernière ligne par la suivante Où D <= @ R2 ET DATEPART (Weekday, D) pas dans (1,7) ET D NOT IN (SELECT Holiday_Date FROM Holidays). Vacances est un tableau qui contient les dates de vacances.
MattE
@MattE Bravo! Pour exclure les week-ends et les jours fériés, j'aurais fait de même. :)
John Cappelletti
3

En utilisant le post d'Aaron Bertrand sur la façon de créer un tableau de dimension de date comme exemple, j'ai trouvé ceci:

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]

Vous devriez pouvoir mettre ce type de logique dans votre procédure stockée et ajouter tout ce dont vous avez besoin.

Scott Hodgin
la source
2

J'ai dû résoudre un problème similaire récemment sur Redshift où je n'avais qu'un accès en lecture et j'avais donc besoin d'une solution purement SQL (pas de procédures stockées) pour obtenir une ligne pour chaque heure dans une plage de dates comme point de départ pour mon jeu de résultats. Je suis sûr que d'autres peuvent rendre cela plus élégant et le modifier à leurs fins, mais pour ceux qui en ont besoin, voici ma solution piratée:

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
la source