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。

相关文章
相关标签/搜索