Vấn đề tối ưu hóa với chức năng do người dùng xác định


26

Tôi có một vấn đề hiểu tại sao máy chủ SQL quyết định gọi hàm do người dùng xác định cho mọi giá trị trong bảng mặc dù chỉ nên tìm nạp một hàng. SQL thực tế phức tạp hơn nhiều, nhưng tôi đã có thể giảm vấn đề xuống đây:

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Đối với truy vấn này, SQL Server quyết định gọi hàm GetgroupCode cho mọi giá trị duy nhất tồn tại trong Bảng SẢN PHẨM, mặc dù ước tính và số lượng hàng thực tế được trả về từ ORDERLINE là 1 (đó là khóa chính):

Kế hoạch truy vấn

Cùng một kế hoạch trong kế hoạch thám hiểm hiển thị số hàng:

Kế hoạch thám hiểm Những cái bàn:

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

Chỉ số đang được sử dụng để quét là:

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

Hàm thực sự phức tạp hơn một chút, nhưng điều tương tự cũng xảy ra với hàm đa câu giả như thế này:

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

Tôi đã có thể "sửa" hiệu suất bằng cách buộc máy chủ SQL tìm nạp 1 sản phẩm hàng đầu, mặc dù 1 là tối đa có thể tìm thấy:

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Sau đó, hình dạng kế hoạch cũng thay đổi thành một cái gì đó mà tôi mong đợi ban đầu:

Kế hoạch truy vấn với đầu trang

Tôi cũng mặc dù rằng chỉ mục PRODUCT_FACTORY nhỏ hơn chỉ mục được phân cụm, SẢN PHẨM sẽ có ảnh hưởng, nhưng ngay cả khi buộc truy vấn phải sử dụng PRODUCT_PK, kế hoạch vẫn giống như ban đầu, với 6655 lệnh gọi đến hàm.

Nếu tôi hoàn toàn rời khỏi ORDERHDR, thì kế hoạch bắt đầu với vòng lặp lồng nhau giữa ORDERLINE và SẢN PHẨM trước tiên và chức năng chỉ được gọi một lần.

Tôi muốn hiểu lý do của việc này là gì vì tất cả các thao tác được thực hiện bằng khóa chính và cách khắc phục nếu xảy ra trong một truy vấn phức tạp hơn không thể giải quyết dễ dàng.

Chỉnh sửa: Tạo báo cáo bảng:

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)

Câu trả lời:


30

Có ba lý do kỹ thuật chính bạn có được kế hoạch bạn làm:

  1. Khung chi phí của trình tối ưu hóa không có hỗ trợ thực sự cho các chức năng phi nội tuyến. Nó không thực hiện bất kỳ nỗ lực nào để xem bên trong định nghĩa hàm để xem nó có thể đắt đến mức nào, nó chỉ gán một chi phí cố định rất nhỏ và ước tính hàm sẽ tạo ra 1 hàng đầu ra mỗi lần nó được gọi. Cả hai giả định mô hình này thường rất không an toàn. Tình hình được cải thiện đôi chút trong năm 2014 với công cụ ước tính cardinality mới được kích hoạt do dự đoán 1 hàng cố định được thay thế bằng dự đoán 100 hàng cố định. Tuy nhiên, vẫn không có hỗ trợ cho chi phí nội dung của các chức năng phi nội tuyến.
  2. SQL Server ban đầu thu gọn các phép nối và áp dụng vào một phép nối logic n-ary nội bộ duy nhất. Điều này giúp lý do tối ưu hóa về việc tham gia đơn hàng sau này. Việc mở rộng n-ary tham gia vào các đơn đặt hàng tham gia ứng cử viên đến sau, và chủ yếu dựa trên kinh nghiệm. Ví dụ, các phép nối bên trong xuất hiện trước các phép nối ngoài, các bảng nhỏ và các phép nối chọn lọc trước các bảng lớn và các phép nối ít chọn lọc hơn, v.v.
  3. Khi SQL Server thực hiện tối ưu hóa dựa trên chi phí, nó sẽ chia nỗ lực thành các giai đoạn tùy chọn để giảm thiểu cơ hội chi tiêu quá lâu để tối ưu hóa các truy vấn chi phí thấp. Có ba giai đoạn chính, tìm kiếm 0, tìm kiếm 1 và tìm kiếm 2. Mỗi giai đoạn có điều kiện nhập cảnh và các giai đoạn sau cho phép khám phá tối ưu hóa nhiều hơn các giai đoạn trước. Truy vấn của bạn xảy ra để đủ điều kiện cho giai đoạn tìm kiếm có khả năng thấp nhất, giai đoạn 0. Một kế hoạch chi phí đủ thấp được tìm thấy ở đó mà các giai đoạn sau không được nhập.

Với ước tính số lượng thẻ nhỏ được gán cho UDF được áp dụng, các heuristic mở rộng tham gia n-ary không may định vị lại nó sớm hơn trong cây hơn bạn mong muốn.

Truy vấn cũng đủ điều kiện để tối ưu hóa tìm kiếm 0 nhờ có ít nhất ba tham gia (bao gồm cả áp dụng). Kế hoạch vật lý cuối cùng bạn nhận được, với lần quét trông kỳ quặc, dựa trên thứ tự tham gia được suy luận theo phương pháp heuristur. Nó có chi phí đủ thấp để trình tối ưu hóa xem xét kế hoạch "đủ tốt". Dự toán chi phí thấp và cardinality cho UDF đóng góp cho kết thúc sớm này.

Tìm kiếm 0 (còn được gọi là giai đoạn Xử lý giao dịch) nhắm mục tiêu các truy vấn loại OLTP có số lượng thẻ thấp, với các gói cuối cùng thường có các vòng lặp lồng nhau tham gia. Quan trọng hơn, tìm kiếm 0 chỉ chạy một tập hợp con tương đối nhỏ trong khả năng khám phá của trình tối ưu hóa. Tập hợp con này không bao gồm việc kéo một ứng dụng lên cây truy vấn qua một phép nối (quy tắc PullApplyOverJoin). Đây chính xác là những gì được yêu cầu trong trường hợp thử nghiệm để định vị lại UDF áp dụng phía trên các phép nối, để xuất hiện lần cuối trong chuỗi các thao tác (như đã từng).

Ngoài ra còn có một vấn đề trong đó trình tối ưu hóa có thể quyết định giữa các vòng lặp lồng nhau ngây thơ (tham số tham gia vào chính liên kết) và tham gia được lập chỉ mục tương quan (áp dụng) trong đó vị từ tương quan được áp dụng ở phía bên trong của phép nối bằng cách tìm kiếm chỉ mục. Cái sau thường là hình dạng kế hoạch mong muốn, nhưng trình tối ưu hóa có khả năng khám phá cả hai. Với ước tính chi phí và số lượng thẻ không chính xác, nó có thể chọn tham gia NL không áp dụng, như trong các kế hoạch đã gửi (giải thích việc quét).

Vì vậy, có nhiều lý do tương tác liên quan đến một số tính năng tối ưu hóa chung thường hoạt động tốt để tìm các kế hoạch tốt trong một khoảng thời gian ngắn mà không sử dụng quá nhiều tài nguyên. Tránh bất kỳ một trong những lý do là đủ để tạo ra hình dạng kế hoạch 'dự kiến' cho truy vấn mẫu, ngay cả với các bảng trống:

Lập kế hoạch trên các bảng trống với tìm kiếm 0 bị vô hiệu hóa

Không có cách nào được hỗ trợ để tránh lựa chọn gói 0 tìm kiếm, chấm dứt tối ưu hóa sớm hoặc để cải thiện chi phí của UDF (ngoài các cải tiến hạn chế trong mô hình SQL Server 2014 CE cho việc này). Điều này để lại những thứ như hướng dẫn kế hoạch, viết lại truy vấn thủ công (bao gồm cả TOP (1)ý tưởng hoặc sử dụng các bảng tạm thời trung gian) và tránh các 'hộp đen' có chi phí thấp (từ quan điểm QO) như các hàm không nội tuyến.

Viết lại CROSS APPLYnhư OUTER APPLYcũng có thể làm việc, vì nó hiện đang ngăn chặn một số công việc đầu tham gia-sụp đổ, nhưng bạn phải cẩn thận để bảo toàn ngữ nghĩa truy vấn ban đầu (ví dụ như từ chối bất kỳ NULLhàng -Extended mà có thể được giới thiệu, nếu không có sự ưu sụp đổ trở lại một áp dụng chéo). Bạn cần lưu ý rằng mặc dù hành vi này không được đảm bảo duy trì ổn định, do đó bạn cần nhớ kiểm tra lại mọi hành vi được quan sát như vậy mỗi khi bạn vá hoặc nâng cấp Máy chủ SQL.

Nhìn chung, giải pháp phù hợp cho bạn phụ thuộc vào nhiều yếu tố mà chúng tôi không thể đánh giá cho bạn. Tuy nhiên, tôi sẽ khuyến khích bạn xem xét các giải pháp được đảm bảo sẽ luôn hoạt động trong tương lai và hoạt động với (chứ không phải chống lại) trình tối ưu hóa bất cứ khi nào có thể.


24

Có vẻ như đây là một quyết định dựa trên chi phí của trình tối ưu hóa nhưng là một quyết định khá tệ.

Nếu bạn thêm 50000 hàng vào SẢN PHẨM, trình tối ưu hóa cho rằng quá trình quét quá nhiều công việc và cung cấp cho bạn một kế hoạch với ba lần tìm kiếm và một cuộc gọi đến UDF.

Gói tôi nhận được cho 6655 hàng trong SẢN PHẨM

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

Thay vào đó, với 50000 hàng trong SẢN PHẨM, tôi có kế hoạch này.

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

Tôi đoán chi phí cho việc gọi UDF được đánh giá thấp.

Một cách giải quyết khác hoạt động tốt trong trường hợp này là thay đổi truy vấn để sử dụng áp dụng bên ngoài đối với UDF. Tôi nhận được kế hoạch tốt cho dù có bao nhiêu hàng trong bảng SẢN PHẨM.

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

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

Cách giải quyết tốt nhất trong trường hợp của bạn có lẽ là lấy các giá trị bạn cần vào bảng tạm thời và sau đó truy vấn bảng tạm thời với một chữ thập áp dụng cho UDF. Bằng cách đó, bạn chắc chắn rằng UDF sẽ không được thực thi nhiều hơn mức cần thiết.

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

Thay vì kiên trì với bảng tạm thời, bạn có thể sử dụng top()trong bảng dẫn xuất để buộc SQL Server đánh giá kết quả từ các phép nối trước khi gọi UDF. Chỉ cần sử dụng một số thực sự cao trong SQL Server hàng đầu phải tính các hàng của bạn cho phần truy vấn đó trước khi có thể tiếp tục và sử dụng UDF.

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

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

Tôi muốn hiểu lý do của việc này là gì vì tất cả các thao tác được thực hiện bằng khóa chính và cách khắc phục nếu xảy ra trong một truy vấn phức tạp hơn không thể giải quyết dễ dàng.

Tôi thực sự không thể trả lời nhưng nghĩ rằng tôi nên chia sẻ những gì tôi biết bằng mọi cách. Tôi không biết tại sao việc quét bảng SẢN PHẨM được xem xét cả. Có thể có những trường hợp đó là điều tốt nhất để làm và có những điều liên quan đến cách các trình tối ưu hóa đối xử với UDF mà tôi không biết.

Một quan sát thêm là truy vấn của bạn có một kế hoạch tốt trong SQL Server 2014 với công cụ ước tính cardinality mới. Đó là bởi vì số lượng hàng ước tính cho mỗi cuộc gọi đến UDF là 100 thay vì 1 như trong SQL Server 2012 trở về trước. Nhưng nó vẫn sẽ đưa ra quyết định dựa trên chi phí giống nhau giữa phiên bản quét và phiên bản tìm kiếm của kế hoạch. Với ít hơn 500 (497 trong trường hợp của tôi) trong SẢN PHẨM, bạn có được phiên bản quét của gói ngay cả trong SQL Server 2014.


2
Bằng cách nào đó làm tôi nhớ đến phiên của Adam Machanic tại SQL Bits: sqlbits.com/Simes/Event14/ mẹo
James Z
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.