Trước đó của loạt bài này (Phần 1 | Phần 2), chúng ta đã nói về việc tạo ra một chuỗi số bằng các kỹ thuật khác nhau. Mặc dù thú vị và hữu ích trong một số trường hợp, một ứng dụng thực tế hơn là tạo một chuỗi các ngày liền kề; ví dụ:một báo cáo yêu cầu hiển thị tất cả các ngày trong tháng, ngay cả khi một số ngày không có giao dịch.
Trong một bài viết trước, tôi đã đề cập rằng rất dễ dàng để lấy ra một chuỗi ngày từ một chuỗi số. Vì chúng tôi đã thiết lập nhiều cách để tính ra một chuỗi số, hãy xem bước tiếp theo như thế nào. Hãy bắt đầu thật đơn giản và giả sử chúng tôi muốn chạy báo cáo trong ba ngày, từ ngày 1 tháng 1 đến ngày 3 tháng 1 và bao gồm một hàng cho mỗi ngày. Cách cũ sẽ là tạo bảng #temp, tạo vòng lặp, có một biến chứa ngày hiện tại, trong vòng lặp chèn một hàng vào bảng #temp cho đến cuối phạm vi, sau đó sử dụng dấu # bảng tạm thời để tham gia bên ngoài vào dữ liệu nguồn của chúng tôi. Đó là nhiều mã hơn tôi thậm chí muốn trình bày ở đây, đừng bận tâm đến việc sản xuất, bảo trì và để các đồng nghiệp học hỏi.
Bắt đầu đơn giản
Với một dãy số đã được thiết lập (bất kể phương pháp bạn chọn là gì), nhiệm vụ này trở nên dễ dàng hơn nhiều. Đối với ví dụ này, tôi có thể thay thế các trình tạo chuỗi phức tạp bằng một liên hợp rất đơn giản, vì tôi chỉ cần ba ngày. Tôi sẽ làm cho tập hợp này chứa bốn hàng, vì vậy cũng dễ dàng chứng minh cách cắt cho chính xác chuỗi mà bạn cần.
Đầu tiên, chúng tôi có một số biến để giữ đầu và cuối của phạm vi mà chúng tôi quan tâm:
DECLARE @s DATE = '2012-01-01', @e DATE = '2012-01-03';
Bây giờ, nếu chúng ta chỉ bắt đầu với trình tạo chuỗi đơn giản, nó có thể trông như thế này. Tôi sẽ thêm một ORDER BY
ở đây cũng vậy, để an toàn, vì chúng ta không bao giờ có thể dựa vào các giả định mà chúng ta đưa ra về đơn đặt hàng.
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT n FROM n ORDER BY n; -- result: n ---- 1 2 3 4
Để chuyển đổi đó thành một chuỗi ngày, chúng ta có thể chỉ cần áp dụng DATEADD()
kể từ ngày bắt đầu:
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT DATEADD(DAY, n, @s) FROM n ORDER BY n; -- result: ---- 2012-01-02 2012-01-03 2012-01-04 2012-01-05
Điều này vẫn chưa hoàn toàn đúng, vì phạm vi của chúng tôi bắt đầu từ ngày thứ 2 thay vì ngày thứ nhất. Vì vậy, để sử dụng ngày bắt đầu của chúng tôi làm cơ sở, chúng tôi cần chuyển đổi nhóm của chúng tôi từ dựa trên 1 sang dựa trên 0. Chúng ta có thể làm điều đó bằng cách trừ đi 1:
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT DATEADD(DAY, n-1, @s) FROM n ORDER BY n; -- result: ---- 2012-01-01 2012-01-02 2012-01-03 2012-01-04
Sắp xong rồi! Chúng tôi chỉ cần giới hạn kết quả từ nguồn chuỗi lớn hơn của chúng tôi, điều này chúng tôi có thể thực hiện bằng cách cung cấp DATEDIFF
, tính bằng ngày, từ đầu đến cuối của phạm vi, đến TOP
toán tử - và sau đó thêm 1 (kể từ DATEDIFF
về cơ bản báo cáo một phạm vi kết thúc mở).
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM n ORDER BY n; -- result: ---- 2012-01-01 2012-01-02 2012-01-03
Thêm dữ liệu thực
Bây giờ để xem chúng ta sẽ kết hợp với một bảng khác như thế nào để lấy báo cáo, chúng ta chỉ có thể sử dụng truy vấn mới và kết hợp bên ngoài của chúng ta với dữ liệu nguồn.
;WITH n(n) AS ( SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 ), d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM n ORDER BY n ) SELECT d.OrderDate, OrderCount = COUNT(o.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeader AS o ON o.OrderDate >= d.OrderDate AND o.OrderDate < DATEADD(DAY, 1, d.OrderDate) GROUP BY d.OrderDate ORDER BY d.OrderDate;
(Lưu ý rằng chúng ta không thể nói COUNT(*)
được nữa , vì điều này sẽ tính phía bên trái, sẽ luôn là 1).
Một cách khác để viết điều này sẽ là:
;WITH d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM ( SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 ) AS n(n) ORDER BY n ) SELECT d.OrderDate, OrderCount = COUNT(o.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeader AS o ON o.OrderDate >= d.OrderDate AND o.OrderDate < DATEADD(DAY, 1, d.OrderDate) GROUP BY d.OrderDate ORDER BY d.OrderDate;
Điều này sẽ giúp bạn dễ dàng hình dung cách bạn sẽ thay thế CTE hàng đầu bằng việc tạo chuỗi ngày từ bất kỳ nguồn nào bạn chọn. Chúng ta sẽ xem xét những điều đó (ngoại trừ phương pháp tiếp cận CTE đệ quy, chỉ dùng để làm lệch đồ thị), sử dụng AdventureWorks2012, nhưng chúng ta sẽ sử dụng SalesOrderHeaderEnlarged
bảng tôi đã tạo từ tập lệnh này của Jonathan Kehayias. Tôi đã thêm một chỉ mục để trợ giúp với truy vấn cụ thể này:
CREATE INDEX d_so ON Sales.SalesOrderHeaderEnlarged(OrderDate);
Cũng xin lưu ý rằng tôi đang chọn một phạm vi ngày tùy ý mà tôi biết có tồn tại trong bảng.
Bảng số
;WITH d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM dbo.Numbers ORDER BY n ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND CONVERT(DATE, s.OrderDate) = d.OrderDate WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
Kế hoạch (bấm để phóng to):
spt_values
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; ;WITH d(OrderDate) AS ( SELECT DATEADD(DAY, n-1, @s) FROM (SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) ROW_NUMBER() OVER (ORDER BY Number) FROM master..spt_values) AS x(n) ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND CONVERT(DATE, s.OrderDate) = d.OrderDate WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
Kế hoạch (bấm để phóng to):
sys.all_objects
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; ;WITH d(OrderDate) AS ( SELECT DATEADD(DAY, n-1, @s) FROM (SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) ROW_NUMBER() OVER (ORDER BY [object_id]) FROM sys.all_objects) AS x(n) ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND CONVERT(DATE, s.OrderDate) = d.OrderDate WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
Kế hoạch (bấm để phóng to):
CTE xếp chồng
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; ;WITH e1(n) AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ), e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b), d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY n)-1, @s) FROM e2 ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND d.OrderDate = CONVERT(DATE, s.OrderDate) WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
Kế hoạch (bấm để phóng to):
Bây giờ, trong phạm vi dài một năm, điều này sẽ không cắt giảm nó, vì nó chỉ sản xuất 100 hàng. Trong một năm, chúng tôi cần phải phủ 366 hàng (để tính cho các năm nhuận tiềm năng), vì vậy nó sẽ giống như sau:
DECLARE @s DATE = '2006-10-23', @e DATE = '2007-10-22'; ;WITH e1(n) AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ), e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b), e3(n) AS (SELECT 1 FROM e2 CROSS JOIN (SELECT TOP (37) n FROM e2) AS b), d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY N)-1, @s) FROM e3 ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND d.OrderDate = CONVERT(DATE, s.OrderDate) WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
Kế hoạch (bấm để phóng to):
Bảng lịch
Đây là một cái mới mà chúng ta đã không nói nhiều trong hai bài viết trước. Nếu bạn đang sử dụng chuỗi ngày cho nhiều truy vấn thì bạn nên cân nhắc việc có cả bảng Số và bảng Lịch. Đối số tương tự nói về việc cần bao nhiêu dung lượng thực sự và tốc độ truy cập sẽ nhanh như thế nào khi bảng được truy vấn thường xuyên. Ví dụ:để lưu trữ 30 năm ngày, nó cần ít hơn 11.000 hàng (con số chính xác phụ thuộc vào số năm nhuận bạn kéo dài) và chỉ chiếm 200 KB. Có, bạn đã đọc đúng:200 kilobyte . (Và được nén, chỉ 136 KB.)
Để tạo bảng Lịch với 30 năm dữ liệu, giả sử bạn đã tin rằng có bảng Số là một điều tốt, chúng tôi có thể làm điều này:
DECLARE @s DATE = '2005-07-01'; -- earliest year in SalesOrderHeader DECLARE @e DATE = DATEADD(DAY, -1, DATEADD(YEAR, 30, @s)); SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) d = CONVERT(DATE, DATEADD(DAY, n-1, @s)) INTO dbo.Calendar FROM dbo.Numbers ORDER BY n; CREATE UNIQUE CLUSTERED INDEX d ON dbo.Calendar(d);
Bây giờ để sử dụng bảng Lịch đó trong truy vấn báo cáo bán hàng của chúng tôi, chúng tôi có thể viết một truy vấn đơn giản hơn nhiều:
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; SELECT OrderDate = c.d, OrderCount = COUNT(s.SalesOrderID) FROM dbo.Calendar AS c LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND c.d = CONVERT(DATE, s.OrderDate) WHERE c.d >= @s AND c.d <= @e GROUP BY c.d ORDER BY c.d;
Kế hoạch (bấm để phóng to):
Hiệu suất
Tôi đã tạo cả bản sao nén và không nén của bảng Số và Lịch, đồng thời thử nghiệm phạm vi một tuần, phạm vi một tháng và phạm vi một năm. Tôi cũng đã chạy các truy vấn với bộ đệm lạnh và bộ nhớ đệm ấm, nhưng điều đó hóa ra phần lớn là không quan trọng.
Thời lượng, tính bằng mili giây, để tạo phạm vi cả tuần
Thời lượng, tính bằng mili giây, để tạo phạm vi dài cả tháng
Thời lượng, tính bằng mili giây, để tạo phạm vi dài cả năm
Phụ lục
Paul White (blog | @SQL_Kiwi) đã chỉ ra rằng bạn có thể ép buộc bảng Numbers để tạo ra một kế hoạch hiệu quả hơn nhiều bằng cách sử dụng truy vấn sau:
SELECT OrderDate = DATEADD(DAY, n, 0), OrderCount = COUNT(s.SalesOrderID) FROM dbo.Numbers AS n LEFT OUTER JOIN Sales.SalesOrderHeader AS s ON s.OrderDate >= CONVERT(DATETIME, @s) AND s.OrderDate < DATEADD(DAY, 1, CONVERT(DATETIME, @e)) AND DATEDIFF(DAY, 0, OrderDate) = n WHERE n.n >= DATEDIFF(DAY, 0, @s) AND n.n <= DATEDIFF(DAY, 0, @e) GROUP BY n ORDER BY n;
Tại thời điểm này, tôi sẽ không chạy lại tất cả các bài kiểm tra hiệu suất (bài tập cho người đọc!), Nhưng tôi sẽ giả định rằng nó sẽ tạo ra thời gian tốt hơn hoặc tương tự. Tuy nhiên, tôi nghĩ rằng bảng Lịch là một thứ hữu ích nên có ngay cả khi nó không thực sự cần thiết.
Kết luận
Các kết quả nói cho mình. Đối với việc tạo ra một chuỗi số, phương pháp tiếp cận bảng Numbers có hiệu quả, nhưng chỉ ở mức nhỏ - ngay cả ở 1.000.000 hàng. Và đối với một loạt ngày, ở phần cuối, bạn sẽ không thấy sự khác biệt nhiều giữa các kỹ thuật khác nhau. Tuy nhiên, rõ ràng là khi phạm vi ngày của bạn lớn hơn, đặc biệt là khi bạn đang xử lý một bảng nguồn lớn, bảng Lịch thực sự thể hiện giá trị của nó - đặc biệt là với bộ nhớ thấp. Ngay cả với hệ thống số liệu kỳ quặc của Canada, 60 mili giây vẫn tốt hơn khoảng 10 * giây * khi nó chỉ phát sinh 200 KB trên đĩa.
Tôi hy vọng bạn thích bộ truyện nhỏ này; đó là một chủ đề mà tôi đã có ý định xem lại từ bao đời nay.
[Phần 1 | Phần 2 | Phần 3]