SQL is een op een set gebaseerde taal en loops zouden een laatste redmiddel moeten zijn. Dus de op sets gebaseerde benadering zou zijn om eerst alle gewenste datums te genereren en ze in één keer in te voegen, in plaats van ze een voor een te herhalen en in te voegen. Aaron Bertrand heeft een geweldige serie geschreven over het genereren van een set of reeks zonder loops:
- Genereer een set of reeks zonder lussen – deel 1
- Genereer een set of reeks zonder lussen – deel 2
- Genereer een set of reeks zonder lussen – deel 3
Deel 3 is specifiek relevant omdat het gaat over datums.
Ervan uitgaande dat u geen kalendertabel hebt, kunt u de gestapelde CTE-methode gebruiken om een lijst met datums tussen uw start- en einddatums te genereren.
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;
Ik heb wat details over hoe dit werkt overgeslagen zoals het wordt behandeld in het gekoppelde artikel, in wezen begint het met een hard gecodeerde tabel van 10 rijen, voegt zich vervolgens bij deze tabel met zichzelf om 100 rijen (10 x 10) te krijgen en voegt zich vervolgens bij deze tabel van 100 rijen naar zichzelf om 10.000 rijen te krijgen (ik stopte op dit punt, maar als je meer rijen nodig hebt, kun je meer joins toevoegen).
Bij elke stap is de output een enkele kolom genaamd N
met een waarde van 1 (om het simpel te houden). Terwijl ik definieer hoe 10.000 rijen moeten worden gegenereerd, vertel ik SQL Server eigenlijk alleen het aantal te genereren dat nodig is door TOP
te gebruiken. en het verschil tussen uw start- en einddatum - TOP(DATEDIFF(DAY, @StartDate, @EndDate) + 1)
. Dit voorkomt onnodig werk. Ik moest 1 bij het verschil optellen om ervoor te zorgen dat beide datums werden opgenomen.
De rangschikkingsfunctie gebruiken ROW_NUMBER()
Ik voeg een oplopend getal toe aan elk van de gegenereerde rijen, dan voeg ik dit oplopende getal toe aan je startdatum om de lijst met datums te krijgen. Sinds ROW_NUMBER()
begint bij 1, ik moet hier 1 aftrekken om ervoor te zorgen dat de startdatum wordt opgenomen.
Dan zou het gewoon een kwestie zijn van het uitsluiten van reeds bestaande datums met behulp van NOT EXISTS
. Ik heb de resultaten van de bovenstaande zoekopdracht ingesloten in hun eigen CTE genaamd 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])
Als u een kalendertabel zou maken (zoals beschreven in de gekoppelde artikelen), is het misschien niet nodig om deze extra rijen in te voegen, u kunt gewoon uw resultatenset direct genereren, zoiets als:
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;
TOEVOEGEN
Om uw eigenlijke vraag te beantwoorden, zou uw lus als volgt worden geschreven:
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
Voorbeeld op SQL Fiddle
Ik pleit niet voor deze aanpak, alleen omdat iets maar één keer wordt gedaan, wil nog niet zeggen dat ik niet de juiste manier van doen moet demonstreren.
VERDERE UITLEG
Aangezien de gestapelde CTE-methode de set-gebaseerde aanpak mogelijk te ingewikkeld heeft gemaakt, zal ik het vereenvoudigen door de ongedocumenteerde systeemtabel master..spt_values
te gebruiken. . Als je rent:
SELECT Number
FROM master..spt_values
WHERE Type = 'P';
Je zult zien dat je alle getallen van 0 -2047 krijgt.
Als je nu rent:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P';
U krijgt alle datums vanaf uw startdatum tot 2047 dagen in de toekomst. Als u nog een waar-clausule toevoegt, kunt u dit beperken tot data vóór uw einddatum:
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;
Nu heb je alle datums die je nodig hebt in een op een enkele set gebaseerde query. Je kunt de rijen die al in je tabel staan verwijderen met 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));
Ten slotte kunt u deze datums in uw tabel invoegen met 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));
Hopelijk laat dit op een of andere manier zien dat de set-gebaseerde aanpak niet alleen veel efficiënter is, maar ook eenvoudiger.