SQL là một ngôn ngữ dựa trên tập hợp và các vòng lặp nên là phương sách cuối cùng. Vì vậy, phương pháp dựa trên tập hợp trước tiên sẽ là tạo tất cả các ngày bạn yêu cầu và chèn chúng vào một lượt, thay vì lặp lại và chèn từng ngày một. Aaron Bertrand đã viết một loạt bài tuyệt vời về cách tạo một tập hợp hoặc trình tự không có vòng lặp:
- Tạo một tập hợp hoặc chuỗi không có vòng lặp - phần 1
- Tạo một tập hợp hoặc chuỗi không có vòng lặp - phần 2
- Tạo một tập hợp hoặc chuỗi không có vòng lặp - phần 3
Phần 3 có liên quan cụ thể vì nó đề cập đến ngày tháng.
Giả sử bạn không có bảng Lịch, bạn có thể sử dụng phương pháp CTE xếp chồng lên nhau để tạo danh sách ngày giữa ngày bắt đầu và ngày kết thúc của bạn.
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;
Tôi đã bỏ qua một số chi tiết về cách hoạt động của điều này vì nó được đề cập trong bài viết được liên kết, về bản chất, nó bắt đầu với một bảng được mã hóa cứng gồm 10 hàng, sau đó tham gia bảng này với chính nó để có được 100 hàng (10 x 10) rồi tham gia bảng này. 100 hàng với chính nó để có được 10.000 hàng (tôi đã dừng lại tại thời điểm này nhưng nếu bạn yêu cầu các hàng khác, bạn có thể thêm các liên kết khác).
Ở mỗi bước, đầu ra là một cột duy nhất được gọi là N
với giá trị 1 (để giữ cho mọi thứ đơn giản). Đồng thời với việc xác định cách tạo 10.000 hàng, tôi thực sự yêu cầu SQL Server chỉ tạo số lượng cần thiết bằng cách sử dụng TOP
và sự khác biệt giữa ngày bắt đầu và ngày kết thúc của bạn - TOP(DATEDIFF(DAY, @StartDate, @EndDate) + 1)
. Điều này tránh những công việc không cần thiết. Tôi đã phải thêm 1 vào sự khác biệt để đảm bảo cả hai ngày đều được bao gồm.
Sử dụng chức năng xếp hạng ROW_NUMBER()
Tôi thêm một số tăng dần vào mỗi hàng được tạo, sau đó tôi thêm số tăng dần này vào ngày bắt đầu của bạn để nhận danh sách ngày. Kể từ ROW_NUMBER()
bắt đầu từ 1, tôi cần trừ 1 từ này để đảm bảo ngày bắt đầu được bao gồm.
Sau đó, nó sẽ chỉ là trường hợp loại trừ các ngày đã tồn tại bằng cách sử dụng NOT EXISTS
. Tôi đã đính kèm các kết quả của truy vấn trên trong CTE của riêng chúng được gọi là 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])
Nếu bạn định tạo một bảng lịch (như được mô tả trong các bài viết được liên kết) thì có thể không cần chèn thêm các hàng này, bạn chỉ có thể tạo tập kết quả của mình một cách nhanh chóng, chẳng hạn như:
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;
BỔ SUNG
Để trả lời câu hỏi thực tế của bạn, vòng lặp của bạn sẽ được viết như sau:
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
Ví dụ về SQL Fiddle
Tôi không ủng hộ cách tiếp cận này, chỉ vì việc gì đó chỉ được thực hiện một lần không có nghĩa là tôi không nên chứng minh cách làm chính xác.
GIẢI THÍCH THÊM
Vì phương pháp CTE xếp chồng có thể phức tạp hơn phương pháp dựa trên tập hợp, tôi sẽ đơn giản hóa nó bằng cách sử dụng bảng hệ thống không có tài liệu master..spt_values
. Nếu bạn chạy:
SELECT Number
FROM master..spt_values
WHERE Type = 'P';
Bạn sẽ thấy rằng bạn nhận được tất cả các số từ 0 -2047.
Bây giờ nếu bạn chạy:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P';
Bạn nhận được tất cả các ngày từ ngày bắt đầu đến năm 2047 trong tương lai. Nếu bạn thêm một điều khoản where khác, bạn có thể giới hạn điều này vào những ngày trước ngày kết thúc của bạn:
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;
Bây giờ bạn có tất cả các ngày bạn cần trong một truy vấn dựa trên một tập hợp duy nhất, bạn có thể loại bỏ các hàng đã tồn tại trong bảng của mình bằng cách sử dụng 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));
Cuối cùng, bạn có thể chèn những ngày này vào bảng của mình bằng cách sử dụng 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));
Hy vọng rằng điều này sẽ giúp cho bạn thấy rằng cách tiếp cận dựa trên tập hợp không chỉ hiệu quả hơn nhiều mà còn đơn giản hơn.