[转载]SQL Server 用户定义的函数

原文地址:https://docs.microsoft.com/zh-cn/previous-versions/aa686015(v=msdn.10)?redirectedfrom=MSDNgit

SQL Server 用户定义的函数

John Papasql

用户定义的函数 (UDF) 是准备好的代码片断,它能够接受参数,处理逻辑,而后返回某些数据。根据 SQL Server Books Online,SQL Server™ 2000 中的 UDF 能够接受从 0 到 1024 的任意个数的参数,不过我必须认可,我还何尝试将 1024 个参数传递到 UDF 中。UDF 的另外一个关键特征是返回一个值。取决于 UDF 的类型,调用例程可使用这个值来继续处理它的数据。所以,若是 UDF 返回单一值(标量值),调用例程就能够在任何可以使用标准变量或文字值的地方使用这个值。若是 UDF 返回一个行集,则调用例程能够循环访问该行集,联接到该行集,或简单地从该行集中选择列。编程

虽然如今大多数编程语言已经暂时支持函数,但只有 SQL Server 2000 引入了 UDF。存储过程和视图在 SQL Server 中可用的时间远早于 UDF,但这些对象中的每个在 SQL Server 开发中都有本身适当的位置。存储过程能够很好地用于处理复杂的 SQL 逻辑、保证和控制对数据的访问,以及将行集返回到调用例程,不管此例程是基于 Visual Basic® 的程序,仍是另外一个 Transact-SQL (T-SQL) 批处理文件。与视图不一样,存储过程是已编译的,这使得它们成为用来表示和处理频繁运行的 SQL 语句的理想候选者。视图能够很好地用于控制对数据的访问,但它们的控制方式与存储过程不一样。视图仅限于生成该视图的基础 SELECT 语句中的某些列和行。于是视图经常使用于表示经常使用的 SELECT 语句,该语句能够联接多个表、使用 WHERE 子句,以及公开特定的列。在联接到其余表和视图的 SQL 语句的 FROM 子句中常常会发现视图。架构

在其核心部分,UDF 既相似于视图,也相似于存储过程。像视图同样,UDF 能够返回一个行集,该行集可用于 JOIN 中。所以,当 UDF 返回一个行集并接受参数时,它像一个您能够联接到的存储过程、或者一个参数化的视图。可是,正如我将演示的,UDF 能够作到这一点,甚至更多。编程语言

有两种主要的 UDF 类型:返回标量值的 UDF 和返回表值的 UDF。在表值 UDF 中,您将找到返回内联表和多语句表的 UDF(请参见图 1)。在如下部分中,我将对每种类型都加以关注。函数

标量 UDF工具

返回标量值的 UDF 最相似于许多编程语言所引用的做为函数的内容。它们返回由标量数据类型(例如,integer、varchar(n)、char(n)、money、datetime、bit,等等)组成的单一值。若是用户定义的数据类型 (UDDT) 基于标量数据类型,UDF 也能够返回这些数据类型。使用返回内联或多语句表的 UDF,能够经过表数据类型返回行集。然而,并不是全部的数据类型均可以从 UDF 中返回。例如,UDF 没法返回下列数据类型中任何一个的值:text、ntext、image、cursor、或 timestamp。性能

返回标量数据类型的 UDF 能够用于多种状况,以使代码具备更好的可维护性、可重用性和更少的复杂性。当 T-SQL 代码的相同段在几个地方(可能由几个存储过程和批 SQL 语句)使用时,这会很是有用。例如,假定一个应用程序中的几个部分都须要查找产品是否必须从新订购。在每一个须要此操做的地方,代码能够检查从新订购等级,并将它与库存量加订购量的和相比较。然而,由于这个代码在几个地方用到,因此能够改成使用 UDF 以减小代码块,并使得万一须要更改时维护函数更加容易。这样的 UDF 可能看起来像图 2 中的代码,并可使用如下 SQL 语句进行调用:测试

SELECT    ProductID,
    ReorderLevel, 
    UnitsInStock, 
    UnitsOnOrder,
    dbo.fnNeedToReorder(ReorderLevel, UnitsInStock, UnitsOnOrder) 
AS sNeedToReorder
FROM    Products

图 2 ** 中,fnNeedToReorder UDF 执行计算并返回适当的值。这原本能够经过 CASE 语句在 SELECT 子句内完成,但若是改成使用 UDF,代码就会简洁得多。并且更容易传播到其余可能须要相同逻辑的地方。假定一个应用程序中有几个部分须要肯定是否要从新订购产品,那么图 2 中的 UDF 确实变得有价值,由于它使得当逻辑改变时应用程序更容易维护。例如,从新订购已经终止的产品并非颇有意义。所以,经过更改 UDF 以说明这个业务规则,能够在一个地方更改此逻辑(请参见图 3)并使用下列代码运行:spa

SELECT    ProductID,
    ReorderLevel, 
    UnitsInStock, 
    UnitsOnOrder,
    dbo.fnNeedToReorder(ReorderLevel, UnitsInStock, UnitsOnOrder, 
                        Discontinued) AS sNeedToReorder
FROM    Products

请注意,UDF 是使用由两个部分(对象全部者和对象名)组成的名称调用的。当使用返回标量数据类型值的 UDF 时须要该对象的全部者。能够受权全部调用 UDF 的地方也必须加以更改,方法是将第四个参数 (Discontinued) 添加到 UDF 中。为了更容易维护,我能够从新编写 UDF,以便使用每一行的 ProductID 来检索数据自己,如图 4 所示。这种技术更容易维护,由于它不须要任何调用例程来更改逻辑改变时更改 UDF 的方式,只要能够从当前 Products 表行中提取数据便可。然而,要得到这种可维护性,会有性能方面的损失。图 4 中的 UDF 必须为每一个从调用例程中返回的行从 Products 表中检索行。由于调用例程已经从 Products 表中检索每一个行,因此若是该表有 77 行,则代码将执行 77 次 SELECT 语句(从主 SELECT 语句中返回每行一次)。虽然每一个 SELECT 都是基于主键字段 (ProductID) 进行选择的,于是会很快,可是当行集很是大或者 SELECT 语句效率较低时,性能就会受到负面影响。图 4 中的代码能够经过如下 SQL 片断来调用:

SELECT    ProductID,
    ReorderLevel, 
    UnitsInStock, 
    UnitsOnOrder,
    dbo.fnNeedToReorder(ProductId) AS sNeedToReorder
FROM    Products

在 SELECT 语句中使用这个函数的可选方法是,在名为 NeedToReorder 的 Products 表中建立一个计算所得的列。该列并不定义为一种数据类型,而是定义为如图 3 所示的 fnNeedToReorder UDF 的返回值。要添加此列,我能够按如下方式更改 Products 表,以指示应计算这个列:

ALTER TABLE Products
    ADD  NeedToReorder AS dbo.fnNeedToReorder(ReorderLevel,   
    UnitsInStock, UnitsOnOrder, Discontinued)

通用 UDF 和嵌套

至此,我已经展现了使用返回标量值的 UDF 解决同一问题的几种方式。还有其余有用的 UDF 应用程序,其中包括 T-SQL 中还未准备好可用的函数。一个例子是专用格式化函数。例如,电话号码一般存储(不带格式化字符)在 char(10) 列中,这些列表示区号和电话号码(假定这是一个美国的号码)。UDF 能够用于在格式化结构中检索电话号码(请参见图 5)。所以,检索和格式化电话号码像下面同样简单:

SELECT dbo.fnCOM_FormatTelephoneNumber ('3335558888')

可使用这种技术建立任何经常使用函数,以增长 SQL Server 中可用函数的数量。另外一个示例是将日期格式化为带有前导零的 MM/DD/YYYY 格式的函数:

CREATE FUNCTION fnCOM_StandardDate (@dtDate DATETIME)
    RETURNS VARCHAR(10)
AS
BEGIN
    RETURN 
        dbo.fnCOM_2Digits (CAST(MONTH(@dtDate) AS VARCHAR(2))) + '/' +
        dbo.fnCOM_2Digits (CAST(DAY(@dtDate) AS VARCHAR(2))) + '/' +
        CAST(YEAR(@dtDate) AS VARCHAR(4)) 
END

fnCOM_StandardDate UDF 接受日期时间值,并返回 MM/DD/YYYY 格式的 varchar(10) 值。固然,这很简单,若是您的应用程序经常须要特定格式,那么这种技术就可使它更容易维护。在前面的代码中须要注意的一个关键部分是嵌套 UDF 的使用。fnCOM_StandardDate UDF 两次调用 fnCOM_2Digits UDF(在下一个示例中显示),每次都在小于 10 的日或月前放置一个前导零。

CREATE FUNCTION fnCOM_2Digits (@sValue VARCHAR(2))
    RETURNS VARCHAR(2)
AS
BEGIN
    IF (LEN(@sValue) < 2)
        SET @sValue = '0' + @sValue 
    RETURN @sValue
END

UDF 能够互相嵌套,只要其中的 UDF 是先建立的便可。使用嵌套函数的一个 catch 是非肯定性内置函数(例如 getdate 函数),不能在另外一个 UDF 内嵌套(不然会引起 SQL Server 错误)。非肯定性函数是用彻底相同的参数调用屡次时可能返回不一样结果的函数。getdate 函数属于这一类,由于每次调用时,它会返回新的当前日期和时间。另外一个经常使用的非肯定性内置函数是 NewID 函数。它也是非肯定性的,由于它老是返回惟一的 GUID,因此 NewID 函数一样不容许在 UDF 内嵌套。

表值 UDF

表值 UDF 的类别中有两种子类型:返回内联表值的 UDF 和返回多语句表值的 UDF。返回内联表的 UDF 经过 SQL Server 表数据类型返回一个行集。它们使用构成函数体的单一 SELECT 语句进行定义。返回内联表值的 UDF 不能在定义它将返回的表的 SQL SELECT 语句以外包含其余 T-SQL 逻辑。然而,它们比返回多语句表的 UDF 要容易建立,由于它们没必要定义要返回的确切表结构。返回内联表的 UDF 从 SELECT 语句自己推断行集的结构。所以,UDF 将返回的列由 SELECT 列表中的列肯定。下列代码显示了 fnGetEmployeesByCity UDF,它接受一个城市,并返回包含全部员工名字、姓和地址的表:

CREATE FUNCTION fnGetEmployeesByCity (@sCity VARCHAR(30))
    RETURNS TABLE
AS
RETURN
    (
        SELECT    FirstName, LastName, Address
        FROM    Employees
        WHERE    City = @sCity 
    )
GO

能够从这个返回内联表值的 UDF 中选择或者甚至联接到它,由于它经过表数据类型返回一个行集,以下所示:

SELECT * FROM dbo.fnGetEmployeesByCity('seattle')

请注意,UDF 是使用由对象全部者和对象名这两个部分组成的名称调用的。然而,当使用返回表数据类型值的 UDF 时,对象全部者不是必需的(但倒是可接受的)。表值 UDF 很是灵活,由于它们能够像准备好的和参数化的视图(若是存在)同样使用。在表值 UDF 中,您可使用参数,得到准备好的查询的性能,并从获得的行集(或本例中的表)中联接或选择。

尽管这种 UDF 类型是简洁的,但重要的是要记住,若是您要向这种 UDF 中添加其余逻辑,就必须将其转换成返回多语句表值的 UDF。另外,返回内联表值的 UDF 在 SELECT 语句中也不能有 ORDER BY 子句(除非它与 TOP 子句一块儿使用)。

返回多语句表的 UDF 显式定义要返回的表的结构。它经过在 RETURNS 子句中正肯定义列名称和数据类型来作到这一点。所以,它会使用比返回内联表值的 UDF 稍多的代码来创建表结构。然而,与返回内联表值的 UDF 相比,它有几个优势,其中包括容纳更复杂的、更大量的 T-SQL 逻辑块的功能。顾名思义,返回多语句表值的 UDF 容许多个语句定义 UDF。所以,诸如流控制、分配、游标、SELECTS、INSERTS、UPDATES 和 DELETES 等语句都是容许的,而且均可以存在于单个 UDF 中。因此,与返回内联表的 UDF 相反,返回多语句表的 UDF 并不限定于单个 SELECT 语句,也不由止对返回行集进行排序。

图 6 显示了如何将返回内联表值的 UDF(我刚才展现的代码片断中的)从新编写为返回多语句表值的 UDF。所以,内联类型能作到的,多语句类型都能作到。返回多语句表的 UDF 的更复杂的用途包括按城市检索全部员工,但若是没有客户与特定的城市相匹配,就返回一个虚行,其中的 Address 字段填写“在指定的城市中未找到匹配的员工”,如图 7 中所示。

包装

还有其余一些关键因素能够帮助建立任何类型的功能强大的 UDF,其中的一种即是递归。UDF 支持递归,以便一个 UDF 能够从自身中调用自身。基本上,递归只是嵌套 UDF,惟一不一样的地方在于您所嵌套的 UDF 正是您所在的 UDF。这在某些状况中可能很是有用,包括在建立一个必须计算某个因子或评估一个字符串中每一个字符的 UDF 时。在 SQL Server 2000 中,递归的限制深度为 32 层,超出限制会引起错误。

还须要指出的是,一个 UDF 能够绑定到它所引用的基础对象架构。为此,UDF 必须使用 WITH SCHEMABINDING 子句来进行建立。若是 UDF 是以这种方式建立的,则当有人试图更改一个基础对象架构而没有先删除架构绑定时,就会生成并引起错误。采用这种选择将有助于确保不会由于基础对象架构中的更改而引发意外的 UDF 中断。

当评估 UDF 时,考虑性能和可维护性之间的平衡是相当重要的。虽然 UDF 能够减小经常使用代码的数量(用做经常使用函数库的一部分),能够提高更短的代码块,而且一般比相同 SQL 逻辑的其余类型更容易维护,可是,若是不先考虑任何缺点就使用 UDF,这将是不计后果的。

若是性能严重下降,那么使用 UDF 就不是一个好主意。例如,假定有一个执行 SQL SELECT 语句的 UDF,执行该语句须要一秒钟。若是此 UDF 在 SELECT 或 WHERE 子句中使用,它将为每一行执行。所以,执行主查询所花费的时间会急剧增长,这取决于评估和返回的行数以及适当的索引类型这样的因素。若是是这种状况,则在使用 UDF 以前,要仔细地权衡所做的选择并进行一些性能测试。然而,使用执行计算的 UDF(例如图 3 中所显示的)几乎不影响查询性能。正如任何工具同样,若是在实际投入以前正确地使用并进行相应地评估,那么UDF 会提供极大的便利和可维护性。

请将您的问题和给 John 的建议发送到 mmdata@microsoft.com.

John Papa 是一个棒球迷,在夏天的大多数夜晚都与他的两个小女儿、妻子和忠实的狗 Kadi 一块儿为 YanKees 队加油。他著有几本关于 ADO、XML 和 SQL Server 的书,并经常在诸如 VSLive 这样的行业大会上演讲。您能够与他联系:mmdata@microsoft.com.

相关文章
相关标签/搜索