SQL är ett setbaserat språk och loopar bör vara en sista utväg. Så det uppsättningsbaserade tillvägagångssättet skulle vara att först generera alla datum du behöver och infoga dem på en gång, snarare än att loopa och infoga ett i taget. Aaron Bertrand har skrivit en fantastisk serie om att generera en uppsättning eller sekvens utan loopar:
- Generera en uppsättning eller sekvens utan loopar – del 1
- Generera en uppsättning eller sekvens utan loopar – del 2
- Generera en uppsättning eller sekvens utan loopar – del 3
Del 3 är specifikt relevant eftersom den handlar om datum.
Förutsatt att du inte har en kalendertabell kan du använda den staplade CTE-metoden för att generera en lista över datum mellan dina start- och slutdatum.
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;
Jag har hoppat över en del detaljer om hur detta fungerar eftersom det beskrivs i den länkade artikeln, i huvudsak börjar det med en hårdkodad tabell med 10 rader, förenar sedan den här tabellen med sig själv för att få 100 rader (10 x 10) och ansluter sedan till den här tabellen av 100 rader för sig själv för att få 10 000 rader (jag slutade vid det här laget men om du behöver fler rader kan du lägga till ytterligare sammanfogningar).
Vid varje steg är utgången en enda kolumn som kallas N
med värdet 1 (för att göra det enkelt). Samtidigt som jag definierar hur man genererar 10 000 rader säger jag faktiskt åt SQL Server att bara generera det antal som behövs genom att använda TOP
och skillnaden mellan ditt start- och slutdatum - TOP(DATEDIFF(DAY, @StartDate, @EndDate) + 1)
. Detta undviker onödigt arbete. Jag var tvungen att lägga till 1 till skillnaden för att säkerställa att båda datumen inkluderades.
Använda rankningsfunktionen ROW_NUMBER()
Jag lägger till ett inkrementellt nummer till var och en av de genererade raderna, sedan lägger jag till detta inkrementella nummer till ditt startdatum för att få listan över datum. Sedan ROW_NUMBER()
börjar vid 1 måste jag dra av 1 från detta för att säkerställa att startdatumet ingår.
Då skulle det bara vara ett fall av att exkludera datum som redan finns med NOT EXISTS
. Jag har bifogat resultaten av ovanstående fråga i deras egen CTE som heter 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])
Om du skulle skapa en kalendertabell (som beskrivs i de länkade artiklarna) kanske det inte är nödvändigt att infoga dessa extra rader, du kan bara generera din resultatuppsättning direkt, något i stil med:
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;
TILLÄGG
För att svara på din faktiska fråga skulle din loop skrivas så här:
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
Exempel på SQL Fiddle
Jag förespråkar inte detta tillvägagångssätt, bara för att något bara görs en gång betyder det inte att jag inte ska visa det korrekta sättet att göra det.
YTTERLIGARE FÖRKLARING
Eftersom den staplade CTE-metoden kan ha överkomplicerat den uppsättningsbaserade metoden kommer jag att förenkla den genom att använda den odokumenterade systemtabellen master..spt_values
. Om du kör:
SELECT Number
FROM master..spt_values
WHERE Type = 'P';
Du kommer att se att du får alla siffror från 0 -2047.
Om du nu kör:
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 alla datum från ditt startdatum till 2047 dagar i framtiden. Om du lägger till ytterligare en where-klausul kan du begränsa detta till datum före ditt slutdatum:
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 alla datum du behöver i en enda uppsättningsbaserad fråga. Du kan eliminera raderna som redan finns i din tabell med 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));
Slutligen kan du infoga dessa datum i din tabell med 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));
Förhoppningsvis visar detta på något sätt att den uppsättningsbaserade metoden inte bara är mycket effektivare, den är också enklare.