SQL jest językiem opartym na zbiorach, a pętle powinny być ostatecznością. Tak więc podejście oparte na zestawach polegałoby na tym, aby najpierw wygenerować wszystkie wymagane daty i wstawić je za jednym razem, a nie zapętlać i wstawiać pojedynczo. Aaron Bertrand napisał świetną serię na temat generowania zestawu lub sekwencji bez pętli:
- Generuj zestaw lub sekwencję bez pętli – część 1
- Generuj zestaw lub sekwencję bez pętli – część 2
- Generuj zestaw lub sekwencję bez pętli – część 3
Część 3 jest szczególnie istotna, ponieważ dotyczy dat.
Zakładając, że nie masz tabeli Kalendarz, możesz użyć skumulowanej metody CTE, aby wygenerować listę dat między datą początkową a końcową.
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;
Pominąłem trochę szczegółów na temat tego, jak to działa, ponieważ jest to omówione w połączonym artykule, w istocie zaczyna się od zakodowanej na sztywno tabeli z 10 wierszami, a następnie łączy się z tą tabelą, aby uzyskać 100 wierszy (10 x 10), a następnie dołącza do tej tabeli 100 wierszy do siebie, aby uzyskać 10 000 wierszy (zatrzymałem się w tym momencie, ale jeśli potrzebujesz kolejnych wierszy, możesz dodać kolejne sprzężenia).
Na każdym kroku wyjściem jest pojedyncza kolumna o nazwie N
o wartości 1 (aby wszystko było proste). W tym samym czasie, gdy definiuję sposób generowania 10 000 wierszy, tak naprawdę mówię SQL Server, aby generował tylko potrzebną liczbę za pomocą TOP
oraz różnica między datą rozpoczęcia i zakończenia — TOP(DATEDIFF(DAY, @StartDate, @EndDate) + 1)
. Pozwala to uniknąć niepotrzebnej pracy. Musiałem dodać 1 do różnicy, aby upewnić się, że obie daty zostały uwzględnione.
Korzystanie z funkcji rankingu ROW_NUMBER()
Dodaję kolejny numer do każdego z wygenerowanych wierszy, a następnie dodaję ten kolejny numer do daty rozpoczęcia, aby uzyskać listę dat. Od ROW_NUMBER()
zaczyna się od 1, muszę od tego odjąć 1, aby mieć pewność, że uwzględniono datę rozpoczęcia.
Wtedy byłby to tylko przypadek wykluczenia dat, które już istnieją za pomocą NOT EXISTS
. Umieściłem wyniki powyższego zapytania w ich własnym CTE o nazwie 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])
Jeśli utworzysz tabelę kalendarza (zgodnie z opisem w powiązanych artykułach), wstawianie tych dodatkowych wierszy może nie być konieczne, możesz po prostu wygenerować zestaw wyników w locie, na przykład:
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;
UZUPEŁNIENIE
Aby odpowiedzieć na twoje rzeczywiste pytanie, twoja pętla zostałaby napisana w następujący sposób:
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
Przykład SQL Fiddle
Nie opowiadam się za takim podejściem, tylko dlatego, że coś robi się tylko raz, nie oznacza, że nie powinienem demonstrować prawidłowego sposobu robienia tego.
DALSZE WYJAŚNIENIE
Ponieważ skumulowana metoda CTE mogła nadmiernie skomplikować podejście oparte na zbiorach, uprości je, używając nieudokumentowanej tabeli systemowej master..spt_values
. Jeśli biegasz:
SELECT Number
FROM master..spt_values
WHERE Type = 'P';
Zobaczysz, że otrzymujesz wszystkie liczby od 0 -2047.
Teraz, jeśli biegniesz:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P';
Otrzymasz wszystkie daty od daty rozpoczęcia do 2047 dni w przyszłości. Jeśli dodasz kolejną klauzulę gdzie, możesz ograniczyć ją do dat przed datą końcową:
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;
Teraz masz wszystkie potrzebne daty w pojedynczym zapytaniu opartym na zestawie, możesz wyeliminować wiersze, które już istnieją w tabeli, używając 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));
Na koniec możesz wstawić te daty do swojej tabeli za pomocą 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));
Mam nadzieję, że w pewien sposób pokazuje to, że podejście oparte na zbiorach jest nie tylko znacznie wydajniejsze, ale także prostsze.