Các chức năng do người dùng định nghĩa trong SQL Server (UDF) là các đối tượng chính mà mỗi nhà phát triển cần lưu ý. Mặc dù chúng rất hữu ích trong nhiều trường hợp (mệnh đề WHERE, cột được tính toán và ràng buộc kiểm tra), chúng vẫn có một số hạn chế và các hoạt động không tốt có thể gây ra các vấn đề về hiệu suất. Các UDF đa tuyên bố có thể gây ra các tác động đáng kể về hiệu suất và bài viết này sẽ thảo luận cụ thể về các tình huống này.
Các hàm không được triển khai theo cách giống như trong ngôn ngữ hướng đối tượng, mặc dù các hàm có giá trị bảng nội tuyến có thể được sử dụng trong các tình huống khi bạn cần các dạng xem tham số hóa, điều này không áp dụng cho các hàm trả về bảng hoặc bảng vô hướng. Các chức năng này cần được sử dụng cẩn thận vì chúng có thể gây ra nhiều vấn đề về hiệu suất. Tuy nhiên, chúng rất cần thiết trong nhiều trường hợp, vì vậy chúng ta sẽ cần chú ý hơn đến việc triển khai chúng. Các hàm được sử dụng trong các câu lệnh SQL bên trong lô, thủ tục, trình kích hoạt hoặc chế độ xem, bên trong truy vấn SQL đặc biệt hoặc như một phần của truy vấn báo cáo được tạo bởi các công cụ như PowerBI hoặc Tableau, trong các trường được tính toán và kiểm tra các ràng buộc. Trong khi các hàm vô hướng có thể đệ quy lên đến 32 cấp, các hàm bảng không hỗ trợ đệ quy.
Các loại hàm trong SQL Server
Trong SQL Server, chúng ta có ba loại hàm:hàm vô hướng do người dùng xác định (SF) trả về một giá trị vô hướng duy nhất, hàm có giá trị bảng do người dùng xác định (TVF) trả về bảng và các hàm có giá trị bảng nội tuyến (ITVF) không có cơ quan chức năng. Các Hàm của Bảng có thể là Inline hoặc Multi-statement. Các hàm nội tuyến không có biến trả về, chúng chỉ trả về các hàm giá trị. Các hàm nhiều câu lệnh được chứa trong các khối mã BEGIN-END và có thể có nhiều câu lệnh T-SQL mà không tạo ra bất kỳ tác dụng phụ nào (chẳng hạn như sửa đổi nội dung trong bảng).
Chúng tôi sẽ hiển thị từng loại chức năng trong một ví dụ đơn giản:
/**
inline table function
**/
CREATE FUNCTION dbo.fnInline( @P1 INT, @P2 VARCHAR(50) )
RETURNS TABLE
AS
RETURN ( SELECT @P1 AS P_OUT_1, @P2 AS P_OUT_2 )
/**
multi-statement table function
**/
CREATE FUNCTION dbo.fnMultiTable( @P1 INT, @P2 VARCHAR(50) )
RETURNS @r_table TABLE ( OUT_1 INT, OUT_2 VARCHAR(50) )
AS
BEGIN
INSERT @r_table SELECT @P1, @P2;
RETURN;
END;
/**
scalar function
**/
CREATE FUNCTION dbo.fnScalar( @P1 INT, @P2 INT )
RETURNS INT
AS
BEGIN
RETURN @P1 + @P2
END
Giới hạn chức năng của SQL Server
Như đã đề cập trong phần giới thiệu, có một số hạn chế trong việc sử dụng chức năng và tôi sẽ chỉ khám phá một số hạn chế bên dưới. Bạn có thể tìm thấy danh sách đầy đủ tại Microsoft Docs :
- Không có khái niệm về các chức năng tạm thời
- Bạn không thể tạo một hàm trong cơ sở dữ liệu khác, nhưng tùy thuộc vào đặc quyền của mình, bạn có thể truy cập nó
- Với UDF, bạn không được phép thực hiện bất kỳ hành động nào làm thay đổi trạng thái cơ sở dữ liệu,
- Bên trong UDF, bạn không thể gọi một thủ tục, ngoại trừ thủ tục được lưu trữ mở rộng
- UDF không thể trả về một tập kết quả mà chỉ trả về một kiểu dữ liệu bảng
- Bạn không thể sử dụng SQL động hoặc các bảng tạm thời trong UDFs
- UDF bị hạn chế về khả năng xử lý lỗi - chúng không hỗ trợ RAISERROR cũng như không THỬ… CATCH và bạn không thể lấy dữ liệu từ biến @ERROR của hệ thống
Điều gì được phép trong các hàm đa câu lệnh?
Chỉ những điều sau được phép:
- Tuyên bố bài tập
- Tất cả các câu lệnh điều khiển luồng, ngoại trừ khối TRY… CATCH
- Lệnh gọi DECLARE, được sử dụng để tạo các biến cục bộ và con trỏ
- Bạn có thể sử dụng các truy vấn SELECT có danh sách với các biểu thức và gán các giá trị này cho các biến được khai báo cục bộ
- Con trỏ chỉ có thể tham chiếu đến các bảng cục bộ và phải được mở và đóng bên trong thân hàm. FETCH chỉ có thể gán hoặc thay đổi giá trị của các biến cục bộ, không thể truy xuất hoặc thay đổi dữ liệu cơ sở dữ liệu
Điều gì nên được hỗ trợ trong các hàm đa câu lệnh, mặc dù được phép?
- Bạn nên tránh các trường hợp trong đó bạn đang sử dụng các cột được tính toán với các hàm vô hướng - điều này sẽ gây ra việc tạo lại chỉ mục và cập nhật chậm yêu cầu tính toán lại
- Cân nhắc rằng bất kỳ hàm đa câu lệnh nào cũng có kế hoạch thực thi và tác động đến hiệu suất của nó
- UDF có giá trị bảng nhiều câu lệnh, nếu được sử dụng trong biểu thức SQL hoặc câu lệnh nối sẽ chậm do kế hoạch thực thi không tối ưu
- Không sử dụng các hàm vô hướng trong câu lệnh WHERE và mệnh đề ON trừ khi bạn chắc chắn rằng nó sẽ truy vấn một tập dữ liệu nhỏ và tập dữ liệu đó sẽ vẫn nhỏ trong tương lai
Tên và tham số hàm
Giống như bất kỳ tên đối tượng nào khác, tên hàm phải tuân thủ các quy tắc cho số nhận dạng và phải là duy nhất trong lược đồ của chúng. Nếu bạn đang tạo các hàm vô hướng, bạn có thể chạy chúng bằng cách sử dụng câu lệnh EXECUTE. Trong trường hợp này, bạn không phải đặt tên giản đồ trong tên hàm. Xem ví dụ về lệnh gọi hàm EXECUTE bên dưới (chúng tôi tạo một hàm trả về sự xuất hiện của ngày thứ N trong một tháng và sau đó truy xuất dữ liệu này):
CREATE FUNCTION dbo.fnGetDayofWeekInMonth
(
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurrence, 3 for the third
)
RETURNS DATETIME
AS
BEGIN
RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0,
CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-
(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0,
CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
END
-- In SQL Server 2012 and later versions, you can use the EXECUTE command or the SELECT command to run a UDF, or use a standard approach
DECLARE @ret DateTime
EXEC @ret = fnGetDayofWeekInMonth '2020', 'Jan', 'Mon',2
SELECT @ret AS Third_Monday_In_January_2020
SELECT dbo.fnGetDayofWeekInMonth('2020', 'Jan', DEFAULT, DEFAULT)
AS 'Using default',
dbo.fnGetDayofWeekInMonth('2020', 'Jan', 'Mon', 2) AS 'explicit'
Chúng ta có thể xác định các giá trị mặc định cho các tham số hàm, chúng phải có tiền tố là “@” và tuân thủ các quy tắc đặt tên mã định danh. Các tham số có thể chỉ là các giá trị không đổi, chúng không thể được sử dụng trong các truy vấn SQL thay vì bảng, dạng xem, cột hoặc các đối tượng cơ sở dữ liệu khác và giá trị không thể là biểu thức, ngay cả những giá trị xác định. Tất cả các kiểu dữ liệu đều được phép, ngoại trừ kiểu dữ liệu TIMESTAMP và không có kiểu dữ liệu không vô hướng nào có thể được sử dụng, ngoại trừ các tham số có giá trị bảng. Trong các lệnh gọi hàm "chuẩn", bạn phải chỉ định thuộc tính DEFAULT nếu bạn muốn cung cấp cho người dùng cuối khả năng tạo một tham số là tùy chọn. Trong các phiên bản mới, sử dụng cú pháp EXECUTE, điều này không còn bắt buộc, bạn chỉ cần nhập tham số này vào lệnh gọi hàm. Nếu chúng tôi đang sử dụng các loại bảng tùy chỉnh, chúng phải được đánh dấu là ĐÃ SN SÀNG, nghĩa là chúng tôi không thể thay đổi giá trị ban đầu bên trong hàm, nhưng chúng có thể được sử dụng trong tính toán và định nghĩa các tham số khác.
Hiệu suất chức năng máy chủ SQL
Chủ đề cuối cùng mà chúng ta sẽ đề cập trong bài viết này, sử dụng các hàm từ chương trước, là hiệu suất của hàm. Chúng tôi sẽ mở rộng chức năng này và giám sát thời gian thực hiện cũng như chất lượng của các kế hoạch thực hiện. Chúng tôi bắt đầu bằng cách tạo các phiên bản chức năng khác và tiếp tục so sánh chúng:
CREATE FUNCTION dbo.fnGetDayofWeekInMonthBound
(
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurrence, 3 for the third
)
RETURNS DATETIME
WITH SCHEMABINDING
AS
BEGIN
RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
END
GO
CREATE FUNCTION dbo.fnNthDayOfWeekOfMonthInline (
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurence, 3 for the third
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN (SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7 AS TheDate)
GO
CREATE FUNCTION dbo.fnNthDayOfWeekOfMonthTVF (
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurence, 3 for the third
)
RETURNS @When TABLE (TheDate DATETIME)
WITH schemabinding
AS
Begin
INSERT INTO @When(TheDate)
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
RETURN
end
GO
Tạo một số cuộc gọi thử nghiệm và trường hợp thử nghiệm
Chúng tôi bắt đầu với các phiên bản bảng:
SELECT * FROM dbo.fnNthDayOfWeekOfMonthTVF('2020','Feb','Tue',2)
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue',2)),113) FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
OUTER apply dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue',2)
Tạo dữ liệu thử nghiệm:
IF EXISTS(SELECT * FROM tempdb.sys.tables WHERE name LIKE '#DataForTest%')
DROP TABLE #DataForTest
GO
SELECT *
INTO #DataForTest
FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
CROSS join (VALUES ('jan'),('feb'),('mar'),('apr'),('may'),('jun'),('jul'),('aug'),('sep'),('oct'),('nov'),('dec'))months(Themonth)
CROSS join (VALUES ('Mon'),('Tue'),('Wed'),('Thu'),('Fri'),('Sat'),('Sun'))day(TheDay)
CROSS join (VALUES (1),(2),(3),(4))nth(nth)
Kiểm tra hiệu suất:
DECLARE @TableLog TABLE (OrderVal INT IDENTITY(1,1), Reason VARCHAR(500), TimeOfEvent DATETIME2 DEFAULT GETDATE())
Thời gian bắt đầu:
INSERT INTO @TableLog(Reason) SELECT 'Starting My_Section_of_code' --place at the start
Đầu tiên, chúng tôi không sử dụng loại hàm nào để lấy đường cơ sở:
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0)+ (7*Nth)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0))
[email protected]@DateFirst+(CHARINDEX(TheDay,'FriThuWedTueMonSunSat')-1)/3)%7 AS TheDate
INTO #Test0
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Using the code entirely unwrapped';
Bây giờ chúng tôi sử dụng một hàm nội tuyến có giá trị Bảng được áp dụng chéo:
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate
INTO #Test1
FROM #DataForTest
CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)
INSERT INTO @TableLog(Reason) SELECT 'Inline function cross apply'
Chúng tôi sử dụng một hàm nội tuyến có giá trị trong Bảng được áp dụng chéo:
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsDate
INTO #Test2
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Inline function Derived table'
Để so sánh không đáng tin cậy, chúng tôi sử dụng một hàm vô hướng với schemabinding:
SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonthBound(TheYear,TheMonth,TheDay,nth))itsdate
INTO #Test3
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Trusted (Schemabound) scalar function'
Tiếp theo, chúng tôi sử dụng một hàm vô hướng không có liên kết lược đồ:
SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonth(TheYear,TheMonth,TheDay,nth))itsdate
INTO #Test6
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Untrusted scalar function'
Sau đó, hàm bảng nhiều câu lệnh được dẫn xuất:
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsdate
INTO #Test4
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'multi-statement table function derived'
Cuối cùng, bảng nhiều câu lệnh được áp dụng chéo:
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate
INTO #Test5
FROM #DataForTest
CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)
INSERT INTO @TableLog(Reason) SELECT 'multi-statement cross APPLY'--where the routine you want to time ends
Liệt kê tất cả thời gian:
SELECT ending.Reason AS Test, DateDiff(ms, starting.TimeOfEvent,ending.TimeOfEvent) [AS Time (ms)] FROM @TableLog starting
INNER JOIN @TableLog ending ON ending.OrderVal=starting.OrderVal+1
DROP table #Test0
DROP table #Test1
DROP table #Test2
DROP table #Test3
DROP table #Test4
DROP table #Test5
DROP table #Test6
DROP TABLE #DataForTest
Bảng trên cho thấy rõ ràng rằng bạn nên xem xét hiệu suất so với chức năng khi bạn đang sử dụng các chức năng do người dùng xác định.
Kết luận
Các hàm được nhiều nhà phát triển thích, chủ yếu là vì chúng là "cấu trúc logic". Bạn có thể dễ dàng tạo các trường hợp thử nghiệm, chúng mang tính xác định và đóng gói, chúng tích hợp độc đáo với luồng mã SQL và cho phép linh hoạt trong tham số hóa. Chúng là một lựa chọn tốt khi bạn cần triển khai logic phức tạp cần được thực hiện trên một tập dữ liệu nhỏ hơn hoặc đã được lọc mà bạn sẽ cần sử dụng lại trong nhiều tình huống. Các dạng xem bảng nội tuyến có thể được sử dụng trong các dạng xem cần tham số, đặc biệt là từ các lớp trên (ứng dụng giao diện khách). Mặt khác, các hàm vô hướng rất phù hợp để làm việc với XML hoặc các định dạng phân cấp khác, vì chúng có thể được gọi một cách đệ quy.
Các hàm đa câu lệnh do người dùng xác định là một bổ sung tuyệt vời cho ngăn xếp công cụ phát triển của bạn, nhưng bạn phải hiểu cách chúng hoạt động và những hạn chế cũng như thách thức về hiệu suất của chúng. Việc sử dụng sai chúng có thể phá hủy hiệu suất của bất kỳ cơ sở dữ liệu nào nhưng nếu bạn biết cách sử dụng các hàm này, chúng có thể mang lại rất nhiều lợi ích cho việc tái sử dụng và đóng gói mã.