1. 程式人生 > >SQL SERVER中使用者定義標量函式(scalar user defined function)的效能問題

SQL SERVER中使用者定義標量函式(scalar user defined function)的效能問題

使用者定義函式(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 [email protected]
    GROUP BY ProductID;
    RETURN @SaleOrderNum;
END
GO

clipboard

我們知道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;

clipboard[1]

為什麼會有這種情況,這是因為SQL語句裡面呼叫使用者定義標量函式(UDF Scalar Function),都是逐行呼叫使用者定義函式,這樣需要為每行去提取使用者定義函式的定義,然後去執行這些定義,從而導致了效能問題;更深層次的原因是因為函式採用了過程式的處理方法,而SQL Server查詢資料則是基於資料集合的,這樣在採用過程式的逐行處理時,SQL Server效能就會顯著降低。

那麼我來分析看看這兩個SQL的實際執行計劃:從下面實際執行計劃,我們可以看到第一個SQL語句執行計劃從Index Seek 到Compute Scalar的資料流變粗了。這個表示第一個SQL語句的Index Seek返回的資料要多。

clipboard[2]

接下來,我們從Compute Scalar(進行一個標量計算並返回計算值)裡面可以看到Actual Number of Rows 的值為4688 和2 。

clipboard[3]

clipboard[4]

而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%。為什麼會這樣呢?我們是不是很迷惑?

clipboard[5]

英文:

      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;

clipboard[6]

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;

為什麼要用臨時表呢?不是子查詢就可以解決問題嗎?問題是實際應用當中,有些邏輯複雜的地方需要藉助臨時表解決,有時候子查詢反而不是一個好的解決方法。

clipboard[7]

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行資料。從下面我們可以看到使用者定義標量函式效能確實很糟糕。

clipboard[8]

下面測試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;

clipboard[9]

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;

clipboard[10]

DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS TIM