在SQL SERVER中,自定义函数可以划分成:
1.内联表值函数
2.多语句表值函数
3.标量值函数
上述三类自定义函数如果使用不当,就会造成性能问题,本片重点关注“多语句表值函数”。
在多语句表值函数在每次调用时都需要使用到一个临时表来存放返回值,因此如果频繁调用该函数,会影响tempdb的性能。
测试代码:
--========================================================================= --创建测试表 GO SELECT * INTO TB001 FROM sys.all_objects GO SELECT * INTO TB002 FROM sys.all_columns GO --========================================================================= --创建内联表值函数 CREATE FUNCTION [dbo].[ufn_GetTop2Columns2] ( @object_ID BIGINT ) RETURNS TABLE AS RETURN ( SELECT TOP(2) name AS ColumnName FROM TB002 WHERE OBJECT_ID=@object_ID ) GO --========================================================================= --多语句表值函数 CREATE FUNCTION [dbo].[ufn_GetTop2Columns] ( @object_ID BIGINT ) RETURNS @result TABLE ( ColumnName NVARCHAR(200) ) AS BEGIN INSERT INTO @result SELECT TOP(2) name AS ColumnName FROM TB002 WHERE OBJECT_ID=@object_ID RETURN END GO SET STATISTICS IO ON SET STATISTICS TIME ON GO --========================================================================= --不使用表值函数 SELECT * FROM TB001 AS T1 CROSS APPLY (SELECT TOP(2) * FROM TB002 AS T2 WHERE T1.Object_id=T2.Object_id ) AS T3 --运行结果 --表 'Worktable'。扫描计数 1989,逻辑读取 15095 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --表 'TB002'。扫描计数 1,逻辑读取 54 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --表 'TB001'。扫描计数 1,逻辑读取 34 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --SQL Server 执行时间: --CPU 时间 = 94 毫秒,占用时间 = 543 毫秒。 --========================================================================= --使用多语句表值函数 SELECT * FROM TB001 AS T1 CROSS APPLY dbo.ufn_GetTop2Columns(T1.Object_id) AS T3 --运行结果 --表 '#756D6ECB'。扫描计数 1989,逻辑读取 1989 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --表 'TB001'。扫描计数 1,逻辑读取 34 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --SQL Server 执行时间: --CPU 时间 = 7129 毫秒,占用时间 = 7262 毫秒。 --========================================================================= --内联表值函数 SELECT * FROM TB001 AS T1 CROSS APPLY dbo.ufn_GetTop2Columns2(T1.Object_id) AS T3 --运行结果 --表 'Worktable'。扫描计数 1989,逻辑读取 14736 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --表 'TB002'。扫描计数 1,逻辑读取 54 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --表 'TB001'。扫描计数 1,逻辑读取 34 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --SQL Server 执行时间: --CPU 时间 = 62 毫秒,占用时间 = 186 毫秒。
多次运行发现,使用内联表值函数执行速度(186ms)快于未使用自定义函数的语句(543ms),而多语句表值函数的执行速度最慢(7262ms).
优化建议:
1. 将多语句表值函数改写成内联表值函数或不使用自定义函数的语句。
2. 将CROSS APPLY改写成INNER JOIN ,以减少多语句表值函数的调用次数
如将上面的语句改成:
CREATE FUNCTION [dbo].[ufn_GetTop2Columns3] ( ) RETURNS @result TABLE ( ColumnName NVARCHAR(200), Object_id BIGINT ) AS BEGIN INSERT INTO @result(ColumnName,Object_id) SELECT ColumnName,Object_id FROM (SELECT ROW_NUMBER()OVER(PARTITION BY T2.Object_id ORDER BY T2.Object_id) AS RID, T2.name AS ColumnName, T2.Object_id FROM TB002 AS T2 ) AS T3 WHERE RID<3 RETURN END GO --=================================================================== --将CROSS APPLY改写成INNER JOIN, SELECT * FROM TB001 AS T1 INNER JOIN [dbo].[ufn_GetTop2Columns3]() AS T3 ON T1.Object_id=T3.Object_id --表 'Worktable'。扫描计数 0,逻辑读取 0 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --表 'TB001'。扫描计数 1,逻辑读取 34 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --表 '#28ED12D1'。扫描计数 1,逻辑读取 5 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。 --SQL Server 执行时间: --CPU 时间 = 47 毫秒,占用时间 = 383 毫秒。
当然,不是所有的多语句表值函数都可以被改写,在优化时测试各种优化方案,寻找到一种最适合业务场景的方法。