用戶定義函數(UDF)分類
SQL SERVER中的用戶定義函數(User Defined Functions 簡稱UDF)分為標量函數(Scalar-Valued Function)和表值函數(Table-Valued Function)。其中表值函數又分為Inline table-valued functions和Multistatement table-valued functions。
用戶定義函數(UDF)在 SQL Server 中發揮重要的作用。用戶定義函數可以用於執行復雜的邏輯,可以接受參數並返回數據。很多時候我們需要寫復雜的邏輯,不能使用單個查詢編寫。在這種情況下,用戶定義函數(UDF)發揮了重要的作用。關於用戶定義函數的優點,可以參考官方文檔“用戶定義函數”。如下所示:
在 SQL Server 中使用用戶定義函數有以下優點:
-
允許模塊化程序設計。
只需創建一次函數並將其存儲在數據庫中,以后便可以在程序中調用任意次。用戶定義函數可以獨立於程序源代碼進行修改。
-
執行速度更快。
與存儲過程相似,Transact-SQL 用戶定義函數通過緩存計划並在重復執行時重用它來降低 Transact-SQL 代碼的編譯開銷。這意味着每次使用用戶定義函數時均無需重新解析和重新優化,從而縮短了執行時間。
和用於計算任務、字符串操作和業務邏輯的 Transact-SQL 函數相比,CLR 函數具有顯著的性能優勢。Transact-SQL 函數更適用於數據訪問密集型邏輯。
-
減少網絡流量。
基於某種無法用單一標量的表達式表示的復雜約束來過濾數據的操作,可以表示為函數。然后,此函數便可以在 WHERE 子句中調用,以減少發送至客戶端的數字或行數。
UDF標量函數(Scalar-Valued Function)影響性能案例
官方文檔說用戶定義函數(UDF)的執行速度更快,意思是性能非常好,如果你對此深信不疑的話,那么我只能呵呵了,其實關於用戶定義函數,尤其是標量函數,需要合理使用。有些場景使用不當,則有可能造成性能問題。關於UDF的標量函數會引起性能的問題,下面我們先看一個我構造的例子吧(AdventureWorks2014),我們需要查詢某個產品有多少訂單(其實也是優化過程中遇到,然后我在此處構造類似這樣的一個案例)
USE AdventureWorks2014;
GO
CREATE FUNCTION Sales.FetchProductOrderNum
(
@ProuctID INT
) RETURNS INT
BEGIN
DECLARE @SaleOrderNum INT;
SELECT @SaleOrderNum=COUNT(SalesOrderID) FROM Sales.SalesOrderDetail
WHERE ProductID=@ProuctID
GROUP BY ProductID;
RETURN @SaleOrderNum;
END
GO
我們知道Sales.SalesOrderDetail表里面ProductID=870的訂單數量有4688,而ProductID=897的訂單數量只有2條記錄。那么執行下面語句時,性能會有什么差異呢?
SET STATISTICS TIME ON;
SELECT DISTINCT ProductID, Sales.FetchProductOrderNum(ProductID) FROM Sales.SalesOrderDetail
WHERE ProductID=870
SELECT DISTINCT ProductID, Sales.FetchProductOrderNum(ProductID) FROM Sales.SalesOrderDetail
WHERE ProductID=897
SET STATISTICS TIME OFF;
為什么會有這種情況,這是因為SQL語句里面調用用戶定義標量函數(UDF Scalar Function),都是逐行調用用戶定義函數,這樣需要為每行去提取用戶定義函數的定義,然后去執行這些定義,從而導致了性能問題;更深層次的原因是因為函數采用了過程式的處理方法,而SQL Server查詢數據則是基於數據集合的,這樣在采用過程式的逐行處理時,SQL Server性能就會顯著降低。
那么我來分析看看這兩個SQL的實際執行計划:從下面實際執行計划,我們可以看到第一個SQL語句執行計划從Index Seek 到Compute Scalar的數據流變粗了。這個表示第一個SQL語句的Index Seek返回的數據要多。
接下來,我們從Compute Scalar(進行一個標量計算並返回計算值)里面可以看到Actual Number of Rows 的值為4688 和2 。
而Compute Scalar在此處就是調用標量函數,而標量函數調用總是需要資源開銷和時間的,當調用次數從2次變為4688次時,elapsed time 和CPU time當然會翻了好多倍。實際環境中,用戶定義標量函數的邏輯比上面簡單的案例更復雜,資源開銷更大,所以有時候你會看到性能差距非常懸殊的SQL案例,在工作中我就發現過這樣的情況,有些開發人員對自定義標量函數使用不當影響性能不甚了解。甚至是完全不知情。他們對此振振有詞:你看我SQL語句是一樣的,只是參數不同,效率差別這么大。肯定是數據庫出現了阻塞或性能問題。要么是服務器的性能問題,反正我SQL是沒有問題的,你看這一條語句執行才一秒,換個參數就要一分多鍾,這不是你數據庫性能問題,那是什么? 這樣的一個偽邏輯讓我很無語。(習慣性就讓我和數據庫、服務器背了一個大黑鍋)。
回到正題,上面兩個SQL語句的實際執行計划的Cost比值為81%:19%;Compute Scalar(進行一個標量計算並返回計算值)的Number of Executions都是1次。但是實際的CPU time &elapsed time的比值比這個大了好多。另外第一個SQL的Compute Scalar的代價比值居然只有1%。為什么會這樣呢?我們是不是很迷惑?
關於這個大家疑惑的地方,T-SQL User-Defined Functions: the good, the bad, and the ugly (part 1)里面給了我們一個闡述,截取文章中兩段在此(翻譯如有不當,請參考原文):
英文:
However, you may not be aware that the “Actual Execution Plan” is a dirty rotten liar. Or maybe I should say that the terms “Actual Execution Plan” and “Estimated Execution Plan” are misleading. There is only one execution plan, it gets created when the queries are compiled, and then the queries are executed. The only difference between the “Actual” and the “Estimated” execution plan is that the estimated plan only tells you the estimates for how many rows flow between iterators and how often iterators are executed, and the actual plan adds the actual data for that. But no “actual operator cost” or “actual subtree cost” is added to the corresponding estimated values – and since those costs are the values that the percentages are based on, the percentages displayed in an actual execution plan are still based only on the estimates.
翻譯:
然而,你可能不知道“實際執行計划”其實是一個骯臟的爛騙子,或者我應該說“實際執行計划”和“估計執行計划”誤導你了。當查詢語句編譯后,只有一個實際的執行計划。“實際執行計划”與“估計執行計划”的區別就在於“估計執行計划”只告訴你估計了有多少行流向迭代和迭代器執行頻率,而實際執行計划將實際數據應用進來。但是“實際操作成本”或“實際子樹成本”並沒有添加到“實際執行計划”的估計值里面, 因為這些代價都是基於百分比的值,在實際執行計划中顯示的百分比仍然基於只估計數。
英文:
But note that, again, the execution plan is lying. First, it implies that the UDF is invoked only once, which is not the case. Second, look at the cost. You may think that the 0% is the effect of rounding down, since a single execution of the function costs so little in relation to the cost of accessing and aggregating 100,000 rows. But if you check the properties of the iterators of the plan for the function, you’ll see that all operator and subtree costs are actually estimated to be exactly 0. This lie is maybe the worst of all – because it’s not just the plan lying to us, it is SQL Server lying to itself. This cost estimate of 0 is actually used by the query optimizer, so all plans it produces are based on the assumption that executing the function is free. As a result, the optimizer will not even consider optimizations it might use if it knew how costly calling a scalar UDF actually is.
翻譯:
但是需要再次注意,執行計划在欺騙你,首先,它意味着只調用了UDF一次,其實不是這樣。其次,從成本(Cost)來看,你可能會認為0%是向下舍入影響,因為單次執行函數的開銷如此之小,以至於執行100,000次的成本也很小。但如果你檢查執行計划的功能迭代器的屬性,你會發現所有的操作代價和子樹代價實際的估計為0,這是一個最糟糕的謊言。 因為它可能不只是為了欺騙我們,而是SQL SERVER為了欺騙它自己。實際上是查詢優化器認為調用函數的成本為0,因此它生成的所有執行計划都是基於調用UDF是免費的。其結果是即使調用標量UDF的代價非常昂貴,查詢優化器也不會考慮優化它。
如何優化UDF標量函數(Scalar-Valued Function)
如何優化上面SQL語句呢?從原理上來講就是不用用戶定義函數或減少調用次數。 其實我在實際應用中,減少調用次數一般通過下面方法優化:
1:減少用戶定義標量函數調用次數(子查詢)
SET STATISTICS TIME ON;
SELECT ProductID, Sales.FetchProductOrderNum(ProductID)
FROM(
SELECT DISTINCT ProductID FROM Sales.SalesOrderDetail
WHERE ProductID=870) T
SET STATISTICS TIME OFF;
2:減少用戶定義標量函數調用次數(臨時表)
SET STATISTICS TIME ON;
SELECT DISTINCT ProductID INTO #SalesOrderDetail FROM Sales.SalesOrderDetail
WHERE ProductID=870;
SELECT ProductID, Sales.FetchProductOrderNum(ProductID)
FROM #SalesOrderDetail
SET STATISTICS TIME OFF;
為什么要用臨時表呢?不是子查詢就可以解決問題嗎?問題是實際應用當中,有些邏輯復雜的地方需要借助臨時表解決,有時候子查詢反而不是一個好的解決方法。
另外,我們來看看Performance Considerations of User-Defined Functions in SQL Server 2012這篇文章中,測試UDF的性能案例,本想單獨翻譯這篇文章,不過結合這篇文章,在此實驗驗證也是個不錯的選擇。下面案例全部來自這篇博客。我們先准備測試環境:
CREATE FUNCTION dbo.Triple(@Input INT)
RETURNS INT
AS
BEGIN;
DECLARE @Result INT;
SET @Result = @Input * 3;
RETURN @Result;
END;
GO
CREATE TABLE dbo.LargeTable
(KeyVal INT NOT NULL PRIMARY KEY,
DataVal INT NOT NULL CHECK (DataVal BETWEEN 1 AND 10)
);
WITH Digits
AS (SELECT d FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) AS d(d))
INSERT INTO dbo.LargeTable (KeyVal, DataVal)
SELECT 1000000 * sm.d
+ 100000 * ht.d + 10000 * tt.d + 1000 * st.d
+ 100 * h.d + 10 * t.d + s.d + 1,
10 * RAND(CHECKSUM(NEWID())) + 1
FROM Digits AS s, Digits AS t, Digits AS h,
Digits AS st, Digits AS tt, Digits AS ht,
Digits AS sm;
GO
CREATE INDEX NCL_LargeTable_DataVal ON dbo.LargeTable (DataVal);
GO
SET STATISTICS TIME ON;
SELECT MAX(dbo.Triple(DataVal)) AS MaxTriple
FROM dbo.LargeTable AS d;
SELECT MAX(3 * DataVal) AS MaxTriple
FROM dbo.LargeTable AS d;
SET STATISTICS TIME OFF;
如上所示,用戶定義的標量函數dbo.Triple,測試用的的一個表dbo.LargeTable ,以及構造了1000000行數據。從下面我們可以看到用戶定義標量函數性能確實很糟糕。
下面測試4中寫法的性能。相信這個簡單的腳本,大家都能看懂,在此不做過多描述、說明:
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET NOCOUNT ON;
SET STATISTICS TIME ON;
SELECT MAX(dbo.Triple(DataVal)) AS MaxTriple
FROM dbo.LargeTable AS d;
SET STATISTICS TIME OFF;
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS TIME ON;
SELECT MAX(dbo.Triple(DataVal)) AS MaxTriple
FROM (SELECT DISTINCT DataVal FROM dbo.LargeTable) AS d;
SET STATISTICS TIME OFF;
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS TIME ON;
SELECT MAX(3 * DataVal) AS MaxTriple
FROM (SELECT DISTINCT DataVal FROM dbo.LargeTable) AS d;
SET STATISTICS TIME OFF;
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS TIME ON;
SELECT MAX(3 * DataVal) AS MaxTriple
FROM dbo.LargeTable AS d;
SET STATISTICS TIME OFF;
Performance Considerations of User-Defined Functions in SQL Server 2012博客里面統計的數據如下所示
T-SQL Syntax |
Avg CPU Time in ms |
Avg Elapsed Time in ms |
Function, no distinct subquery |
12925.0 |
14247.8 |
Function, subquery |
853.0 |
853.8 |
Inline calculation, subquery |
853.2 |
850.4 |
Inline calculation, no distinct subquery |
0.0 |
0.0 |
這個跟我測試的數據有所出入(可能跟數據庫版本、機器配置有一點關系)。但是大體方向是一致的。Avg CPU Time和Avg Elapsed Time 執行時間依然Function, no distinct subquery > Function, subquery = Inline calculation, subquery > Inline calculation, no distinct subquery
那么接下來,我們先進一個表值函數Triple_tbl,對比表值函數和標量函數的性能。如下所示
CREATE FUNCTION dbo.Triple_tbl (@DataVal INT)
RETURNS TABLE
AS
RETURN
SELECT @DataVal * 3 Triple
GO
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS TIME ON;
SELECT MAX(dbo.Triple(DataVal)) AS MaxTriple
FROM (SELECT DISTINCT DataVal FROM dbo.LargeTable) AS d;
SELECT MAX(3 * DataVal) AS MaxTriple
FROM dbo.LargeTable AS d;
SELECT MAX(t.Triple) AS MaxTriple
FROM dbo.LargeTable l
CROSS APPLY dbo.Triple_tbl(l.DataVal) t
SET STATISTICS TIME OFF;
GO
從下可以看出,表值函數比標量函數性能要好很多,所以用表值函數替換標量函數也是一個可以考慮的優化方案。
參考資料: