Rất dễ dàng chứng minh rằng hai biểu thức sau đây mang lại cùng một kết quả:ngày đầu tiên của tháng hiện tại.
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));
Và chúng mất khoảng thời gian như nhau để tính toán:
SELECT SYSDATETIME(); GO DECLARE @d DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); GO 1000000 GO SELECT SYSDATETIME(); GO DECLARE @d DATE = DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()); GO 1000000 SELECT SYSDATETIME();
Trên hệ thống của tôi, mất khoảng 175 giây để hoàn thành cả hai lô.
Vì vậy, tại sao bạn lại thích một phương pháp hơn phương pháp kia? Khi một trong số chúng thực sự gặp rắc rối với các ước tính về bản số .
Để làm mồi nhanh, hãy so sánh hai giá trị sau:
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), -- today: 2013-09-01 DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0); -- today: 1786-05-01 --------------------------------------^^^^^^^^^^^^ notice how these are swapped
(Lưu ý rằng các giá trị thực tế được trình bày ở đây sẽ thay đổi, tùy thuộc vào thời điểm bạn đang đọc bài đăng này - "hôm nay" được tham chiếu trong nhận xét là ngày 5 tháng 9 năm 2013, ngày bài đăng này được viết vào tháng 10 năm 2013, ví dụ:đầu ra sẽ được 2013-10-01
và 1786-04-01
.)
Với điều đó, hãy để tôi cho bạn biết ý của tôi…
Bản diễn lại
Hãy tạo một bảng rất đơn giản, chỉ với một cụm DATE
và tải 15.000 hàng có giá trị 1786-05-01
và 50 hàng có giá trị 2013-09-01
:
CREATE TABLE dbo.DateTest ( CreateDate DATE ); CREATE CLUSTERED INDEX x ON dbo.DateTest(CreateDate); INSERT dbo.DateTest(CreateDate) SELECT TOP (15000) DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0) FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2 UNION ALL SELECT TOP (50) DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0) FROM sys.all_objects;
Và sau đó chúng ta hãy xem xét các kế hoạch thực tế cho hai truy vấn này:
SELECT /* Query 1 */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); SELECT /* Query 2 */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0);
Các sơ đồ đồ họa trông đúng:
Kế hoạch đồ họa cho DATEDIFF (MONTH, 0, GETDATE ()) truy vấn
Kế hoạch đồ họa cho DATEDIFF (MONTH, GETDATE (), 0) truy vấn
Nhưng chi phí ước tính đã vượt quá xa - lưu ý rằng chi phí ước tính cao hơn bao nhiêu cho truy vấn đầu tiên, chỉ trả về 50 hàng, so với truy vấn thứ hai, trả về 15.000 hàng!
Lưới báo cáo hiển thị chi phí ước tính
Và tab Hoạt động hàng đầu cho thấy rằng truy vấn đầu tiên (tìm kiếm 2013-09-01
) ước tính rằng nó sẽ tìm thấy 15.000 hàng, trong khi thực tế nó chỉ tìm thấy 50; truy vấn thứ hai cho thấy điều ngược lại:nó dự kiến sẽ tìm thấy 50 hàng phù hợp với 1786-05-01
, nhưng đã tìm thấy 15.000. Dựa trên các ước tính về bản số không chính xác như thế này, tôi chắc rằng bạn có thể tưởng tượng loại tác động mạnh mẽ này có thể có đối với các truy vấn phức tạp hơn so với các tập dữ liệu lớn hơn nhiều.
Tab Thao tác hàng đầu cho truy vấn đầu tiên [DATEDIFF (MONTH, 0, GETDATE ())]
Tab Thao tác hàng đầu cho truy vấn thứ hai [DATEDIFF (MONTH, 0, GETDATE ())]
Một biến thể hơi khác của truy vấn, sử dụng một biểu thức khác để tính toán đầu tháng (được ám chỉ ở đầu bài đăng), không biểu hiện dấu hiệu này:
SELECT /* Query 3 */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()));
Kế hoạch rất giống với truy vấn 1 ở trên, và nếu bạn không xem xét kỹ hơn, bạn sẽ nghĩ rằng các kế hoạch này là tương đương:
Sơ đồ đồ họa cho truy vấn không phải DATEDIFF
Tuy nhiên, khi bạn nhìn vào tab Hoạt động hàng đầu tại đây, bạn thấy rằng ước tính đang tăng lên:
Tab Hoạt động hàng đầu hiển thị các ước tính chính xác
Đối với kích thước và truy vấn dữ liệu cụ thể này, tác động đến hiệu suất ròng (đáng chú ý nhất là thời lượng và số lần đọc) phần lớn là không liên quan. Và điều quan trọng cần lưu ý là bản thân các truy vấn vẫn trả về dữ liệu chính xác; nó chỉ là những ước tính sai (và có thể dẫn đến một kế hoạch tồi tệ hơn những gì tôi đã trình bày ở đây). Điều đó nói rằng, nếu bạn đang lấy hằng số bằng DATEDIFF trong các truy vấn của bạn theo cách này, bạn thực sự nên kiểm tra tác động này trong môi trường của mình.
Vậy tại sao điều này lại xảy ra?
Nói một cách đơn giản, SQL Server có DATEDIFF
lỗi trong đó nó hoán đổi đối số thứ hai và thứ ba khi đánh giá biểu thức để ước lượng bản số. Điều này dường như liên quan đến việc gấp liên tục, ít nhất là ở ngoại vi; Có rất nhiều chi tiết khác về việc gấp liên tục trong bài viết Sách trực tuyến này nhưng rất tiếc, bài viết không tiết lộ bất kỳ thông tin nào về lỗi cụ thể này.
Có một bản sửa lỗi - hay có?
Có một bài viết cơ sở kiến thức (KB # 2481274) tuyên bố giải quyết vấn đề, nhưng nó có một số vấn đề của riêng nó:
- Bài viết KB tuyên bố rằng sự cố đã được khắc phục trong các gói dịch vụ khác nhau hoặc các bản cập nhật tích lũy cho SQL Server 2005, 2008 và 2008 R2. Tuy nhiên, triệu chứng này vẫn xuất hiện trong các nhánh không được đề cập rõ ràng ở đó, mặc dù họ đã thấy nhiều CU bổ sung kể từ khi bài báo được xuất bản. Tôi vẫn có thể tái tạo sự cố này trên SQL Server 2008 SP3 CU # 8 (10.0.5828) và SQL Server 2012 SP1 CU # 5 (11.0.3373).
- Có thể quên đề cập rằng, để được hưởng lợi từ bản sửa lỗi, bạn cần bật cờ theo dõi 4199 (và "hưởng lợi" từ tất cả các cách khác mà cờ theo dõi cụ thể có thể ảnh hưởng đến trình tối ưu hóa). Thực tế là cờ theo dõi này được yêu cầu cho bản sửa lỗi được đề cập trong mục Connect liên quan, # 630583, nhưng thông tin này đã không được đưa trở lại bài viết KB. Cả bài viết KB và mục Kết nối đều không cung cấp bất kỳ thông tin chi tiết nào về nguyên nhân (rằng các đối số cho
DATEDIFF
đã được hoán đổi trong quá trình đánh giá). Mặt tích cực, chạy các truy vấn trên với cờ theo dõi được bật (sử dụngOPTION (QUERYTRACEON 4199)
) mang lại các kế hoạch không có vấn đề ước tính không chính xác.
- Nó gợi ý bạn sử dụng SQL động để khắc phục sự cố. Trong các thử nghiệm của tôi, sử dụng một biểu thức khác (chẳng hạn như biểu thức ở trên không sử dụng
DATEDIFF
) đã khắc phục sự cố trong các bản dựng hiện đại của cả SQL Server 2008 và SQL Server 2012. Đề xuất SQL động ở đây là phức tạp không cần thiết và có thể là quá mức cần thiết, vì một biểu thức khác có thể giải quyết được vấn đề. Nhưng nếu bạn đang sử dụng SQL động, tôi sẽ làm theo cách này thay vì cách họ đề xuất trong bài viết KB, quan trọng nhất là để giảm thiểu rủi ro chèn SQL:DECLARE @date DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), @sql NVARCHAR(MAX) = N'SELECT COUNT(*) FROM dbo.DateTest WHERE CreateDate = @date;'; EXEC sp_executesql @sql, N'@date DATE', @date;
(Và bạn có thể thêm
OPTION (RECOMPILE)
ở đó, tùy thuộc vào cách bạn muốn SQL Server xử lý việc dò tìm tham số.)Điều này dẫn đến cùng một kế hoạch với truy vấn trước đó không sử dụng
DATEDIFF
, với ước tính phù hợp và 99,1% chi phí trong tìm kiếm chỉ mục được phân nhóm.Một cách tiếp cận khác có thể cám dỗ bạn (và với bạn, ý tôi là tôi, khi tôi mới bắt đầu điều tra) là sử dụng một biến để tính toán trước giá trị:
DECLARE @d DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); SELECT COUNT(*) FROM dbo.DateTest WHERE CreateDate = @d;
Vấn đề với cách tiếp cận này là, với một biến, bạn sẽ kết thúc với một kế hoạch ổn định, nhưng bản chất sẽ dựa trên một phỏng đoán (và loại phỏng đoán sẽ phụ thuộc vào sự hiện diện hay vắng mặt của số liệu thống kê) . Trong trường hợp này, đây là ước tính so với thực tế:
Tab Thao tác hàng đầu cho truy vấn sử dụng biếnĐiều này rõ ràng là không đúng; có vẻ như SQL Server đã đoán rằng biến sẽ khớp với 50% số hàng trong bảng.
SQL Server 2014
Tôi đã tìm thấy một vấn đề hơi khác trong SQL Server 2014. Hai truy vấn đầu tiên đã được khắc phục (bằng các thay đổi đối với công cụ ước tính số lượng hoặc các bản sửa lỗi khác), nghĩa là DATEDIFF
các đối số không còn được chuyển đổi. Yay!
Tuy nhiên, một hồi quy dường như đã được giới thiệu với cách giải quyết là sử dụng một biểu thức khác - bây giờ nó có một ước tính không chính xác (dựa trên 50% phỏng đoán giống như sử dụng một biến). Đây là những truy vấn tôi đã chạy:
SELECT /* 0, GETDATE() (2013) */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0); SELECT /* GETDATE(), 0 (1786) */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = DATEADD(MONTH, DATEDIFF(MONTH, GETDATE(), 0), 0); SELECT /* Non-DATEDIFF */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = CONVERT(DATE, DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE())); DECLARE @d DATE = DATEADD(DAY, 1 - DAY(GETDATE()), GETDATE()); SELECT /* Variable */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = @d; DECLARE @date DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0), @sql NVARCHAR(MAX) = N'SELECT /* Dynamic SQL */ COUNT(*) FROM dbo.DateTest WHERE CreateDate = @date;'; EXEC sp_executesql @sql, N'@date DATE', @date;
Dưới đây là lưới báo cáo so sánh chi phí ước tính và số liệu thời gian chạy thực tế:
Chi phí ước tính cho 5 truy vấn mẫu trên SQL Server 2014
Và đây là số lượng hàng ước tính và thực tế của chúng (được lắp ráp bằng Photoshop):
Số hàng ước tính và thực tế cho 5 truy vấn trên SQL Server 2014
Rõ ràng là từ kết quả này rằng biểu thức trước đây đã giải quyết vấn đề nay đã giới thiệu một biểu thức khác. Tôi không chắc liệu đây có phải là một dấu hiệu của việc chạy trong CTP (ví dụ:một cái gì đó sẽ được sửa) hay đây thực sự là một hồi quy.
Trong trường hợp này, cờ theo dõi 4199 (tự nó) không có hiệu lực; công cụ ước tính cardinality mới đang đưa ra các phỏng đoán và đơn giản là không chính xác. Việc nó có dẫn đến một vấn đề về hiệu suất thực tế hay không phụ thuộc rất nhiều vào nhiều yếu tố khác ngoài phạm vi của bài đăng này.
Nếu bạn gặp sự cố này, bạn có thể - ít nhất là trong các CTP hiện tại - khôi phục hành vi cũ bằng cách sử dụng OPTION (QUERYTRACEON 9481, QUERYTRACEON 4199)
. Cờ theo dõi 9481 vô hiệu hóa công cụ ước lượng bản số mới, như được mô tả trong các ghi chú phát hành này (chắc chắn sẽ biến mất hoặc ít nhất là di chuyển vào một thời điểm nào đó). Điều này đến lượt nó khôi phục các ước tính chính xác cho không phải DATEDIFF
phiên bản của truy vấn, nhưng rất tiếc là vẫn không giải quyết được vấn đề trong đó phỏng đoán được thực hiện dựa trên một biến (và chỉ sử dụng TF9481, không có TF4199, buộc hai truy vấn đầu tiên hồi quy về hành vi hoán đổi đối số cũ).
Kết luận
Tôi phải thừa nhận đây là một bất ngờ lớn đối với tôi. Kudo to Martin Smith và t-clausen.dk vì đã kiên trì và thuyết phục tôi rằng đây là một vấn đề có thật chứ không phải tưởng tượng. Cũng xin gửi lời cảm ơn sâu sắc đến Paul White (@SQL_Kiwi), người đã giúp tôi giữ được sự tỉnh táo và nhắc nhở tôi về những điều tôi không nên nói. :-)
Không biết về lỗi này, tôi kiên quyết rằng kế hoạch truy vấn tốt hơn được tạo ra chỉ đơn giản bằng cách thay đổi văn bản truy vấn, không phải do thay đổi cụ thể. Hóa ra, đôi khi một thay đổi đối với một truy vấn mà bạn sẽ giả định sẽ không có gì khác biệt, thực sự sẽ. Vì vậy, tôi khuyên bạn rằng nếu bạn có bất kỳ mẫu truy vấn nào tương tự trong môi trường của mình, hãy kiểm tra chúng và đảm bảo rằng các ước tính về bản số được đưa ra đúng. Và hãy ghi chú để kiểm tra lại chúng khi bạn nâng cấp.