Tại sao có sự khác biệt về kế hoạch thực hiện giữa OFFSET 1999 FETCH và sơ đồ ROW_NUMBER kiểu cũ?


15

OFFSET ... FETCHMô hình mới giới thiệu với SQL Server 2012 cung cấp phân trang đơn giản và nhanh hơn. Tại sao có sự khác biệt nào cả khi xem xét hai hình thức này giống hệt nhau về mặt ngữ nghĩa và rất phổ biến?

Người ta sẽ cho rằng trình tối ưu hóa nhận ra cả hai và tối ưu hóa chúng (tầm thường) đến mức tối đa.

Đây là một trường hợp rất đơn giản, OFFSET ... FETCHnhanh hơn ~ 2 lần theo dự toán.

SELECT * INTO #objects FROM sys.objects

SELECT *
FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
) x
WHERE r >= 30 AND r < (30 + 10)
    ORDER BY object_id

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

offset-fetch.png

Người ta có thể thay đổi trường hợp thử nghiệm này bằng cách tạo CI trên object_idhoặc thêm bộ lọc nhưng không thể loại bỏ tất cả các khác biệt về gói. OFFSET ... FETCHluôn luôn nhanh hơn vì nó làm việc ít hơn trong thời gian thực hiện.


Không chắc lắm, vì vậy, đặt nó làm bình luận, nhưng tôi đoán nó bởi vì bạn có cùng thứ tự theo điều kiện để đánh số hàng và tập kết quả cuối cùng. Vì trong điều kiện thứ 2, trình tối ưu hóa biết điều này, nên không cần phải sắp xếp lại kết quả. Tuy nhiên, trong trường hợp đầu tiên, cần đảm bảo các kết quả từ lựa chọn bên ngoài được sắp xếp cũng như đánh số hàng trong kết quả bên trong. Tạo một chỉ mục thích hợp trên #objects sẽ giải quyết vấn đề
Akash

Câu trả lời:


13

Các ví dụ trong câu hỏi không hoàn toàn tạo ra kết quả giống nhau ( OFFSETví dụ này có lỗi do lỗi một). Các hình thức cập nhật dưới đây khắc phục vấn đề đó, loại bỏ sắp xếp bổ sung cho ROW_NUMBERtrường hợp và sử dụng các biến để làm cho giải pháp tổng quát hơn:

DECLARE 
    @PageSize bigint = 10,
    @PageNumber integer = 3;

WITH Numbered AS
(
    SELECT TOP ((@PageNumber + 1) * @PageSize) 
        o.*,
        rn = ROW_NUMBER() OVER (
            ORDER BY o.[object_id])
    FROM #objects AS o
    ORDER BY 
        o.[object_id]
)
SELECT
    x.name,
    x.[object_id],
    x.principal_id,
    x.[schema_id],
    x.parent_object_id,
    x.[type],
    x.type_desc,
    x.create_date,
    x.modify_date,
    x.is_ms_shipped,
    x.is_published,
    x.is_schema_published
FROM Numbered AS x
WHERE
    x.rn >= @PageNumber * @PageSize
    AND x.rn < ((@PageNumber + 1) * @PageSize)
ORDER BY
    x.[object_id];

SELECT
    o.name,
    o.[object_id],
    o.principal_id,
    o.[schema_id],
    o.parent_object_id,
    o.[type],
    o.type_desc,
    o.create_date,
    o.modify_date,
    o.is_ms_shipped,
    o.is_published,
    o.is_schema_published
FROM #objects AS o
ORDER BY 
    o.[object_id]
    OFFSET @PageNumber * @PageSize - 1 ROWS 
    FETCH NEXT @PageSize ROWS ONLY;

Các ROW_NUMBERkế hoạch có chi phí ước tính của 0.0197935 :

Gói số hàng

Các OFFSETkế hoạch có chi phí ước tính của 0.0196955 :

Kế hoạch bù đắp

Đó là tiết kiệm 0,000098 đơn vị chi phí ước tính (mặc dù OFFSETkế hoạch sẽ yêu cầu các nhà khai thác bổ sung nếu bạn muốn trả về một số hàng cho mỗi hàng). Các OFFSETkế hoạch vẫn sẽ hơi rẻ hơn, nói chung, nhưng hãy nhớ rằng chi phí ước tính là chính xác điều đó - thử nghiệm thực vẫn còn cần thiết. Phần lớn chi phí trong cả hai gói là chi phí của toàn bộ bộ đầu vào, vì vậy các chỉ mục hữu ích sẽ có lợi cho cả hai giải pháp.

Khi các giá trị bằng chữ không đổi được sử dụng (ví dụ OFFSET 30trong ví dụ ban đầu), trình tối ưu hóa có thể sử dụng Sắp xếp TopN thay vì sắp xếp đầy đủ theo sau là Top. Khi các hàng cần từ Sắp xếp TopN là một chữ không đổi và <= 100 (tổng OFFSETFETCH), công cụ thực thi có thể sử dụng thuật toán sắp xếp khác có thể thực hiện nhanh hơn so với sắp xếp TopN tổng quát. Tất cả ba trường hợp có đặc điểm hiệu suất tổng thể khác nhau.

Về lý do tại sao trình tối ưu hóa không tự động chuyển đổi ROW_NUMBERmẫu cú pháp để sử dụng OFFSET, có một số lý do:

  1. Hầu như không thể viết một biến đổi phù hợp với tất cả các sử dụng hiện có
  2. Có một số truy vấn phân trang tự động chuyển đổi và không có truy vấn khác có thể gây nhầm lẫn
  3. Các OFFSETkế hoạch không đảm bảo được tốt hơn trong mọi trường hợp

Một ví dụ cho điểm thứ ba ở trên xảy ra khi bộ phân trang khá rộng. Có thể hiệu quả hơn nhiều khi tìm kiếm các khóa cần thiết bằng cách sử dụng chỉ mục không bao gồm và tra cứu thủ công so với chỉ mục được nhóm so với quét chỉ mục bằng OFFSEThoặc ROW_NUMBER. Có nhiều vấn đề cần xem xét nếu ứng dụng phân trang cần biết tổng số có bao nhiêu hàng hoặc trang. Có một cuộc thảo luận tốt khác về giá trị tương đối của các phương pháp 'tìm kiếm chính' và 'bù đắp' ở đây .

Nhìn chung, có lẽ tốt hơn là mọi người đưa ra quyết định có căn cứ để thay đổi truy vấn phân trang của họ để sử dụng OFFSET, nếu phù hợp, sau khi thử nghiệm kỹ lưỡng.


1
Vì vậy, lý do cho việc chuyển đổi không được thực hiện trong các trường hợp phổ biến có lẽ là quá khó để tìm ra một sự đánh đổi kỹ thuật chấp nhận được. Bạn đã cung cấp lý do chính đáng cho lý do đó có thể là trường hợp.; Tôi phải nói rằng đây là một câu trả lời tốt. Nhiều hiểu biết và suy nghĩ mới. Tôi sẽ để câu hỏi mở một chút và sau đó chọn câu trả lời hay nhất.
usr

5

Với một chút thắc mắc về truy vấn của bạn, tôi có được ước tính chi phí bằng nhau (50/50) và số liệu thống kê IO bằng nhau:

; WITH cte AS
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
)
SELECT *
FROM cte
WHERE r >= 30 AND r < 40
ORDER BY r

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

Điều này tránh loại sắp xếp bổ sung xuất hiện trong phiên bản của bạn bằng cách sắp xếp rthay vì object_id.


Cảm ơn bạn cho cái nhìn sâu sắc này. Bây giờ tôi nghĩ về điều này, tôi đã thấy trình tối ưu hóa không hiểu bản chất được sắp xếp của đầu ra ROW_NUMBER trước đây. Nó coi tập hợp không có thứ tự bởi object_id. Hoặc ít nhất là không được sắp xếp cả theo r và object_id.
usr

2
@usr ĐẶT HÀNG THEO ROW_NUMBER () sử dụng định nghĩa cách nó gán các số. Không có gì để hứa thứ tự đầu ra - đó là riêng biệt. Nó chỉ xảy ra rằng nó thường trùng, nhưng nó không được bảo đảm.
Aaron Bertrand

@AaronBertrand Tôi hiểu rằng ROW_NUMBER không đặt hàng đầu ra. Nhưng nếu ROW_NUMBER được sắp xếp theo cùng một cột với đầu ra, thì cùng một thứ tự được đảm bảo, phải không? Vì vậy, trình tối ưu hóa truy vấn có thể sử dụng thực tế đó. Vì vậy, hai hoạt động sắp xếp luôn luôn không cần thiết trong truy vấn này.
usr

1
@usr bạn đã gặp trường hợp sử dụng phổ biến mà trình tối ưu hóa không tính đến, nhưng đó không phải là trường hợp sử dụng duy nhất . Xem xét các trường hợp trong đó thứ tự bên trong ROW_NUMBER () là cột đó và một cái gì đó khác. Hoặc khi thứ tự bên ngoài bằng cách sắp xếp thứ cấp trên một cột khác. Hoặc khi bạn muốn đặt hàng giảm dần. Hoặc bởi một cái gì đó khác hoàn toàn. Tôi thích sắp xếp theo biểu thức rthay vì cột cơ sở, nếu chỉ vì nó khớp với những gì tôi sẽ làm trong truy vấn không lồng nhau và sắp xếp theo biểu thức - tôi sẽ sử dụng bí danh được gán cho biểu thức thay vì lặp lại biểu thức.
Aaron Bertrand

4
@usr Và theo quan điểm của Paul, sẽ có trường hợp bạn có thể tìm thấy những khoảng trống về chức năng trong trình tối ưu hóa. Nếu chúng không được sửa và bạn biết cách tốt hơn để viết truy vấn, hãy sử dụng cách tốt hơn. Bệnh nhân: "Bác sĩ ơi, đau lắm khi tôi làm x." Bác sĩ: "Đừng làm x." :-)
Aaron Bertrand

-3

Họ đã sửa đổi trình tối ưu hóa truy vấn để thêm tính năng này. Có nghĩa là họ đã triển khai các cơ chế đặc biệt để hỗ trợ lệnh ... tìm nạp. Nói cách khác, đối với truy vấn hàng đầu, SQL Server phải thực hiện nhiều công việc hơn. Do đó, sự khác biệt trong kế hoạch truy vấn.

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.