SQL es un lenguaje basado en conjuntos y los bucles deben ser el último recurso. Por lo tanto, el enfoque basado en conjuntos sería generar primero todas las fechas que necesita e insertarlas de una sola vez, en lugar de repetir e insertar una a la vez. Aaron Bertrand ha escrito una gran serie sobre la generación de un conjunto o secuencia sin bucles:
- Generar un conjunto o secuencia sin bucles:parte 1
- Generar un conjunto o secuencia sin bucles:parte 2
- Generar un conjunto o secuencia sin bucles:parte 3
La Parte 3 es específicamente relevante ya que trata de fechas.
Suponiendo que no tiene una tabla de calendario, puede usar el método CTE apilado para generar una lista de fechas entre las fechas de inicio y finalización.
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
WITH N1 (N) AS (SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2)
SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY N) - 1, @StartDate)
FROM N3;
Omití algunos detalles sobre cómo funciona esto, ya que se cubre en el artículo vinculado, en esencia, comienza con una tabla codificada de 10 filas, luego se une a esta tabla para obtener 100 filas (10 x 10) y luego se une a esta tabla de 100 filas para obtener 10,000 filas (me detuve en este punto, pero si necesita más filas, puede agregar más combinaciones).
En cada paso, la salida es una sola columna llamada N
con un valor de 1 (para simplificar las cosas). Al mismo tiempo que defino cómo generar 10 000 filas, en realidad le digo a SQL Server que solo genere el número necesario usando TOP
y la diferencia entre su fecha de inicio y finalización - TOP(DATEDIFF(DAY, @StartDate, @EndDate) + 1)
. Esto evita trabajos innecesarios. Tuve que sumar 1 a la diferencia para asegurarme de que ambas fechas estuvieran incluidas.
Usando la función de clasificación ROW_NUMBER()
Agrego un número incremental a cada una de las filas generadas, luego agrego este número incremental a su fecha de inicio para obtener la lista de fechas. Desde ROW_NUMBER()
comienza en 1, necesito deducir 1 de esto para asegurarme de que se incluya la fecha de inicio.
Entonces solo sería un caso de excluir fechas que ya existen usando NOT EXISTS
. Adjunté los resultados de la consulta anterior en su propio CTE llamado dates
:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
WITH N1 (N) AS (SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Dates AS
( SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY N) - 1, @StartDate)
FROM N3
)
INSERT INTO MyTable ([TimeStamp])
SELECT Date
FROM Dates AS d
WHERE NOT EXISTS (SELECT 1 FROM MyTable AS t WHERE d.Date = t.[TimeStamp])
Si tuviera que crear una tabla de calendario (como se describe en los artículos vinculados), entonces puede que no sea necesario insertar estas filas adicionales, simplemente podría generar su conjunto de resultados sobre la marcha, algo como:
SELECT [Timestamp] = c.Date,
t.[FruitType],
t.[NumOffered],
t.[NumTaken],
t.[NumAbandoned],
t.[NumSpoiled]
FROM dbo.Calendar AS c
LEFT JOIN dbo.MyTable AS t
ON t.[Timestamp] = c.[Date]
WHERE c.Date >= @StartDate
AND c.Date < @EndDate;
ANEXO
Para responder a su pregunta real, su ciclo se escribiría de la siguiente manera:
DECLARE @StartDate AS DATETIME
DECLARE @EndDate AS DATETIME
DECLARE @CurrentDate AS DATETIME
SET @StartDate = '2015-01-01'
SET @EndDate = GETDATE()
SET @CurrentDate = @StartDate
WHILE (@CurrentDate < @EndDate)
BEGIN
IF NOT EXISTS (SELECT 1 FROM myTable WHERE myTable.Timestamp = @CurrentDate)
BEGIN
INSERT INTO MyTable ([Timestamp])
VALUES (@CurrentDate);
END
SET @CurrentDate = DATEADD(DAY, 1, @CurrentDate); /*increment current date*/
END
Ejemplo en SQL Fiddle
No defiendo este enfoque, el hecho de que algo solo se haga una vez no significa que no deba demostrar la forma correcta de hacerlo.
EXPLICACIÓN ADICIONAL
Dado que el método CTE apilado puede haber complicado demasiado el enfoque basado en conjuntos, lo simplificaré utilizando la tabla de sistema no documentada master..spt_values
. Si ejecuta:
SELECT Number
FROM master..spt_values
WHERE Type = 'P';
Verás que obtienes todos los números del 0 al 2047.
Ahora si ejecutas:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P';
Obtiene todas las fechas desde su fecha de inicio hasta 2047 días en el futuro. Si agrega una cláusula where adicional, puede limitar esto a fechas anteriores a la fecha de finalización:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY, number, @StartDate) <= @EndDate;
Ahora que tiene todas las fechas que necesita en una sola consulta basada en conjuntos, puede eliminar las filas que ya existen en su tabla usando NOT EXISTS
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY, number, @StartDate) <= @EndDate
AND NOT EXISTS (SELECT 1 FROM MyTable AS t WHERE t.[Timestamp] = DATEADD(DAY, number, @StartDate));
Finalmente puedes insertar estas fechas en tu tabla usando INSERT
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
INSERT YourTable ([Timestamp])
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY, number, @StartDate) <= @EndDate
AND NOT EXISTS (SELECT 1 FROM MyTable AS t WHERE t.[Timestamp] = DATEADD(DAY, number, @StartDate));
Con suerte, esto demuestra de alguna manera que el enfoque basado en conjuntos no solo es mucho más eficiente, sino que también es más simple.