SQL er et sætbaseret sprog, og loops bør være en sidste udvej. Så den sætbaserede tilgang ville være først at generere alle de datoer, du har brug for, og indsætte dem på én gang, i stedet for at sløjfe og indsætte én ad gangen. Aaron Bertrand har skrevet en fantastisk serie om at generere et sæt eller en sekvens uden loops:
- Generer et sæt eller en sekvens uden loops – del 1
- Generer et sæt eller en sekvens uden loops – del 2
- Generer et sæt eller en sekvens uden loops – del 3
Del 3 er specifikt relevant, da den omhandler datoer.
Forudsat at du ikke har en kalendertabel, kan du bruge den stablede CTE-metode til at generere en liste over datoer mellem dine start- og slutdatoer.
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;
Jeg har sprunget over nogle detaljer om, hvordan dette fungerer, da det er dækket i den linkede artikel, i det væsentlige starter det med en hårdkodet tabel på 10 rækker, forener derefter denne tabel med sig selv for at få 100 rækker (10 x 10) og forbinder derefter denne tabel af 100 rækker til sig selv for at få 10.000 rækker (jeg stoppede på dette tidspunkt, men hvis du har brug for flere rækker, kan du tilføje yderligere sammenføjninger).
Ved hvert trin er output en enkelt kolonne kaldet N
med en værdi på 1 (for at holde tingene simple). Samtidig med at jeg definerer, hvordan man genererer 10.000 rækker, fortæller jeg faktisk SQL Server kun at generere det nødvendige antal ved at bruge TOP
og forskellen mellem din start- og slutdato - TOP(DATEDIFF(DAY, @StartDate, @EndDate) + 1)
. Dette undgår unødvendigt arbejde. Jeg var nødt til at tilføje 1 til forskellen for at sikre, at begge datoer var inkluderet.
Brug af rangeringsfunktionen ROW_NUMBER()
Jeg tilføjer et trinvis tal til hver af de genererede rækker, og derefter tilføjer jeg dette trinvise tal til din startdato for at få listen over datoer. Siden ROW_NUMBER()
begynder kl. 1, skal jeg trække 1 fra dette for at sikre, at startdatoen er inkluderet.
Så ville det bare være et tilfælde af at ekskludere datoer, der allerede eksisterer ved hjælp af NOT EXISTS
. Jeg har vedlagt resultaterne af ovenstående forespørgsel i deres egen CTE kaldet 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])
Hvis du skulle oprette en kalendertabel (som beskrevet i de linkede artikler), så er det muligvis ikke nødvendigt at indsætte disse ekstra rækker, du kan bare generere dit resultatsæt i en fart, sådan som:
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;
TILFØJELSE
For at besvare dit egentlige spørgsmål vil din loop blive skrevet som følger:
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
Eksempel på SQL Fiddle
Jeg går ikke ind for denne tilgang, bare fordi noget kun bliver gjort én gang, betyder det ikke, at jeg ikke skal demonstrere den korrekte måde at gøre det på.
YDERLIGERE FORKLARING
Da den stablede CTE-metode kan have overkompliceret den sætbaserede tilgang, vil jeg forenkle den ved at bruge den udokumenterede systemtabel master..spt_values
. Hvis du kører:
SELECT Number
FROM master..spt_values
WHERE Type = 'P';
Du vil se, at du får alle tallene fra 0 -2047.
Hvis du nu kører:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P';
Du får alle datoer fra din startdato til 2047 dage i fremtiden. Hvis du tilføjer en yderligere where-klausul, kan du begrænse dette til datoer før din slutdato:
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 har du alle de datoer, du har brug for i et enkelt sæt baseret forespørgsel, du kan fjerne de rækker, der allerede findes i din tabel ved hjælp af 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));
Endelig kan du indsætte disse datoer i din tabel ved at bruge 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));
Forhåbentlig viser dette en vis måde, at den sætbaserede tilgang ikke kun er meget mere effektiv, den er også enklere.