(《SQL Server 2005 编程入门经典》 第13章)

用户自定义函数和存储过程非常相似,但它们也有一些行为和能力的区别。

13.1 UDF的定义

用户自定义函数是有序的T-SQL语句集合,该语句集合能够预先优化和编译,并且可以作为一个单元来调用。它和存储过程的主要区别在于返回结果的方式。为了能支持多种不同的返回值,UDF比存储过程有更多地限制。

可以在使用存储过程的时候传入参数,也可以以参数的形式得到返回值。存储过程可以返回值,不过该值是为了指示成功或失败的,而非返回数据。

然而,可以在使用UDF的时候传入参数,但是可以不传出任何值。UDF还可以返回标量(scalar)值,这个值可以是大部分SQL Server的数据类型。UDF还可以返回表。

按照返回值的类型,UDF有两种类型:

  • 返回标量的UDF
  • 返回表的UDF

创建UDF的基本语法:

CREATE FUNCTION [<schema name>.]<function name> 
    ([<@parameer name> [AS] [<schema name>.]<scalar data type> 
        [= <default value>] 
    [, ...n]]) 
RETURNS {<scalar type>|TABLE [(<table definition>)]} 
    [WITH [ENCRYPTION]|[SCHEMABINDING]| 
    [RETURNS NOLL ON NULL INPUT|CALLED ON NULL INPUT]| 
    [EXECUTE AS {CALLER|SELF|OWNER|<'user name'>}]] 
[AS] {EXTERNAL NAME <external method name> 
BEGIN 
    [<function statements>] 
    {RETURN <type as defined in RESURNS clause>|RETURN 
    (<select statement>)} 
END }[;]

这里一个很大的问题在于是返回一个标量数据类型还是表,是使用基于T-SQL的函数还是使用CLR和.NET。

13.2 返回标量值得UDF

这种类型的UDF和大多数SQL Server内建的函数一样,会向调用脚本或存储过程返回标量值,例如GETDATE()和USER()函数就会返回标量值。

UDF可以返回除了BLOB、CURSOR和TIMESTAMP以外的任何SQL Server中有效的数据类型(包含用户自定义类型)。如果想返回整数,UDF也和存储过程不同的是:

  • UDF返回值的目的是提供有意义的数据,而不是说明成功或失败。
  • 在查询中可以内联地执行函数,而使用存储过程则不行。

示例——返回去掉时分秒的日期:

CREATE FUNCTION DayOnly(@Date DATETIME) 
RETURNS VARCHAR(12) 
AS 
BEGIN 
    RETURN CONVERT(VARCHAR(12), @Date, 101) 
END 
函数的使用方法如下:
SELECT * 
FROM Orders 
WHERE DayOnly(OrderDate) = DayOnly(GETDATE()) 
在一个UDF中调用另一个UDF:
CREATE FUNCTION AveragePrice() 
RETURNS MONEY 
WITH SCHEMABINDING 
AS 
BEGIN 
    RETURN (SELECT AVG(Price) FROM Titles) 
END 
GO 
 
CREATE FUNCTION PriceDifference(@Price MONEY) 
RETURN MONEY 
AS 
BEGIN 
    RETURN @Price – AveragePrice() 
END 
使用UDF可以大大增加查询语句的可读性,并实现了代码重用:
USE pubs 
SELECT Title, 
    Price, 
    AveragePrice() AS Average, 
    PriceDifference(Price) AS Difference 
FROM Titles 
WHERE Type = 'popular_comp'

13.3 返回表的UDF

可以对UDF返回的表执行JOIN,甚至对结果应用WHERE条件。相对简单的函数示例如下:

USE pubs 
GO 
 
CREATE FUNCTION fnAuthorList() 
RETURN TABLE 
AS 
RETURN ( 
SELECT au_id, 
    au_lname + ', ' + au_fname AS au_name 
    address AS address1, 
    city + ', ' + state + ', ' + zip AS address2 
FROM authors 
) 
GO

这样的话,使用这个函数就像使用表一样:

SELECT * 
FROM fnAuthorList()

使用返回表的UDF比使用视图的好处在于可以在UDF中将条件参数化,而视图不得不包含不想要的数据,然后再通过WHERE子句过滤。例如:

CREATE VIEW vSalesCount 
AS 
SELECT au.au_id, 
    au.aulname + ', ' + au.au_fname AS au_name, 
    au.address AS address1, 
    city + ', ' + state + ', ' + zip AS address2, 
    SUM(s.qty) AS SalesCount 
FROM authors au 
INNER JOIN titleauthor ta 
    ON au.au_id = ta.au_id 
INNER JOIN sales s 
    ON ta.title_id = s.title_id 
GROUP BY au.au_id, 
    au.au_lname + ', ' + au.au_fname, 
    au.address, 
    au.city + ', ' + au.state + ', ' + zip 
GO 
 
SELECT au_name, address1, address2 
FROM vSalesCount 
WHERE SalesCount > 25 
为了使得代码更简单,把所有的代码都封装到一个函数中:
USE pubs 
GO 
 
CREATE FUNCTION fnSalesCount(@SalesQty BIGINT) 
RETURNS TABLE 
AS 
RETURN ( 
SELECT au.au_id, 
    au.aulname + ', ' + au.au_fname AS au_name, 
    au.address AS address1, 
    city + ', ' + state + ', ' + zip AS address2, 
    SUM(s.qty) AS SalesCount 
FROM authors au 
INNER JOIN titleauthor ta 
    ON au.au_id = ta.au_id 
INNER JOIN sales s 
    ON ta.title_id = s.title_id 
GROUP BY au.au_id, 
    au.au_lname + ', ' + au.au_fname, 
    au.address, 
    au.city + ', ' + au.state + ', ' + zip 
HAVING SUM(qty) > @SalesQty 
) 
GO 
为了执行该函数,只需要调用它并提供参数:
SELECT * 
FROM fnSalesCount(25) 
再进一步,如果需要查询每一个销售超过25本书以上的作者和出版社的信息,这需要连接UDF返回的表:
SELECT DISTINCT p.pub_name, a.au_name 
FROM dbo.fnSalesCount(25) AS a 
INNER JOIN titleauthor AS ta 
    ON a.au_id = ta.au_id 
INNER JOIN titles AS t 
    ON ta.title_id = t.title_id 
INNER JOIN publishers AS p 
    ON t.pub_id = p.pub_id 
这里对函数进行了连接,就好像它是表或视图一样。唯一的区别在于可以对它进行参数化。
再进一步,UDF也可以递归调用,并同样存在最深32层的限制。例如,现在需要查询向Andrew Fuller报告的报告链的所有人员:
CREATE FUNCTION dbo.fnGetReports(@EmployeeID AS INT) 
RETURNS @Reports TABLE ( 
    EmployeeID INT NOT NULL, 
    ReportsToID INT NULL 
) 
AS 
BEGIN 
    DECLARE @Employee AS INT 
     
    INERT INTO @Reports 
    SELECT EmployeeID, ReportsTo 
    FROM Employees 
    WHERE EmployeeID = @EmployeeID 
     
    SELECT @Employee = MIN(EmployeeID) 
    FROM Employees 
    WHERE ReportsTo = @EmployeeID 
     
    WHILE @Employee IS NOT NULL 
    BEGIN 
        INSERT INTO @Reports 
        SELECT * 
        FROM fnGetReports(@Employee) 
         
        SELECT @Employee = MIN(EmployeeID) 
        FROM Employees 
        WHERE EmployeeID > @Employee AND ReportsTo = @EmployeeID 
    END 
     
    RETURN 
END 
GO 
现在如果需要查询员工ID为2的员工的报告链:
SELECT * FROM fnGetReports(2)

理解决定性

如果SQL Server需要建立一个索引的话,则它必须能决定性地定义(明确定义)对哪个项目进行索引。用户自定义函数可以是决定性的也可以是非决定性的。决定性并不是根据任何参数类型来定义的,而是根据函数的功能来定义的。如果给定了一个特有的有效输入集的话,那么每次函数就都能返回相同的结果,我们就说该函数是决定性的。内建函数是决定性的一个示例是SUM()。3、5、10的总和永远是18。但是GETDATE()的值就是非决定性的,因为每次调用它的时候它都会改变。

为了达到决定性的要求,函数必须满足4个标准:

  • 函数必须是限于模式的。这意味着函数所依赖的任何对象会有一个依赖记录,并且在没有删除这个依赖的函数之前都不允许改变这些对象。
  • 函数引用到的所有其他函数,都必须是决定性的。
  • 不能引用定义在函数本身之外的表(可以使用表变量和临时表)。
  • 不能在函数内部使用扩展的存储过程。

决定性的重要性在于它揭示了是否要在视图或计算列上建立索引。这意味着,如果视图或计算列引用非决定性函数的话,则在该视图或列上将不允许建立任何索引。

可以利用OBJECTPROPERTY函数检查一个函数是不是决定性的——它检验存储在对象的IsDeterministic属性来得到结论。例如,我们可以检验DayOnly函数的决定性:

SELECT OBJECTPROPERTY(OBJECT_ID('DayOnly'), 'IsDeterministic') 
给DayOnly()函数添加上WITH SCHEMABINDING选项后,该函数将是决定性的:
ALTER FUNCTION DayOnly(@Date DATETIME) 
RETURNS VARCHAR(12) 
WITH SCHEMABINDING 
AS 
BEGIN 
    RETURN CONVERT(VARCHAR(12), @Date, 101) 
END