Làm thế nào tôi có thể buộc một UDF vô hướng được đánh giá chỉ một lần trong một truy vấn?


12

Tôi có một truy vấn cần lọc theo kết quả của UDF vô hướng. Truy vấn phải được gửi dưới dạng một câu lệnh (vì vậy tôi không thể gán kết quả UDF cho một biến cục bộ) và tôi không thể sử dụng TVF. Tôi nhận thức được các vấn đề về hiệu suất gây ra bởi các UDF vô hướng, bao gồm việc buộc toàn bộ kế hoạch phải được điều hành một cách an toàn, cấp bộ nhớ quá mức, các vấn đề ước tính về tim mạch và thiếu nội tuyến. Đối với câu hỏi này, giả sử rằng tôi cần sử dụng UDF vô hướng.

Bản thân UDF khá tốn kém để gọi nhưng về lý thuyết, các truy vấn có thể được trình tối ưu hóa triển khai một cách hợp lý theo cách mà hàm chỉ cần được tính một lần. Tôi đã chế nhạo một ví dụ rất đơn giản cho câu hỏi này. Truy vấn sau đây mất 6152 ms để thực thi trên máy của tôi:

SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();

Toán tử bộ lọc trong kế hoạch truy vấn cho thấy rằng hàm được đánh giá một lần cho mỗi hàng:

kế hoạch truy vấn 1

DDL và chuẩn bị dữ liệu:

CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
AS
BEGIN
    DECLARE @tbl TABLE (VAL VARCHAR(5));

    -- make the function expensive to call
    INSERT INTO @tbl
    SELECT [VALUE]
    FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');

    RETURN 1;
END;

GO

DROP TABLE IF EXISTS dbo.X_100_INTEGERS;

CREATE TABLE dbo.X_100_INTEGERS (ID INT NOT NULL);

-- insert 100 integers from 1 - 100
WITH
    L0   AS(SELECT 1 AS c UNION ALL SELECT 1),
    L1   AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
    L2   AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
    L3   AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
    L4   AS(SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
    L5   AS(SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
    Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM L5)
INSERT INTO dbo.X_100_INTEGERS WITH (TABLOCK)
SELECT n FROM Nums WHERE n <= 100;

Đây là một liên kết db fiddle cho ví dụ trên, mặc dù mã mất khoảng 18 giây để thực thi ở đó.

Trong một số trường hợp, tôi không thể chỉnh sửa mã của hàm vì nó được cung cấp bởi nhà cung cấp. Trong các trường hợp khác, tôi có thể thay đổi. Làm thế nào tôi có thể buộc một UDF vô hướng được đánh giá chỉ một lần trong một truy vấn?

Câu trả lời:


17

Cuối cùng, không thể buộc SQL Server đánh giá UDF vô hướng chỉ một lần trong một truy vấn. Tuy nhiên, có một số bước có thể được thực hiện để khuyến khích nó. Với thử nghiệm tôi tin rằng bạn có thể có được thứ gì đó hoạt động với phiên bản SQL Server hiện tại, nhưng có thể những thay đổi trong tương lai sẽ yêu cầu bạn xem lại mã của mình.

Nếu có thể chỉnh sửa mã, điều tốt đầu tiên cần thử là làm cho hàm xác định nếu có thể. Paul White chỉ ra ở đây rằng hàm phải được tạo bằng SCHEMABINDINGtùy chọn và chính mã chức năng phải có tính xác định.

Sau khi thực hiện thay đổi sau:

CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
    DECLARE @tbl TABLE (VAL VARCHAR(5));

    -- make the function expensive to call
    INSERT INTO @tbl
    SELECT [VALUE]
    FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');

    RETURN 1;
END;

Truy vấn từ câu hỏi được thực hiện trong 64 ms:

SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();

Gói truy vấn không còn có toán tử lọc:

kế hoạch truy vấn 1

Để chắc chắn rằng nó chỉ thực hiện một lần chúng ta có thể sử dụng sys.dm_exec_feft_stats DMV mới được phát hành trong SQL Server 2016:

SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');

Phát hành một ALTERchức năng sẽ thiết lập lại execution_countcho đối tượng đó. Truy vấn trên trả về 1 có nghĩa là hàm chỉ được thực hiện một lần.

Lưu ý rằng chỉ vì hàm có tính xác định không có nghĩa là nó sẽ chỉ được đánh giá một lần cho bất kỳ truy vấn nào. Trong thực tế, đối với một số truy vấn thêm SCHEMABINDINGcó thể làm giảm hiệu suất. Hãy xem xét các truy vấn sau:

WITH cte (UDF_VALUE) AS
(
    SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;

Phần thừa DISTINCTđã được thêm vào để thoát khỏi toán tử Filter. Kế hoạch có vẻ đầy hứa hẹn:

kế hoạch truy vấn 2

Dựa vào đó, người ta sẽ mong muốn UDF được đánh giá một lần và được sử dụng làm bảng bên ngoài trong phép nối vòng lặp lồng nhau. Tuy nhiên, truy vấn mất 6446 ms để chạy trên máy của tôi. Theo sys.dm_exec_function_statschức năng đã được thực hiện 100 lần. Làm thế nào là điều đó có thể? Trong " Tính toán vô hướng, biểu thức và hiệu suất kế hoạch thực hiện ", Paul White chỉ ra rằng toán tử vô hướng tính toán có thể được hoãn lại:

Thường xuyên hơn không, một tính toán vô hướng đơn giản xác định một biểu thức; tính toán thực tế được hoãn lại cho đến khi một cái gì đó sau này trong kế hoạch thực hiện cần kết quả.

Đối với truy vấn này, có vẻ như cuộc gọi UDF đã bị hoãn lại cho đến khi cần, tại thời điểm đó nó được đánh giá 100 lần.

Thật thú vị, ví dụ CTE thực thi trong 71 ms trên máy của tôi khi UDF không được xác định SCHEMABINDING, như trong câu hỏi ban đầu. Hàm chỉ được thực hiện một lần khi truy vấn được chạy. Đây là kế hoạch truy vấn cho điều đó:

kế hoạch truy vấn 3

Không rõ tại sao Compute Scalar không được hoãn lại. Có thể là do tính không đặc trưng của hàm giới hạn việc sắp xếp lại các toán tử mà trình tối ưu hóa truy vấn có thể thực hiện.

Một cách tiếp cận khác là thêm một bảng nhỏ vào CTE và truy vấn hàng duy nhất trong bảng đó. Bất kỳ bảng nhỏ nào cũng được, nhưng hãy sử dụng như sau:

CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);

INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);

Truy vấn sau đó trở thành:

WITH cte (UDF_VALUE) AS
(       
    SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
    FROM dbo.X_ONE_ROW_TABLE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;

Việc bổ sung dbo.X_ONE_ROW_TABLEthêm sự không chắc chắn cho trình tối ưu hóa. Nếu bảng có 0 hàng thì CTE sẽ trả về 0 hàng. Trong mọi trường hợp, trình tối ưu hóa không thể đảm bảo rằng CTE sẽ trả về một hàng nếu UDF không mang tính xác định, do đó có vẻ như UDF sẽ được đánh giá trước khi tham gia. Tôi mong muốn trình tối ưu hóa quét dbo.X_ONE_ROW_TABLE, sử dụng tổng hợp luồng để lấy giá trị tối đa của một hàng được trả về (yêu cầu hàm được ước tính) và sử dụng đó làm bảng bên ngoài cho một vòng lặp lồng nhau tham gia dbo.X_100_INTEGERSvào truy vấn chính . Điều này dường như là những gì xảy ra :

kế hoạch truy vấn 4

Truy vấn thực hiện trong khoảng 110 ms trên máy của tôi và UDF chỉ được đánh giá một lần theo sys.dm_exec_function_stats. Sẽ không đúng khi nói rằng trình tối ưu hóa truy vấn buộc phải đánh giá UDF chỉ một lần. Tuy nhiên, thật khó để tưởng tượng một trình viết lại tối ưu hóa sẽ dẫn đến một truy vấn chi phí thấp hơn, ngay cả với những hạn chế xung quanh UDF và tính toán chi phí vô hướng.

Tóm lại, đối với các hàm xác định (phải bao gồm SCHEMABINDINGtùy chọn) hãy thử viết truy vấn theo cách đơn giản nhất có thể. Nếu trên SQL Server 2016 hoặc phiên bản mới hơn, hãy xác nhận rằng chức năng chỉ được thực hiện một lần bằng cách sử dụng sys.dm_exec_function_stats. Kế hoạch thực hiện có thể gây hiểu nhầm về vấn đề đó.

Đối với các chức năng không được SQL Server coi là có tính xác định, bao gồm mọi thứ thiếu SCHEMABINDINGtùy chọn, một cách tiếp cận là đặt UDF trong bảng CTE được tạo cẩn thận hoặc bảng dẫn xuất. Điều này đòi hỏi một chút cẩn thận nhưng cùng một CTE có thể hoạt động cho cả hai chức năng xác định và không xác định.

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.