原文地址:https://docs.microsoft.com/zh-cn/previous-versions/aa686015(v=msdn.10)?redirectedfrom=MSDN
SQL Server 用户定义的函数
John Papa
用户定义的函数 (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)并使用下列代码运行:
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 的建议发送到 [email protected].
John Papa 是一个棒球迷,在夏天的大多数夜晚都与他的两个小女儿、妻子和忠实的狗 Kadi 一起为 YanKees 队加油。他著有几本关于 ADO、XML 和 SQL Server 的书,并常常在诸如 VSLive 这样的行业大会上演讲。您可以与他联系:[email protected].