SQL é uma linguagem baseada em conjunto e os loops devem ser o último recurso. Portanto, a abordagem baseada em conjunto seria primeiro gerar todas as datas necessárias e inseri-las de uma só vez, em vez de fazer um loop e inserir uma de cada vez. Aaron Bertrand escreveu uma ótima série sobre como gerar um conjunto ou sequência sem loops:
- Gerar um conjunto ou sequência sem loops – parte 1
- Gerar um conjunto ou sequência sem loops – parte 2
- Gerar um conjunto ou sequência sem loops – parte 3
A Parte 3 é especificamente relevante, pois trata de datas.
Supondo que você não tenha uma tabela de calendário, você pode usar o método CTE empilhado para gerar uma lista de datas entre as datas de início e término.
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;
Eu pulei alguns detalhes sobre como isso funciona, pois é abordado no artigo vinculado, em essência, ele começa com uma tabela codificada de 10 linhas, depois une esta tabela consigo mesma para obter 100 linhas (10 x 10) e, em seguida, une esta tabela de 100 linhas para si mesmo para obter 10.000 linhas (eu parei neste ponto, mas se você precisar de mais linhas, poderá adicionar mais junções).
Em cada etapa, a saída é uma única coluna chamada
N
com um valor de 1 (para manter as coisas simples). Ao mesmo tempo em que defini como gerar 10.000 linhas, na verdade digo ao SQL Server para gerar apenas o número necessário usando TOP
e a diferença entre sua data de início e término - TOP(DATEDIFF(DAY, @StartDate, @EndDate) + 1)
. Isso evita trabalho desnecessário. Eu tive que adicionar 1 à diferença para garantir que ambas as datas fossem incluídas. Usando a função de classificação
ROW_NUMBER()
Eu adiciono um número incremental a cada uma das linhas geradas e, em seguida, adiciono esse número incremental à sua data de início para obter a lista de datas. Desde ROW_NUMBER()
começa em 1, preciso deduzir 1 disso para garantir que a data de início seja incluída. Então seria apenas um caso de excluir datas que já existem usando
NOT EXISTS
. Incluí os resultados da consulta acima em seu próprio CTE chamado 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])
Exemplo no SQL Fiddle
Se você criasse uma tabela de calendário (conforme descrito nos artigos vinculados), talvez não fosse necessário inserir essas linhas extras, você poderia apenas gerar seu conjunto de resultados em tempo real, 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;
ADENDO
Para responder à sua pergunta real, seu loop seria escrito da seguinte forma:
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
Exemplo no SQL Fiddle
Eu não defendo essa abordagem, só porque algo está sendo feito apenas uma vez não significa que eu não deva demonstrar a maneira correta de fazê-lo.
OUTRAS EXPLICAÇÕES
Como o método CTE empilhado pode ter complicado demais a abordagem baseada em conjunto, vou simplificá-lo usando a tabela de sistema não documentada
master..spt_values
. Se você executar:SELECT Number
FROM master..spt_values
WHERE Type = 'P';
Você verá que obtém todos os números de 0 a 2047.
Agora se você executar:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P';
Você obtém todas as datas desde sua data de início até 2047 dias no futuro. Se você adicionar mais uma cláusula where, poderá limitá-la a datas anteriores à sua data de término:
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;
Agora você tem todas as datas que você precisa em uma única consulta baseada em conjunto, você pode eliminar as linhas que já existem em sua tabela 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, você pode inserir essas datas em sua tabela 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));
Espero que isso ajude de alguma forma a mostrar que a abordagem baseada em conjuntos não é apenas muito mais eficiente, mas também mais simples.