SQL è un linguaggio basato su set e i loop dovrebbero essere l'ultima risorsa. Quindi l'approccio basato su set sarebbe quello di generare prima tutte le date necessarie e inserirle in una volta sola, piuttosto che eseguire il ciclo e inserirne una alla volta. Aaron Bertrand ha scritto una grande serie sulla generazione di un set o di una sequenza senza loop:
- Genera un set o una sequenza senza loop – parte 1
- Genera un set o una sequenza senza loop – parte 2
- Genera un set o una sequenza senza loop – parte 3
La parte 3 è particolarmente rilevante in quanto riguarda le date.
Supponendo che tu non disponga di una tabella Calendar, puoi utilizzare il metodo CTE in pila per generare un elenco di date tra le date di inizio e di fine.
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;
Ho saltato alcuni dettagli su come funziona poiché è trattato nell'articolo collegato, in sostanza inizia con una tabella codificata di 10 righe, quindi si unisce a questa tabella con se stessa per ottenere 100 righe (10 x 10), quindi si unisce a questa tabella di 100 righe su se stesso per ottenere 10.000 righe (mi sono fermato a questo punto ma se hai bisogno di ulteriori righe puoi aggiungere ulteriori join).
Ad ogni passaggio l'output è una singola colonna chiamata N
con un valore di 1 (per semplificare le cose). Contemporaneamente alla definizione di come generare 10.000 righe, in realtà dico a SQL Server di generare solo il numero necessario utilizzando TOP
e la differenza tra la data di inizio e di fine - TOP(DATEDIFF(DAY, @StartDate, @EndDate) + 1)
. Ciò evita il lavoro non necessario. Ho dovuto aggiungere 1 alla differenza per assicurarmi che fossero incluse entrambe le date.
Utilizzo della funzione di classificazione ROW_NUMBER()
Aggiungo un numero incrementale a ciascuna delle righe generate, quindi aggiungo questo numero incrementale alla data di inizio per ottenere l'elenco delle date. Da ROW_NUMBER()
inizia a 1, devo sottrarre 1 da questo per assicurarmi che la data di inizio sia inclusa.
Quindi si tratterebbe solo di escludere date che già esistono utilizzando NOT EXISTS
. Ho racchiuso i risultati della query di cui sopra nel loro CTE chiamato 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])
Se dovessi creare una tabella di calendario (come descritto negli articoli collegati), potrebbe non essere necessario inserire queste righe extra, potresti semplicemente generare il tuo set di risultati al volo, qualcosa del tipo:
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;
APPENDICE
Per rispondere alla tua vera domanda, il tuo ciclo verrebbe scritto come segue:
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
Esempio su SQL Fiddle
Non sostengo questo approccio, solo perché qualcosa viene fatto solo una volta non significa che non dovrei dimostrare il modo corretto di farlo.
ULTERIORI SPIEGAZIONI
Poiché il metodo CTE in pila potrebbe aver complicato l'approccio basato su insiemi, lo semplificherò utilizzando la tabella di sistema non documentata master..spt_values
. Se corri:
SELECT Number
FROM master..spt_values
WHERE Type = 'P';
Vedrai che ottieni tutti i numeri da 0 a 2047.
Ora se corri:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P';
Ottieni tutte le date dalla data di inizio a 2047 giorni nel futuro. Se aggiungi un'ulteriore clausola where, puoi limitarla a date prima della data di fine:
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;
Ora hai tutte le date di cui hai bisogno in un'unica query basata su set puoi eliminare le righe che già esistono nella tua tabella 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));
Infine puoi inserire queste date nella tua tabella 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));
Si spera che questo in qualche modo dimostri che l'approccio basato sugli insiemi non solo è molto più efficiente, ma è anche più semplice.