Cách cải thiện ước tính 1 hàng trong Chế độ xem bị ràng buộc bởi DateAdd () so với chỉ mục


8

Sử dụng Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64).

Đưa ra một bảng và chỉ mục:

create table [User].[Session] 
(
  SessionId int identity(1, 1) not null primary key
  CreatedUtc datetime2(7) not null default sysutcdatetime())
)

create nonclustered index [IX_User_Session_CreatedUtc]
on [User].[Session]([CreatedUtc]) include (SessionId)

Hàng thực tế cho mỗi truy vấn sau là 3,1M, các hàng ước tính được hiển thị dưới dạng nhận xét.

Khi các truy vấn này cung cấp một truy vấn khác trong Chế độ xem , trình tối ưu hóa sẽ chọn tham gia vòng lặp vì ước tính 1 hàng. Làm cách nào để cải thiện ước tính ở mức mặt đất này để tránh ghi đè truy vấn cha mẹ tham gia gợi ý hoặc dùng đến SP?

Sử dụng một ngày mã hóa hoạt động tuyệt vời:

 select distinct SessionId from [User].Session -- 2.9M (great)
  where CreatedUtc > '04/08/2015'  -- but hardcoded

Các truy vấn tương đương này tương thích với xem nhưng tất cả ước tính 1 hàng:

select distinct SessionId from [User].Session -- 1
 where CreatedUtc > dateadd(day, -365, sysutcdatetime())         

select distinct SessionId from [User].Session  -- 1
 where dateadd(day, 365, CreatedUtc) > sysutcdatetime();          

select distinct SessionId from [User].Session s  -- 1
 inner loop join  (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
    on d.MinCreatedUtc < s.CreatedUtc    
    -- (also tried reversing join order, not shown, no change)

select distinct SessionId from [User].Session s -- 1
 cross apply (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 where d.MinCreatedUtc < s.CreatedUtc
    -- (also tried reversing join order, not shown, no change)

Hãy thử một số gợi ý (nhưng N / A để xem):

 select distinct SessionId from [User].Session -- 1
  where CreatedUtc > dateadd(day, -365, sysutcdatetime())
 option (recompile);

select distinct SessionId from [User].Session  -- 1
 where CreatedUtc > (select dateadd(day, -365, sysutcdatetime()))
 option (recompile, optimize for unknown);

select distinct SessionId                     -- 1
  from (select dateadd(day, -365, sysutcdatetime()) as MinCreatedUtc) d
 inner loop join [User].Session s    
    on s.CreatedUtc > d.MinCreatedUtc  
option (recompile);

Hãy thử sử dụng Thông số / Gợi ý (nhưng N / A để xem):

declare
    @minDate datetime2(7) = dateadd(day, -365, sysutcdatetime());

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate;

select distinct SessionId from [User].Session  -- 2.96M (great)
 where CreatedUtc > @minDate
option (recompile);

select distinct SessionId from [User].Session  -- 1.2M (adequate)
 where CreatedUtc > @minDate
option (optimize for unknown);

Ước tính so với thực tế

Các số liệu thống kê được cập nhật.

DBCC SHOW_STATISTICS('user.Session', 'IX_User_Session_CreatedUtc') with histogram;

Một số hàng cuối cùng của biểu đồ (tổng số 189 hàng) được hiển thị:

nhập mô tả hình ảnh ở đây

Câu trả lời:


6

Một câu trả lời ít toàn diện hơn Aaron nhưng vấn đề cốt lõi là lỗi ước tính cardinality DATEADDkhi sử dụng loại datetime2 :

Kết nối: Ước tính không chính xác khi sysdatetime xuất hiện trong biểu thức dateadd ()

Một cách giải quyết khác là sử dụng GETUTCDATE(trả về datetime):

WHERE CreatedUtc > CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()))

Lưu ý việc chuyển đổi sang datetime2 phải ở bên ngoài DATEADDđể tránh lỗi.

Vấn đề về ước tính cardinality 1 hàng sao chép cho tôi trong tất cả các phiên bản SQL Server cho đến và bao gồm cả RC0 2016 trong đó công cụ ước tính cardinality mô hình 70 được sử dụng.

Aaron Bertrand đã viết một bài viết về điều này cho SQLPerformance.com:


6

Trong một số trường hợp, SQL Server có thể có các ước tính thực sự cho DATEADD/ DATEDIFF, tùy thuộc vào các đối số là gì và dữ liệu thực tế của bạn trông như thế nào. Tôi đã viết về điều này DATEDIFFkhi làm việc với đầu tháng và một số cách giải quyết ở đây:

Nhưng, lời khuyên tiêu biểu của tôi là chỉ dừng sử dụng DATEADD/ DATEDIFFtrong đó / tham gia mệnh đề.

Cách tiếp cận sau đây, mặc dù không chính xác khi một năm nhuận nằm trong phạm vi được lọc (nó sẽ bao gồm thêm một ngày trong trường hợp đó), và trong khi làm tròn đến ngày, sẽ có được ước tính tốt hơn (nhưng vẫn không tuyệt vời!), Giống như bạn không thể DATEDIFFchống lại cách tiếp cận cột và vẫn cho phép tìm kiếm được sử dụng:

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  DAY(GETUTCDATE())
);

SELECT ... WHERE CreatedUtc >= @start;

Bạn có thể thao tác các yếu tố đầu vào để DATEFROMPARTStránh các vấn đề trong ngày nhuận, sử dụng DATETIMEFROMPARTSđể có độ chính xác cao hơn thay vì làm tròn đến ngày, v.v ... Điều này chỉ để chứng minh rằng bạn có thể nhập một biến với một ngày trong quá khứ mà không cần sử dụng DATEADD(đó chỉ là một ít việc hơn) và do đó tránh được phần làm tê liệt hơn của lỗi ước tính (được sửa trong năm 2014+).

Để tránh lỗi vào ngày nhuận, bạn có thể thực hiện việc này thay vào đó, bắt đầu từ ngày 28 tháng 2 năm ngoái thay vì 29:

DECLARE @start date = DATEFROMPARTS
(
  YEAR(GETUTCDATE())-1, 
  MONTH(GETUTCDATE()), 
  CASE WHEN DAY(GETUTCDATE()) = 29 AND MONTH(GETUTCDATE()) = 2 
    THEN 28 ELSE DAY(GETUTCDATE()) END
);

Bạn cũng có thể nói thêm một ngày bằng cách kiểm tra xem liệu chúng ta có vượt qua một ngày nhuận trong năm nay không và nếu có, hãy thêm một ngày vào đầu (thú vị là sử dụng DATEADD ở đây vẫn cho phép ước tính chính xác):

DECLARE @base date = GETUTCDATE();
IF GETUTCDATE() >= DATEFROMPARTS(YEAR(GETUTCDATE()),3,1) AND 
  TRY_CONVERT(datetime, DATEFROMPARTS(YEAR(GETUTCDATE()),2,29)) IS NOT NULL
BEGIN
  SET @base = DATEADD(DAY, 1, GETUTCDATE());
END

DECLARE @start date = DATEFROMPARTS
(
  YEAR(@base)-1, 
  MONTH(@base),
  CASE WHEN DAY(@base) = 29 AND MONTH(@base) = 2 
    THEN 28 ELSE DAY(@base) END
);

SELECT ... WHERE CreatedUtc >= @start;

Nếu bạn cần chính xác hơn so với ban ngày vào lúc nửa đêm, thì bạn chỉ cần thêm thao tác trước khi chọn:

DECLARE @accurate_start datetime2(7) = DATETIME2FROMPARTS
(
  YEAR(@start), MONTH(@start), DAY(@start),
  DATEPART(HOUR,  SYSUTCDATETIME()), 
  DATEPART(MINUTE,SYSUTCDATETIME()),
  DATEPART(SECOND,SYSUTCDATETIME()), 
  0,0
);

SELECT ... WHERE CreatedUtc >= @accurate_start;

Bây giờ, bạn có thể giải quyết tất cả những điều này trong một chế độ xem và nó vẫn sẽ sử dụng tìm kiếm và ước tính 30% mà không yêu cầu bất kỳ gợi ý hoặc cờ theo dõi nào, nhưng nó không đẹp. Các CTE lồng nhau chỉ để tôi không phải gõ SYSUTCDATETIME()hàng trăm lần hoặc lặp lại các biểu thức được sử dụng lại - chúng vẫn có thể được đánh giá nhiều lần.

CREATE VIEW dbo.v5 
AS
  WITH d(d) AS ( SELECT SYSUTCDATETIME() ),
  base(d) AS
  (
    SELECT DATEADD(DAY,CASE WHEN d >= DATEFROMPARTS(YEAR(d),3,1) 
      AND TRY_CONVERT(datetime,RTRIM(YEAR(d))+RIGHT('0'+RTRIM(MONTH(d)),2)
      +RIGHT('0'+RTRIM(DAY(d)),2)) IS NOT NULL THEN 1 ELSE 0 END, d)
    FROM d
  ),
  src(d) AS
  (
    SELECT DATETIME2FROMPARTS
    (
      YEAR(d)-1, 
      MONTH(d),
      CASE WHEN MONTH(d) = 2 AND DAY(d) = 29
        THEN 28 ELSE DAY(d) END,
      DATEPART(HOUR,d), 
      DATEPART(MINUTE,d),
      DATEPART(SECOND,d),
      10*DATEPART(MICROSECOND,d),
      7
    ) FROM base
  )
  SELECT DISTINCT SessionId FROM [User].[Session]
    WHERE CreatedUtc >= (SELECT d FROM src);

Điều này dài dòng hơn nhiều so DATEDIFFvới cột của bạn , nhưng như tôi đã đề cập trong một bình luận , cách tiếp cận đó không có khả năng và có thể sẽ thực hiện một cách cạnh tranh trong khi hầu hết các bảng phải được đọc, nhưng tôi nghi ngờ nó sẽ trở thành gánh nặng vì "năm ngoái" trở thành tỷ lệ phần trăm thấp hơn của bảng.

Ngoài ra, chỉ để tham khảo, đây là một số số liệu tôi có được khi tôi cố gắng sao chép:

nhập mô tả hình ảnh ở đây

Tôi không thể có được ước tính 1 hàng và tôi đã rất cố gắng để phù hợp với phân phối của bạn (3,13 triệu hàng, 2,89 triệu so với năm ngoái). Nhưng bạn có thể thấy:

  • cả hai giải pháp của chúng tôi thực hiện đọc gần tương đương.
  • giải pháp của bạn kém chính xác hơn một chút vì nó chỉ chiếm ranh giới ngày (và điều đó có thể ổn, quan điểm của tôi có thể được thực hiện ít chính xác hơn để phù hợp).
  • 4199 + biên dịch lại không thực sự thay đổi các ước tính (hoặc các kế hoạch).

Đừng rút quá nhiều từ các số liệu thời lượng - hiện tại họ đang ở gần, nhưng có thể không ở gần khi bảng phát triển (một lần nữa, tôi tin vì ngay cả khi tìm kiếm vẫn phải đọc hầu hết bảng).

Dưới đây là các kế hoạch cho v4 (ngày tháng của bạn so với cột) và v5 (phiên bản của tôi):

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây


Tóm lại, như đã nêu trong blog của bạn . câu trả lời này cung cấp một ước tính có thể sử dụng và tìm kiếm kế hoạch dựa trên. Câu trả lời của @PaulWhite đưa ra ước tính tốt nhất. Có lẽ ước tính 1 hàng tôi nhận được (so với 1500) có thể là do bảng không có bất kỳ hàng nào trong ~ 24 giờ qua.
crokusek

@crokusek Nếu bạn nói >= DATEADD(DAY, -365, SYSDATETIME())lỗi là ước tính dựa trên >= SYSDATETIME(). Vì vậy, về mặt kỹ thuật ước tính dựa trên số lượng hàng trong bảng CreatedUtctrong tương lai. Đây có thể là 0, nhưng SQL Server luôn làm tròn 0 đến 1 cho các hàng ước tính.
Aaron Bertrand

1

Thay thế dateadd () bằng dateiff () để có được xấp xỉ đầy đủ (30% ish).

 select distinct SessionId from [User].Session     -- 1.2M est, 3.0M act.
  where datediff(day, CreatedUtc, sysutcdatetime()) <= 365

Đây có vẻ là một lỗi tương tự như MS Connect 630583 .

Tùy chọn biên dịch lại làm cho không có sự khác biệt.

Kế hoạch thống kê


2
Lưu ý rằng việc áp dụng dateiff cho cột làm cho biểu thức không thể mở rộng được, vì vậy bạn sẽ phải quét. Điều này có thể ổn khi 90 +% bảng cần được đọc bằng mọi giá, nhưng khi bảng càng lớn thì điều này sẽ càng tốn kém hơn.
Aaron Bertrand

Điểm tuyệt vời. Tôi đã nghĩ rằng nó có thể chuyển đổi nó trong nội bộ. Xác nhận rằng nó đang thực hiện quét.
crokusek
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.