SQL Server自定义函数(User Defined Function,简称UDF)是数据库中用于封装可复用逻辑的对象。它允许开发者将复杂的计算、字符串处理或数据转换逻辑写成独立模块,在查询、视图或存储过程中像系统函数一样调用。按照返回结果形态,主要分为标量值函数(返回单个值)和表值函数(返回行集),后者又包括内联表值函数和多语句表值函数。理解它们的差异并掌握书写规范,能显著提升SQL代码的可维护性。

一、标量值函数实例:计算带税金额
标量函数最常见的用途是封装固定计算公式。例如电商系统中,订单金额需要根据不同税率计算含税价。如果每次查询都写一遍乘法和四舍五入,既容易出错也不利于税率调整。我们可以创建一个接收金额和税率、返回小数的函数。
下面的示例创建了一个名为dbo.fn_CalcTax的标量函数,它使用RETURNS指定返回类型,在函数体内通过RETURN返回计算结果。注意在函数中不能使用会产生副作用的语句,比如INSERT或UPDATE,只能做纯计算。
CREATE FUNCTION dbo.fn_CalcTax
(
@Amount DECIMAL(10,2),
@Rate DECIMAL(4,2)
)
RETURNS DECIMAL(10,2)
AS
BEGIN
DECLARE @Result DECIMAL(10,2);
-- 计算含税金额并保留两位小数
SET @Result = ROUND(@Amount * (1 + @Rate), 2);
RETURN @Result;
END;
调用方式非常直观,可以放在SELECT列表里直接使用:
SELECT
OrderID,
Amount,
dbo.fn_CalcTax(Amount, 0.13) AS TaxAmount
FROM dbo.Orders;
这种写法的好处是业务逻辑集中管理,当税率规则变化(如增加减免逻辑)时,只需修改函数本身,所有调用处自动生效。不过需要注意,在大数据量查询中,逐行调用标量函数可能带来一定的CPU开销,后续会提到优化思路。
二、内联表值函数实例:拆分逗号分隔字符串
很多遗留系统把多个ID存进一个带逗号的字段,查询时又需要按行展开。内联表值函数(Inline TVF)非常适合这种场景,它在结构上类似带参数的视图,SQL Server能将其逻辑合并到主查询执行计划中,性能优于多语句表值函数。
以下函数接收字符串和分隔符,利用STRING_SPLIT(SQL Server 2016及以上支持)返回拆分后的行集。由于是内联形式,没有BEGIN/END包裹的函数体,直接RETURN一个SELECT即可。
CREATE FUNCTION dbo.fn_SplitString
(
@Str NVARCHAR(MAX),
@Delimiter NCHAR(1)
)
RETURNS TABLE
AS
RETURN
(
SELECT
value AS Item,
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS Seq
FROM STRING_SPLIT(@Str, @Delimiter)
);
使用时把它当作一张表,通过CROSS APPLY关联主表:
SELECT
u.UserID,
s.Item AS RoleID
FROM dbo.Users u
CROSS APPLY dbo.fn_SplitString(u.RoleList, ',') s;
内联表值函数不会生成独立的临时结果集,优化器能把STRING_SPLIT的扫描直接推入主查询,因此面对百万级数据也比自定义循环拆分快很多。如果要在更早版本实现,可以用递归CTE写在函数里,但同样建议保持内联结构。
三、多语句表值函数与适用边界
当返回结果需要复杂加工(比如先过滤再聚合再关联)时,可以使用多语句表值函数。它内部有BEGIN/END块,先声明表变量,再插入数据,最后RETURN该变量。虽然灵活,但优化器难以预估行数,容易生成次优计划。
示例:返回一个部门下所有员工的汇总信息。
CREATE FUNCTION dbo.fn_DeptSummary
(
@DeptID INT
)
RETURNS @Result TABLE
(
EmpID INT,
EmpName NVARCHAR(50),
TotalSales DECIMAL(12,2)
)
AS
BEGIN
INSERT INTO @Result
SELECT
e.EmpID,
e.EmpName,
ISNULL(SUM(s.Amount), 0)
FROM dbo.Employee e
LEFT JOIN dbo.Sales s ON e.EmpID = s.EmpID
WHERE e.DeptID = @DeptID
GROUP BY e.EmpID, e.EmpName;
RETURN;
END;
调用时同样放在FROM中:
SELECT * FROM dbo.fn_DeptSummary(3);
要注意,多语句表值函数因表变量缺乏统计信息,在传入参数变化大时可能走错误的执行计划。若性能敏感,建议改写为内联表值函数或直接使用带参数的视图、存储过程输出结果集。
四、自定义函数的性能与维护建议
使用UDF时,有几个实践原则能减少后期麻烦。第一,标量函数尽量避免在WHERE条件里包裹列,例如WHERE dbo.fn_GetYear(OrderDate)=2023会阻止索引查找,应改成范围比较。第二,优先选择内联表值函数替代游标或循环拆分逻辑。第三,函数内不要访问其他表以外的外部资源,保持纯计算或数据集操作。
另外,函数一旦被视图或计算列引用,修改定义前需先删除依赖对象。可以通过sys.sql_expression_dependencies查看引用链。合理的函数粒度应该是单一职责:一个函数只解决一类转换,不要写成几十行的大杂烩,否则测试和排错成本会急剧上升。
通过上面三个实例可以看出,SQL Server自定义函数并不是万能胶,而是把确定性逻辑从重复SQL中抽离出来的工具。在报表、接口层和数据清洗环节用对地方,代码量和运维负担都能明显下降。
SQL_Server自定义函数UDF修改时间:2026-08-10 10:03:36