SQL SERVER中用户定义标量函数(scalar user defined function)的性能问题
用戶定義函數(shù)(UDF)分類?
???? SQL SERVER中的用戶定義函數(shù)(User Defined Functions 簡稱UDF)分為標(biāo)量函數(shù)(Scalar-Valued Function)和表值函數(shù)(Table-Valued Function)。其中表值函數(shù)又分為Inline table-valued functions和Multistatement table-valued functions。
??? 用戶定義函數(shù)(UDF)在 SQL Server 中發(fā)揮重要的作用。用戶定義函數(shù)可以用于執(zhí)行復(fù)雜的邏輯,可以接受參數(shù)并返回?cái)?shù)據(jù)。很多時(shí)候我們需要寫復(fù)雜的邏輯,不能使用單個(gè)查詢編寫。在這種情況下,用戶定義函數(shù)(UDF)發(fā)揮了重要的作用。關(guān)于用戶定義函數(shù)的優(yōu)點(diǎn),可以參考官方文檔“用戶定義函數(shù)”。如下所示:
用戶定義函數(shù)的優(yōu)點(diǎn)
在 SQL Server 中使用用戶定義函數(shù)有以下優(yōu)點(diǎn):
-
允許模塊化程序設(shè)計(jì)。
只需創(chuàng)建一次函數(shù)并將其存儲(chǔ)在數(shù)據(jù)庫中,以后便可以在程序中調(diào)用任意次。用戶定義函數(shù)可以獨(dú)立于程序源代碼進(jìn)行修改。
-
執(zhí)行速度更快。
與存儲(chǔ)過程相似,Transact-SQL 用戶定義函數(shù)通過緩存計(jì)劃并在重復(fù)執(zhí)行時(shí)重用它來降低 Transact-SQL 代碼的編譯開銷。這意味著每次使用用戶定義函數(shù)時(shí)均無需重新解析和重新優(yōu)化,從而縮短了執(zhí)行時(shí)間。
和用于計(jì)算任務(wù)、字符串操作和業(yè)務(wù)邏輯的 Transact-SQL 函數(shù)相比,CLR 函數(shù)具有顯著的性能優(yōu)勢(shì)。Transact-SQL 函數(shù)更適用于數(shù)據(jù)訪問密集型邏輯。
-
減少網(wǎng)絡(luò)流量。
基于某種無法用單一標(biāo)量的表達(dá)式表示的復(fù)雜約束來過濾數(shù)據(jù)的操作,可以表示為函數(shù)。然后,此函數(shù)便可以在 WHERE 子句中調(diào)用,以減少發(fā)送至客戶端的數(shù)字或行數(shù)。
?
?
UDF標(biāo)量函數(shù)(Scalar-Valued Function)影響性能案例
?? 官方文檔說用戶定義函數(shù)(UDF)的執(zhí)行速度更快,意思是性能非常好,如果你對(duì)此深信不疑的話,那么我只能呵呵了,其實(shí)關(guān)于用戶定義函數(shù),尤其是標(biāo)量函數(shù),需要合理使用。有些場(chǎng)景使用不當(dāng),則有可能造成性能問題。關(guān)于UDF的標(biāo)量函數(shù)會(huì)引起性能的問題,下面我們先看一個(gè)我構(gòu)造的例子吧(AdventureWorks2014),我們需要查詢某個(gè)產(chǎn)品有多少訂單(其實(shí)也是優(yōu)化過程中遇到,然后我在此處構(gòu)造類似這樣的一個(gè)案例)
USE AdventureWorks2014;GOCREATE?FUNCTION Sales.FetchProductOrderNum(??? @ProuctID? INT) RETURNS?INTBEGIN??? DECLARE @SaleOrderNum INT;??? SELECT @SaleOrderNum=COUNT(SalesOrderID)? FROM Sales.SalesOrderDetail ??? WHERE ProductID=@ProuctID??? GROUP?BY ProductID;???? RETURN @SaleOrderNum;ENDGO
我們知道Sales.SalesOrderDetail表里面ProductID=870的訂單數(shù)量有4688,而ProductID=897的訂單數(shù)量只有2條記錄。那么執(zhí)行下面語句時(shí),性能會(huì)有什么差異呢?
SET?STATISTICS?TIME?ON;?SELECT?DISTINCT ProductID, Sales.FetchProductOrderNum(ProductID) FROM Sales.SalesOrderDetailWHERE ProductID=870?SELECT?DISTINCT ProductID, Sales.FetchProductOrderNum(ProductID) FROM Sales.SalesOrderDetailWHERE ProductID=897??SET?STATISTICS?TIME?OFF;
為什么會(huì)有這種情況,這是因?yàn)镾QL語句里面調(diào)用用戶定義標(biāo)量函數(shù)(UDF Scalar Function),都是逐行調(diào)用用戶定義函數(shù),這樣需要為每行去提取用戶定義函數(shù)的定義,然后去執(zhí)行這些定義,從而導(dǎo)致了性能問題;更深層次的原因是因?yàn)楹瘮?shù)采用了過程式的處理方法,而SQL Server查詢數(shù)據(jù)則是基于數(shù)據(jù)集合的,這樣在采用過程式的逐行處理時(shí),SQL Server性能就會(huì)顯著降低。
那么我來分析看看這兩個(gè)SQL的實(shí)際執(zhí)行計(jì)劃:從下面實(shí)際執(zhí)行計(jì)劃,我們可以看到第一個(gè)SQL語句執(zhí)行計(jì)劃從Index Seek 到Compute Scalar的數(shù)據(jù)流變粗了。這個(gè)表示第一個(gè)SQL語句的Index Seek返回的數(shù)據(jù)要多。
接下來,我們從Compute Scalar(進(jìn)行一個(gè)標(biāo)量計(jì)算并返回計(jì)算值)里面可以看到Actual Number of Rows 的值為4688 和2 。
而Compute Scalar在此處就是調(diào)用標(biāo)量函數(shù),而標(biāo)量函數(shù)調(diào)用總是需要資源開銷和時(shí)間的,當(dāng)調(diào)用次數(shù)從2次變?yōu)?688次時(shí),elapsed time 和CPU time當(dāng)然會(huì)翻了好多倍。實(shí)際環(huán)境中,用戶定義標(biāo)量函數(shù)的邏輯比上面簡單的案例更復(fù)雜,資源開銷更大,所以有時(shí)候你會(huì)看到性能差距非常懸殊的SQL案例,在工作中我就發(fā)現(xiàn)過這樣的情況,有些開發(fā)人員對(duì)自定義標(biāo)量函數(shù)使用不當(dāng)影響性能不甚了解。甚至是完全不知情。他們對(duì)此振振有詞:你看我SQL語句是一樣的,只是參數(shù)不同,效率差別這么大。肯定是數(shù)據(jù)庫出現(xiàn)了阻塞或性能問題。要么是服務(wù)器的性能問題,反正我SQL是沒有問題的,你看這一條語句執(zhí)行才一秒,換個(gè)參數(shù)就要一分多鐘,這不是你數(shù)據(jù)庫性能問題,那是什么? 這樣的一個(gè)偽邏輯讓我很無語。(習(xí)慣性就讓我和數(shù)據(jù)庫、服務(wù)器背了一個(gè)大黑鍋)。
?? 回到正題,上面兩個(gè)SQL語句的實(shí)際執(zhí)行計(jì)劃的Cost比值為81%:19%;Compute Scalar(進(jìn)行一個(gè)標(biāo)量計(jì)算并返回計(jì)算值)的Number of Executions都是1次。但是實(shí)際的CPU time &elapsed time的比值比這個(gè)大了好多。另外第一個(gè)SQL的Compute Scalar的代價(jià)比值居然只有1%。為什么會(huì)這樣呢?我們是不是很迷惑?
?
關(guān)于這個(gè)大家疑惑的地方,T-SQL User-Defined Functions: the good, the bad, and the ugly (part 1)里面給了我們一個(gè)闡述,截取文章中兩段在此(翻譯如有不當(dāng),請(qǐng)參考原文):
?
英文:
????? 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.
?
翻譯:
??? 然而,你可能不知道“實(shí)際執(zhí)行計(jì)劃”其實(shí)是一個(gè)骯臟的爛騙子,或者我應(yīng)該說“實(shí)際執(zhí)行計(jì)劃”和“估計(jì)執(zhí)行計(jì)劃”誤導(dǎo)你了。當(dāng)查詢語句編譯后,只有一個(gè)實(shí)際的執(zhí)行計(jì)劃。“實(shí)際執(zhí)行計(jì)劃”與“估計(jì)執(zhí)行計(jì)劃”的區(qū)別就在于“估計(jì)執(zhí)行計(jì)劃”只告訴你估計(jì)了有多少行流向迭代和迭代器執(zhí)行頻率,而實(shí)際執(zhí)行計(jì)劃將實(shí)際數(shù)據(jù)應(yīng)用進(jìn)來。但是“實(shí)際操作成本”或“實(shí)際子樹成本”并沒有添加到“實(shí)際執(zhí)行計(jì)劃”的估計(jì)值里面, 因?yàn)檫@些代價(jià)都是基于百分比的值,在實(shí)際執(zhí)行計(jì)劃中顯示的百分比仍然基于只估計(jì)數(shù)。
?
英文:
??? 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.
?
翻譯:
但是需要再次注意,執(zhí)行計(jì)劃在欺騙你,首先,它意味著只調(diào)用了UDF一次,其實(shí)不是這樣。其次,從成本(Cost)來看,你可能會(huì)認(rèn)為0%是向下舍入影響,因?yàn)閱未螆?zhí)行函數(shù)的開銷如此之小,以至于執(zhí)行100,000次的成本也很小。但如果你檢查執(zhí)行計(jì)劃的功能迭代器的屬性,你會(huì)發(fā)現(xiàn)所有的操作代價(jià)和子樹代價(jià)實(shí)際的估計(jì)為0,這是一個(gè)最糟糕的謊言。 因?yàn)樗赡懿恢皇菫榱似垓_我們,而是SQL SERVER為了欺騙它自己。實(shí)際上是查詢優(yōu)化器認(rèn)為調(diào)用函數(shù)的成本為0,因此它生成的所有執(zhí)行計(jì)劃都是基于調(diào)用UDF是免費(fèi)的。其結(jié)果是即使調(diào)用標(biāo)量UDF的代價(jià)非常昂貴,查詢優(yōu)化器也不會(huì)考慮優(yōu)化它。
?
如何優(yōu)化UDF標(biāo)量函數(shù)(Scalar-Valued Function)
如何優(yōu)化上面SQL語句呢?從原理上來講就是不用用戶定義函數(shù)或減少調(diào)用次數(shù)。 其實(shí)我在實(shí)際應(yīng)用中,減少調(diào)用次數(shù)一般通過下面方法優(yōu)化:
1:減少用戶定義標(biāo)量函數(shù)調(diào)用次數(shù)(子查詢)
SET?STATISTICS?TIME?ON;?SELECT ProductID, Sales.FetchProductOrderNum(ProductID)FROM(??? SELECT?DISTINCT ProductID FROM Sales.SalesOrderDetailWHERE ProductID=870) T?SET?STATISTICS?TIME?OFF;
?
2:減少用戶定義標(biāo)量函數(shù)調(diào)用次數(shù)(臨時(shí)表)
SET?STATISTICS?TIME?ON;??SELECT?DISTINCT ProductID INTO #SalesOrderDetail FROM Sales.SalesOrderDetailWHERE ProductID=870;?SELECT ProductID, Sales.FetchProductOrderNum(ProductID)FROM #SalesOrderDetail?SET?STATISTICS?TIME?OFF;?
為什么要用臨時(shí)表呢?不是子查詢就可以解決問題嗎?問題是實(shí)際應(yīng)用當(dāng)中,有些邏輯復(fù)雜的地方需要借助臨時(shí)表解決,有時(shí)候子查詢反而不是一個(gè)好的解決方法。
另外,我們來看看Performance Considerations of User-Defined Functions in SQL Server 2012這篇文章中,測(cè)試UDF的性能案例,本想單獨(dú)翻譯這篇文章,不過結(jié)合這篇文章,在此實(shí)驗(yàn)驗(yàn)證也是個(gè)不錯(cuò)的選擇。下面案例全部來自這篇博客。我們先準(zhǔn)備測(cè)試環(huán)境:
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;?
如上所示,用戶定義的標(biāo)量函數(shù)dbo.Triple,測(cè)試用的的一個(gè)表dbo.LargeTable ,以及構(gòu)造了1000000行數(shù)據(jù)。從下面我們可以看到用戶定義標(biāo)量函數(shù)性能確實(shí)很糟糕。
下面測(cè)試4中寫法的性能。相信這個(gè)簡單的腳本,大家都能看懂,在此不做過多描述、說明:
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ǒng)計(jì)的數(shù)據(jù)如下所示
| 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 |
這個(gè)跟我測(cè)試的數(shù)據(jù)有所出入(可能跟數(shù)據(jù)庫版本、機(jī)器配置有一點(diǎn)關(guān)系)。但是大體方向是一致的。Avg CPU Time和Avg Elapsed Time 執(zhí)行時(shí)間依然Function, no distinct subquery? > Function, subquery = Inline calculation, subquery > Inline calculation, no distinct subquery
那么接下來,我們先進(jìn)一個(gè)表值函數(shù)Triple_tbl,對(duì)比表值函數(shù)和標(biāo)量函數(shù)的性能。如下所示
CREATE?FUNCTION dbo.Triple_tbl (@DataVal INT)RETURNS?TABLE?ASRETURNSELECT @DataVal * 3 TripleGO??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 MaxTripleFROM dbo.LargeTable lCROSS APPLY dbo.Triple_tbl(l.DataVal) t?SET?STATISTICS?TIME?OFF;GO從下可以看出,表值函數(shù)比標(biāo)量函數(shù)性能要好很多,所以用表值函數(shù)替換標(biāo)量函數(shù)也是一個(gè)可以考慮的優(yōu)化方案。
?
參考資料:
http://sqlblog.com/blogs/hugo_kornelis/archive/2012/05/20/t-sql-user-defined-functions-the-good-the-bad-and-the-ugly-part-1.aspx
https://www.captechconsulting.com/blogs/performance-considerations-of-user-defined-functions-in-sql-server-2012
https://connect.microsoft.com/SQLServer/feedback/details/524983/user-defined-function-performance-is-unacceptable
posted on 2018-08-03 09:18 NET未來之路 閱讀(...) 評(píng)論(...) 編輯 收藏轉(zhuǎn)載于:https://www.cnblogs.com/lonelyxmas/p/9411554.html
總結(jié)
以上是生活随笔為你收集整理的SQL SERVER中用户定义标量函数(scalar user defined function)的性能问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于爬虫的日常复习(8)—— 实战:re
- 下一篇: Redis学习笔记(一)