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 SCHEMABINDING
tù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:
Để 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 ALTER
chức năng sẽ thiết lập lại execution_count
cho đố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 SCHEMABINDING
có 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:
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_stats
chứ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 đó:
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_TABLE
thê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_INTEGERS
vào truy vấn chính . Điều này dường như là những gì xảy ra :
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 SCHEMABINDING
tù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 SCHEMABINDING
tù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.