Trở lại năm 2013, tôi đã viết về một lỗi trong trình tối ưu hóa trong đó đối số thứ 2 và thứ 3 đối với DATEDIFF()
có thể được hoán đổi - điều này có thể dẫn đến ước tính số lượng hàng không chính xác và do đó, lựa chọn kế hoạch thực thi kém:
- Sự ngạc nhiên và giả định về hiệu suất:DATEDIFF
Cuối tuần vừa qua, tôi đã biết về một tình huống tương tự và đưa ra giả thiết ngay lập tức rằng đó là vấn đề tương tự. Rốt cuộc, các triệu chứng có vẻ gần giống nhau:
- Có một hàm ngày / giờ trong
WHERE
mệnh đề.- Lần này là
DATEADD()
thay vìDATEDIFF()
.
- Lần này là
- Có một ước tính số hàng rõ ràng không chính xác là 1, so với số lượng hàng thực tế là hơn 3 triệu.
- Đây thực sự là một ước tính bằng 0, nhưng SQL Server luôn làm tròn các ước tính đó thành 1.
- Một lựa chọn kế hoạch kém đã được thực hiện (trong trường hợp này, một phép nối vòng được chọn) do ước tính thấp.
Mẫu vi phạm trông như thế này:
WHERE [datetime2(7) column] >= DATEADD(DAY, -365, SYSUTCDATETIME());
Người dùng đã thử một số biến thể, nhưng không có gì thay đổi; cuối cùng họ đã giải quyết được vấn đề bằng cách thay đổi vị từ thành:
WHERE DATEDIFF(DAY, [column], SYSUTCDATETIME()) <= 365;
Điều này có một ước tính tốt hơn (phỏng đoán bất bình đẳng 30% điển hình); vì vậy không hoàn toàn đúng. Và trong khi nó loại bỏ phép nối vòng lặp, có hai vấn đề lớn với vị từ này:
- Nó không phải cùng một truy vấn, vì nó hiện đang tìm kiếm các ranh giới 365 ngày đã trôi qua, trái ngược với việc lớn hơn một thời điểm cụ thể trong 365 ngày trước. Ý nghĩa thống kê? Có thể không. Nhưng về mặt kỹ thuật, ngưỡng cửa không giống nhau.
- Việc áp dụng hàm đối với cột làm cho toàn bộ biểu thức không thể phân chia được - dẫn đến việc quét toàn bộ. Khi bảng chỉ chứa một ít dữ liệu trong hơn một năm, đây không phải là vấn đề lớn, nhưng khi bảng trở nên lớn hơn hoặc vị từ trở nên hẹp hơn, điều này sẽ trở thành một vấn đề.
Một lần nữa, tôi đi đến kết luận rằng DATEADD()
hoạt động là vấn đề và đề xuất một phương pháp không dựa vào DATEADD()
- xây dựng datetime
từ tất cả các phần của thời gian hiện tại, cho phép tôi trừ đi một năm mà không cần sử dụng DATEADD()
:
WHERE [column] >= DATETIMEFROMPARTS( DATEPART(YEAR, SYSUTCDATETIME())-1, DATEPART(MONTH, SYSUTCDATETIME()), DATEPART(DAY, SYSUTCDATETIME()), DATEPART(HOUR, SYSUTCDATETIME()), DATEPART(MINUTE, SYSUTCDATETIME()), DATEPART(SECOND, SYSUTCDATETIME()), 0);
Ngoài việc cồng kềnh, điều này còn có một số vấn đề của riêng nó, đó là một loạt các logic sẽ phải được thêm vào để tính toán đúng cho các năm nhuận. Đầu tiên, để nó không bị lỗi nếu nó xảy ra vào ngày 29 tháng 2 và thứ hai, bao gồm chính xác 365 ngày trong mọi trường hợp (thay vì 366 trong năm sau một ngày nhuận). Tất nhiên, các bản sửa lỗi dễ dàng, nhưng chúng làm cho logic xấu hơn nhiều - đặc biệt là vì truy vấn cần thiết để tồn tại bên trong một chế độ xem, nơi không thể thực hiện được các biến trung gian và nhiều bước.
Trong thời gian chờ đợi, OP đã gửi một mục Kết nối, bị thất vọng bởi ước tính 1 hàng:
- Kết nối # 2567628:Ràng buộc với DateAdd () không cung cấp ước tính tốt
Sau đó, Paul White (@SQL_Kiwi) xuất hiện và giống như nhiều lần trước đây, làm sáng tỏ thêm một số vấn đề. Anh ấy đã chia sẻ một mục Connect liên quan do Erland Sommarskog đệ trình vào năm 2011:
- Kết nối # 685903:Ước tính không chính xác khi thời gian đồng bộ xuất hiện trong biểu thức dateadd ()
Về cơ bản, vấn đề là một ước tính kém có thể được thực hiện không đơn giản khi SYSDATETIME()
(hoặc SYSUTCDATETIME()
) xuất hiện, như Erland đã báo cáo ban đầu, nhưng khi bất kỳ datetime2
nào biểu thức có liên quan đến vị từ (và có lẽ chỉ khi DATEADD()
cũng được sử dụng). Và nó có thể đi theo cả hai cách - nếu chúng ta hoán đổi >=
cho <=
, ước tính trở thành toàn bộ bảng, vì vậy, có vẻ như trình tối ưu hóa đang xem xét SYSDATETIME()
giá trị như một hằng số và hoàn toàn bỏ qua bất kỳ thao tác nào như DATEADD()
được thực hiện chống lại nó.
Paul chia sẻ rằng giải pháp đơn giản là sử dụng datetime
tương đương khi tính toán ngày, trước khi chuyển đổi nó sang kiểu dữ liệu thích hợp. Trong trường hợp này, chúng ta có thể hoán đổi SYSUTCDATETIME()
và thay đổi nó thành GETUTCDATE()
:
WHERE [column] >= CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()));
Có, điều này làm mất độ chính xác một chút, nhưng một hạt bụi có thể làm chậm ngón tay của bạn trên đường nhấn
Các lần đọc tương tự vì bảng hầu như chỉ chứa dữ liệu từ năm trước, vì vậy ngay cả một lần tìm kiếm cũng trở thành một lần quét phạm vi của hầu hết bảng. Số lượng hàng không giống nhau vì (a) truy vấn thứ hai bị ngắt vào lúc nửa đêm và (b) truy vấn thứ ba bao gồm thêm một ngày dữ liệu do ngày nhuận đầu năm nay. Trong mọi trường hợp, điều này vẫn chứng tỏ cách chúng ta có thể tiến gần hơn đến các ước tính thích hợp bằng cách loại bỏ DATEADD()
, nhưng cách khắc phục thích hợp là xóa kết hợp trực tiếp của DATEADD()
và datetime2
.
Để minh họa thêm cách ước tính sai như thế nào, bạn có thể thấy rằng nếu chúng tôi chuyển các đối số và hướng khác nhau cho truy vấn ban đầu và Paul viết lại, thì số lượng hàng ước tính cho trước đây luôn dựa trên thời gian hiện tại - chúng không 'không thay đổi theo số ngày trôi qua (trong khi Paul's tương đối chính xác mọi lúc):
Các hàng thực tế cho truy vấn đầu tiên thấp hơn một chút vì điều này được thực thi sau một thời gian dài
Các ước tính không phải lúc nào cũng tốt như vậy; bảng của tôi chỉ có phân phối tương đối ổn định. Tôi điền nó bằng truy vấn sau và sau đó cập nhật thống kê bằng fullscan, trong trường hợp bạn muốn tự mình thử điều này:
-- OP's table definition: CREATE TABLE dbo.DateaddRepro ( SessionId int IDENTITY(1, 1) NOT NULL PRIMARY KEY, CreatedUtc datetime2(7) NOT NULL DEFAULT SYSUTCDATETIME() ); GO CREATE NONCLUSTERED INDEX [IX_User_Session_CreatedUtc] ON dbo.DateaddRepro(CreatedUtc) INCLUDE (SessionId); GO INSERT dbo.DateaddRepro(CreatedUtc) SELECT dt FROM ( SELECT TOP (3150000) dt = DATEADD(HOUR, (s1.[precision]-ROW_NUMBER() OVER (PARTITION BY s1.[object_id] ORDER BY s2.[object_id])) / 15, GETUTCDATE()) FROM sys.all_columns AS s1 CROSS JOIN sys.all_objects AS s2 ) AS x; UPDATE STATISTICS dbo.DateaddRepro WITH FULLSCAN; SELECT DISTINCT SessionId FROM dbo.DateaddRepro WHERE /* pick your WHERE clause to test */;
Tôi đã nhận xét về mục Connect mới và có khả năng sẽ quay lại và chỉnh sửa câu trả lời Stack Exchange của tôi.
Đạo đức của câu chuyện
Cố gắng tránh kết hợp DATEADD()
với các biểu thức mang lại datetime2
, đặc biệt là trên các phiên bản SQL Server cũ hơn (đây là phiên bản SQL Server 2012). Nó cũng có thể là một vấn đề, ngay cả trên SQL Server 2016, khi sử dụng mô hình ước tính bản số cũ hơn (do mức độ tương thích thấp hơn hoặc sử dụng rõ ràng cờ theo dõi 9481). Những vấn đề như thế này là tế nhị và không phải lúc nào cũng rõ ràng ngay lập tức, vì vậy hy vọng đây là một lời nhắc nhở (thậm chí có thể đối với tôi vào lần tới khi tôi gặp tình huống tương tự). Giống như tôi đã đề xuất trong bài viết trước, nếu bạn có các mẫu truy vấn như thế này, hãy kiểm tra xem bạn có đang nhận được ước tính chính xác hay không và ghi chú ở đâu đó để kiểm tra lại bất cứ khi nào có bất kỳ thay đổi lớn nào trong hệ thống (như nâng cấp hoặc gói dịch vụ).