UDF vô hướng luôn là con dao hai lưỡi - chúng tuyệt vời cho các nhà phát triển, những người có thể loại bỏ logic tẻ nhạt thay vì lặp lại nó trong các truy vấn của họ, nhưng chúng rất tệ đối với hiệu suất thời gian chạy trong sản xuất, bởi vì trình tối ưu hóa không ' t xử lý chúng tốt. Về cơ bản những gì sẽ xảy ra là các thực thi UDF được giữ tách biệt với phần còn lại của kế hoạch thực thi và do đó chúng được gọi một lần cho mỗi hàng và không thể được tối ưu hóa dựa trên số hàng ước tính hoặc thực tế hoặc được xếp vào phần còn lại của kế hoạch.
Vì, bất chấp những nỗ lực hết mình kể từ SQL Server 2000, chúng tôi không thể ngăn việc sử dụng các UDF vô hướng một cách hiệu quả, sẽ không tuyệt khi làm cho SQL Server đơn giản xử lý chúng tốt hơn phải không?
SQL Server 2019 giới thiệu một tính năng mới được gọi là Nội tuyến UDF vô hướng. Thay vì giữ chức năng riêng biệt, nó được kết hợp vào kế hoạch tổng thể. Điều này dẫn đến một kế hoạch thực thi tốt hơn nhiều và do đó, hiệu suất thời gian chạy tốt hơn.
Nhưng trước tiên, để minh họa rõ hơn nguồn gốc của vấn đề, hãy bắt đầu với một cặp bảng đơn giản chỉ có vài hàng, trong cơ sở dữ liệu chạy trên SQL Server 2017 (hoặc trên 2019 nhưng có mức độ tương thích thấp hơn):
CREATE DATABASE Whatever; GO ALTER DATABASE Whatever SET COMPATIBILITY_LEVEL = 140; GO USE Whatever; GO CREATE TABLE dbo.Languages ( LanguageID int PRIMARY KEY, Name sysname ); CREATE TABLE dbo.Employees ( EmployeeID int PRIMARY KEY, LanguageID int NOT NULL FOREIGN KEY REFERENCES dbo.Languages(LanguageID) ); INSERT dbo.Languages(LanguageID, Name) VALUES(1033, N'English'), (45555, N'Klingon'); INSERT dbo.Employees(EmployeeID, LanguageID) SELECT [object_id], CASE ABS([object_id]%2) WHEN 1 THEN 1033 ELSE 45555 END FROM sys.all_objects;
Bây giờ, chúng tôi có một truy vấn đơn giản mà chúng tôi muốn hiển thị từng nhân viên và tên ngôn ngữ chính của họ. Giả sử truy vấn này được sử dụng ở rất nhiều nơi và / hoặc theo nhiều cách khác nhau, vì vậy, thay vì xây dựng một phép nối vào truy vấn, chúng tôi viết một UDF vô hướng để trừu tượng hóa phép nối đó:
CREATE FUNCTION dbo.GetLanguage(@id int) RETURNS sysname AS BEGIN RETURN (SELECT Name FROM dbo.Languages WHERE LanguageID = @id); END
Sau đó, truy vấn thực tế của chúng tôi trông giống như sau:
SELECT TOP (6) EmployeeID, Language = dbo.GetLanguage(LanguageID) FROM dbo.Employees;
Nếu chúng ta xem xét kế hoạch thực thi cho truy vấn, điều gì đó bị thiếu một cách kỳ lạ:
Kế hoạch thực thi hiển thị quyền truy cập vào Nhân viên nhưng không truy cập vào Ngôn ngữ
Bảng Ngôn ngữ được truy cập như thế nào? Kế hoạch này trông rất hiệu quả bởi vì - giống như bản thân hàm - nó đang trừu tượng hóa một số sự phức tạp liên quan. Trên thực tế, sơ đồ đồ họa này giống với một truy vấn chỉ gán một hằng số hoặc một biến cho Language
cột:
SELECT TOP (6) EmployeeID, Language = N'Sanskrit' FROM dbo.Employees;
Nhưng nếu bạn chạy theo dấu vết so với truy vấn ban đầu, bạn sẽ thấy rằng thực sự có sáu lệnh gọi hàm (một cho mỗi hàng) ngoài truy vấn chính, nhưng các kế hoạch này không được SQL Server trả về.
Bạn cũng có thể xác minh điều này bằng cách kiểm tra sys.dm_exec_function_stats
, nhưng đây không phải là một bảo đảm :
SELECT [function] = OBJECT_NAME([object_id]), execution_count FROM sys.dm_exec_function_stats WHERE object_name(object_id) IS NOT NULL;
function execution_count ----------- --------------- GetLanguage 6
SentryOne Plan Explorer sẽ hiển thị các câu lệnh nếu bạn tạo một kế hoạch thực tế từ bên trong sản phẩm, nhưng chúng tôi chỉ có thể lấy những thông tin đó từ dấu vết và vẫn không có kế hoạch nào được thu thập hoặc hiển thị cho các lệnh gọi hàm riêng lẻ:
Câu lệnh theo dõi cho các lệnh gọi UDF vô hướng riêng lẻ
Tất cả những điều này khiến chúng rất khó khắc phục sự cố, vì bạn phải đi tìm chúng, ngay cả khi bạn đã biết chúng ở đó. Nó cũng có thể tạo ra một mớ hỗn độn thực sự của phân tích hiệu suất nếu bạn đang so sánh hai kế hoạch dựa trên những thứ như chi phí ước tính, bởi vì không chỉ các nhà khai thác liên quan đang ẩn khỏi sơ đồ vật lý, chi phí cũng không được kết hợp vào bất kỳ đâu trong kế hoạch.
Chuyển tiếp nhanh tới SQL Server 2019
Sau ngần ấy năm hành vi có vấn đề và nguyên nhân gốc rễ không rõ ràng, họ đã thực hiện để một số chức năng có thể được tối ưu hóa vào kế hoạch thực thi tổng thể. Nội tuyến UDF vô hướng làm cho các đối tượng mà chúng truy cập có thể nhìn thấy được để khắc phục sự cố * và * cho phép chúng được xếp lại vào chiến lược kế hoạch thực thi. Giờ đây, các ước tính về số lượng (dựa trên thống kê) cho phép các chiến lược kết hợp đơn giản là không thể thực hiện được khi hàm được gọi một lần cho mỗi hàng.
Chúng ta có thể sử dụng cùng một ví dụ như trên, tạo cùng một tập hợp các đối tượng trên cơ sở dữ liệu SQL Server 2019 hoặc xóa bộ nhớ cache của kế hoạch và nâng cấp độ tương thích lên 150:
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO ALTER DATABASE Whatever SET COMPATIBILITY_LEVEL = 150; GO
Bây giờ khi chúng tôi chạy lại truy vấn sáu hàng của mình:
SELECT TOP (6) EmployeeID, Language = dbo.GetLanguage(LanguageID) FROM dbo.Employees;
Chúng tôi nhận được một kế hoạch bao gồm bảng Ngôn ngữ và chi phí liên quan đến việc truy cập nó:
Kế hoạch bao gồm quyền truy cập vào các đối tượng được tham chiếu bên trong UDF vô hướng
Ở đây, trình tối ưu hóa đã chọn một phép nối các vòng lồng nhau nhưng trong các trường hợp khác nhau, nó có thể chọn một chiến lược nối khác, kết hợp song song và về cơ bản được tự do thay đổi hoàn toàn hình dạng kế hoạch. Bạn không có khả năng thấy điều này trong truy vấn trả về 6 hàng và không phải là vấn đề về hiệu suất theo bất kỳ cách nào, nhưng ở quy mô lớn hơn thì có thể.
Kế hoạch phản ánh rằng hàm không được gọi trên mỗi hàng - trong khi tìm kiếm thực sự được thực hiện sáu lần, bạn có thể thấy rằng bản thân hàm không còn hiển thị trong sys.dm_exec_function_stats
. Một nhược điểm mà bạn có thể bỏ qua là, nếu bạn sử dụng DMV này để xác định xem một hàm có đang được sử dụng tích cực hay không (như chúng ta thường làm đối với các thủ tục và chỉ mục), thì điều đó sẽ không còn đáng tin cậy nữa.
Lưu ý
Không phải mọi hàm vô hướng đều có thể nội dòng và ngay cả khi một hàm * là * nội tuyến, nó sẽ không nhất thiết phải được nội tuyến trong mọi trường hợp. Điều này thường liên quan đến độ phức tạp của hàm, độ phức tạp của truy vấn liên quan hoặc sự kết hợp của cả hai. Bạn có thể kiểm tra xem một hàm có thể nội dòng trong sys.sql_modules
hay không xem danh mục:
SELECT OBJECT_NAME([object_id]), definition, is_inlineable FROM sys.sql_modules;
Và nếu, vì bất kỳ lý do gì, bạn không muốn một chức năng nhất định (hoặc bất kỳ chức năng nào trong cơ sở dữ liệu) được nội tuyến, bạn không cần phải dựa vào mức độ tương thích của cơ sở dữ liệu để kiểm soát hành vi đó. Tôi chưa bao giờ thích sự kết hợp lỏng lẻo đó, giống như việc chuyển phòng để xem một chương trình truyền hình khác thay vì chỉ đơn giản là thay đổi kênh. Bạn có thể kiểm soát điều này ở cấp mô-đun bằng cách sử dụng tùy chọn INLINE:
ALTER FUNCTION dbo.GetLanguage(@id int) RETURNS sysname WITH INLINE = OFF AS BEGIN RETURN (SELECT Name FROM dbo.Languages WHERE LanguageID = @id); END GO
Và bạn có thể kiểm soát điều này ở cấp cơ sở dữ liệu, nhưng tách biệt với cấp tương thích:
ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = OFF;
Mặc dù bạn phải có một trường hợp sử dụng khá tốt để vung chiếc búa đó, IMHO.
Kết luận
Bây giờ, tôi không đề nghị bạn có thể trừu tượng hóa mọi phần logic thành một UDF vô hướng và giả sử rằng bây giờ SQL Server sẽ xử lý tất cả các trường hợp. Nếu bạn có cơ sở dữ liệu với nhiều mức sử dụng UDF vô hướng, bạn nên tải xuống CTP SQL Server 2019 mới nhất, khôi phục bản sao lưu cơ sở dữ liệu của bạn ở đó và kiểm tra DMV để xem có bao nhiêu chức năng trong số đó sẽ có thể nội tuyến khi đến thời điểm. Đây có thể là một điểm nhấn quan trọng vào lần tới khi bạn tranh cãi về việc nâng cấp, vì về cơ bản bạn sẽ nhận được tất cả hiệu suất đó và thời gian khắc phục sự cố lãng phí trở lại.
Trong thời gian chờ đợi, nếu bạn đang gặp phải hiệu suất UDF vô hướng và bạn sẽ không sớm nâng cấp lên SQL Server 2019, có thể có những cách khác để giúp giảm thiểu (các) vấn đề.
Lưu ý:Tôi đã viết và xếp hàng đợi bài viết này trước khi nhận ra rằng mình đã đăng một bài khác ở nơi khác.