Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

While loop no SQL Server 2008 iterando por meio de um intervalo de datas e, em seguida, INSERT


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:

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.