¿Cómo crear una fila para cada día en un rango de fechas usando un procedimiento almacenado?

10

Me gustaría crear un procedimiento almacenado que creará una fila en una tabla para cada día en un rango de fechas determinado. El procedimiento almacenado acepta dos entradas: una fecha de inicio y una fecha de finalización del intervalo de fechas deseado por el usuario.

Entonces, digamos que tengo una tabla así:

SELECT Day, Currency
FROM ConversionTable

El día es una fecha y hora, y la moneda es solo un número entero.

Para simplificar las cosas, digamos que siempre quiero que la columna Moneda sea 1 para cada una de estas filas insertadas. Entonces, si alguien ingresa '5 de marzo de 2017' como la fecha de inicio y '11 de abril de 2017' como la fecha de finalización, me gustaría crear las siguientes filas:

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

¿Cuál es la mejor manera de codificar el procedimiento almacenado para hacer esto? Estoy usando SQL Server 2008 R2 en mi entorno de prueba, pero nuestro entorno real usa SQL Server 2012, por lo que puedo actualizar mi máquina de prueba si hay una nueva funcionalidad introducida en 2012 que facilita esta tarea.

Rob V
fuente

Respuestas:

14

Una opción es un CTE recursivo:

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

( MAXRECURSIONpista agregada gracias al comentario de Scott Hodgin, a continuación).

RDFozz
fuente
¡Oh si! Pensé en un enfoque de tabla de "números" (la respuesta de Scott Hodgin). Para un gran número de días, podría funcionar mejor. Y un buen enfoque reutilizable (la respuesta de John C) siempre es bueno. Esta fue la respuesta más simple y directa que se me ocurrió.
RDFozz
1
Solo recuerde: la recursión tiene un límite predeterminado de 100 cuando no especifica una MAXRECURSION: odio que su SP explote cuando se pasan rangos de fechas más amplios :)
Scott Hodgin
2
@ Scott Hodgin Tienes razón. Se agregó la MAXRECURSIONpista a la consulta para resolver.
RDFozz
Este método tiene un comportamiento intencional o involuntario de incluir el día siguiente del valor EndDate incluye una hora. Si este comportamiento no es deseable, cambie WHERE a DATEADD (DAY, 1, theDate) <@EndDate
Chris Porter
1
@ChrisPorter - ¡Excelente punto! Sin embargo, si lo logra DATEADD(DAY, 1, theDate) < @EndDate, no obtiene el final del rango cuando ambos valores de fecha y hora tienen el mismo componente de tiempo. Modifiqué la respuesta apropiadamente, pero usando <= @EndDate. Si no desea que se incluya el valor del final del rango, entonces < @EndDatesería correcto.
RDFozz
6

Otra opción es utilizar una función con valores de tabla. Este enfoque es muy rápido y ofrece un poco más de flexibilidad. Usted proporciona el rango de fecha / hora, la fecha y el incremento. También ofrece la ventaja de incluirlo en una APLICACIÓN CRUZADA

Por ejemplo

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

Devoluciones

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

El UDF si está interesado

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
fuente
@RobV Muy cierto. Aprendí hace mucho tiempo que NUNCA hay una respuesta verdadera.
John Cappelletti
Gracias por la respuesta. Parece que hay varias formas de hacer esto :) El otro tipo respondió mi pregunta de una manera que resolvió con precisión la salida deseada planteada, pero tienes razón, la tuya parece ser más flexible.
Rob V
@JohnCappelletti Esto es perfecto para lo que necesito hacer, pero lo necesitaba para eliminar los fines de semana y días festivos ... Hice esto cambiando la última línea a la siguiente Donde D <= @ R2 Y DATEPART (Weekday, D) no en (1,7) Y NO EN (SELECCIONE Holiday_Date FROM Holidays). Vacaciones es una tabla que contiene fechas de vacaciones.
MattE
@MattE ¡Bien hecho! Para excluir fines de semana y días festivos, habría hecho lo mismo. :)
John Cappelletti
3

Usando la publicación de Aaron Bertrand sobre cómo crear una tabla de dimensiones de fecha como ejemplo, se me ocurrió esto:

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]

Debería poder poner este tipo de lógica en su procedimiento almacenado y agregar cualquier otra cosa que necesite.

Scott Hodgin
fuente
2

Tuve que resolver un problema similar recientemente en Redshift, donde solo tenía acceso de lectura y, por lo tanto, necesitaba una solución puramente basada en SQL (sin procedimientos almacenados) para obtener una fila por cada hora en un rango de fechas como punto de partida para mi conjunto de resultados. Estoy seguro de que otros pueden hacer esto más elegante y modificarlo para sus propósitos, pero para aquellos que lo necesitan, aquí está mi solución pirateada:

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
fuente