SQL SERVER 用戶自定義函數(UDF)深刻解析

本文內容概要:

  1. UDF 概念、原理、優缺點、UDF 的分類sql

  2. 詳細講述3種 UDF 的建立、調用方法以及注意事項數據庫

  3. UDF 的實踐建議

基本原理:

UDF:user-defined functions,用戶自定義函數的簡稱。架構

UDF 是一個例程,它接受參數、執行操做並返回該操做的結果。根據定義,結果能夠是標量值(單個)或表。函數

UDF 的優勢:

  1. UDF 能夠把複雜的邏輯嵌入到查詢中。UDF 能夠爲複雜的表達式建立新函數。
  2. UDF 能夠運用在一個表達式或 SELECT 語句的 FROM 子句中,而且還能夠綁定到架構。此外,UDF 還能夠接受參數。UDF 有助於實施一致性和可重用性。

UDF 的缺點:

該函數一旦誤用會產生潛在的性能問題。必須針對WHERE子句的每一行執行的任何函數,不論是用戶定義的函數仍是系統函數,都將減慢執行速度。性能

UDF 的類型:

        UDF 主要有 3 種類型(SQL Server Management Studio 把內聯表值函數與多語句表值函數放到了一個組中):優化

  1. 標量函數
  2. 內聯表值函數
  3. 多語句表值函數

1、標量函數


標量函數是返回一個具體值的函數。函數能夠接收多個參數、執行計算而後返回一個值。返回值經過RETURN命令返回。用戶定義的函數中的每一個可能代碼路徑都以RETURN命令結尾。spa

標量函數能夠運用於 SQL Server 中的任何表達式,甚至在 CHECK 約束的表達式中也可使用(但不推薦這種用法)。code

  • 函數限制

標量函數必須是肯定性的,也就是說標量函數必須反覆地爲相同的輸入參數返回相同的值。所以,如newid()函數和rand()函數不容許出如今標量函數中。不容許用戶定義標量函數更新數據庫、調用存儲過程或調用DBCC命令,惟一的例外是能夠更新表變量。用戶定義函數不能返回BLOB(二進制大型對象)數據,如text、next、timestamp和image數據類型變量。也不能返回表變量可cursor數據類型。對於錯誤處理,UDF 也不包含 TRY...CATCH 或 RAISERROR。對象

UDF 能夠調用嵌套深度爲 32 層之內的其餘用戶定義函數,或者遞歸調用本身到 32 層的深度。固然,這只是理論限制,嵌套函數會嚴重影響性能,應儘量避免使用嵌套函數。blog

  • 建立方法

1 CREATE FUNCTION FunctionName (InputParameters) 
2 RETURNS DataType 
3 AS
4 BEGIN
5   Code;
6   RETURN Expression;
7 END;

InputParameters 輸入參數包含數據類型定義。參數能夠設置默認值(Parameter = default ),須要注意的是在 UDF 中有默認值的參數並不能成爲可選參數,爲在調用函數時請求到默認值,須要把關鍵字 DEFAULT 傳遞到函數的默認值參數位置。

示例1:下面的 UDF 執行一個簡單的數學計算,其中第二個參數帶有默認值。

CREATE FUNCTION dbo.ufnCalculate
(@Numer_a numeric(5,2),
 @Numer_b numeric(5,2) = 1.0)
RETURNS numeric(5,2)
AS
BEGIN
  RETURN @Numer_a / @Numer_b ;
END;
GO

select dbo.ufnCalculate(15.3 , 6.54),
       dbo.ufnCalculate(9.0 , DEFAULT);

結果:
------  ------
2.38      9.00

示例2:計算並返回某個時間所在月份的天數。

CREATE FUNCTION [dbo].[GetMonthDay](@date datetime)
RETURNS int
AS 
BEGIN
  DECLARE @date1 datetime
  SELECT @date1 =Dateadd(MM,1,@date)
  RETURN day(Dateadd(DD,-day(@date1),@date1))
END;
  • 調用方法

在接受單值的表達式中,標量函數可用於任何地方。用戶定義的標量函數必須經過一個最少有兩部分的名稱(全部者.函數名)來調用。

下面的腳本演示了在數據庫的訂單表中調用示例2中的函數及其返回值。

SELECT S.BIL_DD,dbo.GetMonthDay(BIL_DD) as DAYS_M 
FROM Orders S

結果
BIL_DD        DAYS_M
------        ------
2019-01-31     31
2019-02-15     28

2、內聯表值函數


與視圖類似,內聯表值函數也是爲一個存儲的SELECT語句封裝。內聯表值函數保留了視圖的優勢,還添加了一些參數。

  • 建立方法

內聯表值用戶定義函數沒有BEGIN / END主體。SELECT語句是做爲一個虛擬數據表返回的:

CREATE FUNCTION FunctionName (InputParameters)
RETURNS Table 
AS 
RETURN (Select Statement);

示例:下面的示例返回某個客戶所訂購產品的彙總狀況。

CREATE FUNCTION dbo.ufnGetProductTotalByCust (@custNo varchar (10))
RETURNS Table 
AS
RETURN(
SELECT H.CUS_NO,B.PRD_NO,SUM(B.QTY) as TOTAL_PRD 
FROM TF_POS AS B       --訂單貨品明細表
LEFT JOIN MF_POS  AS H --訂單客戶信息表
       ON H.OS_NO=B.OS_NO 
WHERE H.CUS_NO=@custNo  
GROUP BY H.CUS_NO,B.PRD_NO );
GO
  • 調用方法

經過dbo.ufnGetProductTotalByCust查詢客戶代號爲"CT060228" 的產品彙總數據,函數出如今SELECT語句的FROM部分:

SELECT PRD_NO,TOTAL_PRD FROM 
dbo.ufnGetProductTotalByCust('CT060228') 
ORDER BY PRD_NO DESC 

返回結果(部分):

PRD_NO           TOTAL_PRD
------------   ------------------
10910030006      5792.00000000
10910040003      10776.00000000
10912060014      11442.00000000
10913040009      9276.00000000
11410030028      900.00000000
......
  • 與視圖的關係

與視圖相比,內聯表值函數的優點在於其可使用參數。而視圖不包含參數,並且在運行時想要限制結果須要把 WHERE 子句添加到調用視圖的 SELECT 語句中來實現。

示圖的調用示例,假設已經存在視圖 dbo.vwProductTotalByCust,調用視圖時,在 SELECT 語句中添加了一個 WHERE 子句限制:

SELECT * FROM dbo.vwProductTotalByCust WHERE cus_no='CT060228' 

  • 關聯方法

表值用戶定義函數的關聯可使用 APPLY 命令,從而使 UDF 針對由主查詢處理的每一行接受一個不一樣的參數值。

APPLY 命令具備兩種形式。最普通的一種形式是 CROSS APPLY,它運行起來更像一個內聯接。CROSS APPLY 命令聯接主查詢的數據與來自用戶自定義函數的任意表值數據集。若是未從UDF 返回數據,那麼主查詢的行也不能返回,以下圖的例子所示:

SELECT T.PRD_NO,P.NAME,T.TOTAL_PRD 
FROM PRDT P --產品資料表
CROSS APPLY dbo.ufnGetProductTotalByCust('CT060228')  T
ORDER BY T.PRD_NO DESC 

結果:
PRD_NO         NAME               TOTAL_PRD
------------  ------------        ------------------------
10910030006   3pcs storage jar    5792.00000000
10910040003   2pcs storage jar    10776.00000000
10912060014   4pcs spice jar      11442.00000000
10913040009   6pcs spice jar      9276.00000000
11410030028   salad dressing      900.00000000
......

CROSS APPLY 的第2種形式是 OUTER APPLY 命令,操做上與左聯接類似。這種形式下,主查詢的行將包含在結果集中,而無論 UDF 返回的虛擬表是否爲空。

標量函數和內聯表值函數可生成完成相同的結果集,那麼這二者的區別是什麼呢?

標量函數針對每一行運行一次,而內聯表值函數由查詢優化器處理,很是相似於視圖。由於內聯表值函數會由查詢優化器進行處理,因此建議儘量優先使用內聯表值函數,而非標量函數。

  • 架構綁定

架構綁定阻止更改或刪除函數所依賴的任何對象。若是架構綁定函數引用了某個表A,那麼表A不可更改或刪除,但能夠將列添加到表A。

架構綁定的方法:在函數建立語句的 RETURNS 以後和 AS 以前添加選項 WITH SCHEMA BINDING,以下所示:

1 CREATE FUNCTION FunctionName (InputParameters) 
2 RETURNS DataType 
3 WITH SCHEMA BINDING 
4 AS
5 BEGIN
6   Code;
7   RETURN Expression;
8 END;

可使用ALTER修改函數,使其再也不包含架構綁定,以即可以修改引用對象。

3、多語句表值函數


將標量函數與內聯表值函數的功能結合起來就構成了複雜的多語句表值函數。

特徵:這種類型的函數建立了一個表變量,將它置於代碼中,而後從函數返回,以便能在SELECT語句中使用。

優勢:能夠代碼內生成複雜結果集,以便在SELECT語句中使用,在查詢中構建複雜邏輯,並解決那些沒有遊標就很難解決的問題。

  • 建立方法

建立多語句表值函數的語法與建立標量函數的語法類似:

CREATE FUNCTION FunctionName (InputParamenters)
RETURNS @TableName TABLE (columns)
AS
BEGIN; 
    Code to populate table variable
    RETURN;
END;

示例:下面的過程構建了一個返回基本結果集的多語句表值用戶定義的函數,函數首先在 CREATE FUNCTION 頭中建立了一個名爲 @PruductList 的表變量,在函數體中,兩個 INSERT 語句置於@ProductList 表變動中,若是函數執行完畢,表變動 @ProductList 將做爲函數的輸出傳回。

ufnGetProductsAndOrderTotals函數返回Product表中的每一個產品和每一個產品的訂單總數。

CREATE FUNCTION ufnGetProductsAndOrderTotals() 
RETURNS @ProductList TABLE 
            (ProductID int,
             ProductName nvarchar(100),
             TotalOrders int)
AS
BEGIN;
        INSERT @ProductList(ProductID,ProductName) 
        SELECT ProductID,Name 
        FROM Product;

        UPDATE p1 
        SET TotalOrders = 
            (SELECT sum(sod.OrderQty) 
             FROM @ProductList ip1 
               JOIN SalesOrderDetail sod 
               ON ip1.ProductID = sod.ProductID
             WHERE ip1.ProductID = p1.ProductID) 
        FROM @ProductList p1 ;

        RETURN;
END;
  • 調用方法

只須要在SELECT語句的FROM部分引用該函數,便可查詢到函數的執行結果。下面的代碼檢索ufnGetProductsAndOrderTotals函數的結果:

SELECT ProductID,ProductName,TotalOrders 
FROM ufnGetProductsAndOrderTotals() 
ORDER BY TotalOrders DESC 

結果集以下:

ProductID        ProductName                TotalOrders
------------       -------------------          --------------
715                 4 PCS Storage Jar           8311
780                 6 PCS Spice Jar             6800
......

4、UDF 的實踐建議

無疑 UDF 爲咱們的 T-SQL 選項添加了靈活性,但若是這些函數運用不當,帶來的性能缺陷也是很嚴重的。UDF 並不能成爲子查詢、視圖或存儲過程的替代物。

從上面的示例,咱們不難看出,三種類型函數能夠產生基本相同的結果集,實踐中能夠將本身的函數定義爲其種任意一種。

建議一:性能最優化

若是選擇 UDF 來封裝查詢邏輯,則建議遵循下面的這些基本原則:

  1. 相對於多語句表值函數,儘量優先選擇內聯表值函數;
  2. 儘可能避免使用標量函數,儘量使用內聯表值函數取代它;
  3. 若是須要使用多語句表值函數,則對比一下存儲過程是否是更合適的解決文案。雖然須要花更多的時間,但考慮長期的性能影響,仍是值得的。

建議二:命名一致性

爲方便咱們的T-SQL更易於閱讀更容易排除故障,咱們應該確保爲全部的 UDF 建立某種統一類型的命名約束。最經常使用的方法是採用名稱前綴,更進一步,可讓前綴代表 UDF 是標量函數、內聯表值函數仍是多語句表值函數。例如,返回每一個產品類別的月平均銷售額的內聯表值函數,能夠將其命名爲 udfAvgMonSalesPerCategory 或 ifn_AvgMonSalesPerCategory。

相關文章
相關標籤/搜索