SQL Server 执行速度因向内联表函数提供参数的方式而异
Posted
技术标签:
【中文标题】SQL Server 执行速度因向内联表函数提供参数的方式而异【英文标题】:SQL Server execution speed varies wildly depending on how parameters are provided to an inline table function 【发布时间】:2019-09-30 13:03:16 【问题描述】:我正在调查 SQL Server 中内联表函数的执行速度问题。或者这就是我认为问题所在。我遇到了
T-SQL code is extremely slow when saved as an Inline Table-valued Function
这看起来很有希望,因为它描述了我所看到的,但我似乎遇到了相反的问题 - 当我将变量传递给我的函数时,需要 17 秒,但是当我在查询窗口中运行我的函数代码时,对变量使用 DECLARE 语句(我认为这有效地使它们成为文字),它以毫秒为单位运行。相同的代码,相同的参数 - 只是将它们包装在一个内联表函数中似乎会拖累它。
我试图将我的查询减少到仍然表现出该行为的尽可能少的代码。我正在使用许多现有的内联表函数(所有这些函数多年来都运行良好),并设法将我的代码精简为只需要调用一个现有的内联表函数即可突出显示速度差异。但在这样做的过程中,我注意到了一些非常奇怪的事情
SELECT strStudentNumber
FROM dbo.udfNominalSnapshot('2019', 'REG')
需要 17 秒,而
DECLARE @strAcademicSessionStart varchar(4) = '2019'
DECLARE @strProgressCode varchar(12)= 'REG'
SELECT strStudentNumber
FROM dbo.udfNominalSnapshot(@strAcademicSessionStart, @strProgressCode)
需要几毫秒!因此,与将代码包装在内联表函数中无关,而是与如何将参数传递给其中的嵌套函数有关。根据引用的文章,我猜测有两种不同的执行计划在起作用,但我不知道为什么/如何,更重要的是,我能做些什么来说服 SQL Server 使用高效的?
附:这是响应评论请求的内部 UDF 调用的代码
ALTER FUNCTION [dbo].[udfNominalSnapshot]
(
@strAcademicSessionStart varchar(4)='%',
@strProgressCode varchar(10)='%'
)
RETURNS TABLE
AS
RETURN
(
SELECT TOP 100 PERCENT S.strStudentNumber, S.strSurname, S.strForenames, S.strTitle, S.strPreviousSurname, S.dtmDoB, S.strGender, S.strMaritalStatus,
S.strResidencyCode, S.strNationalityCode, S.strHESAnumber, S.strSLCnumber, S.strPreviousSchoolName, S.strPreviousSchoolCode,
S.strPreviousSchoolType,
COLLEGE_EMAIL.strEmailAddress AS strEmailAlias,
PERSONAL_EMAIL.strEmailAddress AS strPersonalEmail,
P.[str(Sub)Plan], P.intYearOfCourse, P.strProgressCode,
P.strAcademicSessionStart, strC2Knumber AS C2K_ID, AcadPlan, strC2KmailAlias
,ISNULL([strC2KmailAlias], [strC2Knumber]) + '@c2kni.net' AS strC2KmailAddress
FROM dbo.tblStudents AS S
LEFT JOIN
dbo.udfMostRecentEmail('COLLEGE') AS COLLEGE_EMAIL ON S.strStudentNumber = COLLEGE_EMAIL.strStudentNumber
LEFT JOIN
dbo.udfMostRecentEmail('PERSONAL') AS PERSONAL_EMAIL ON S.strStudentNumber = PERSONAL_EMAIL.strStudentNumber
INNER JOIN
dbo.udfProgressHistory(@strAcademicSessionStart) AS P ON S.strStudentNumber = P.strStudentNumber
WHERE (P.strProgressCode LIKE @strProgressCode OR (SUBSTRING(@strProgressCode, 1, 1) = '^' AND P.strProgressCode NOT LIKE SUBSTRING(@strProgressCode, 2, LEN(@strProgressCode)))) AND
(P.strStudentNumber NOT IN
(SELECT strStudentNumber
FROM dbo.tblPilgrims
WHERE (strAcademicSessionStart = @strAcademicSessionStart) AND (strScheme = 'BEI')))
ORDER BY P.[str(Sub)Plan], P.intYearOfCourse, S.strSurname
)
【问题讨论】:
是varchar(4) = 2019
还是int = 2019
?
您查看过这两个查询的执行计划了吗?除非您使用强制参数,否则 SQL 服务器会为一个查询缓存多个计划并不少见。这也可能是参数嗅探问题。请看以下链接:brentozar.com/sql/parameter-sniffing
值 2019 是一个 varchar,我想它应该用引号引起来,但 SQL 似乎很高兴,在慢速版本中将它引用为 '2019' 并没有任何区别
也可以分享一下功能吗?
你能把计划贴在这里brentozar.com/pastetheplan吗?并在这里分享?
【参考方案1】:
扩展@Ross Pressers 评论,这可能不是一个真正的答案,但根据我对正在发生的事情的理解(可能是错误的!)展示了正在发生的事情(有点)......
最后运行设置代码,然后......
使用 (Ctrl-M) 上的查询计划执行以下操作...(注意:取决于随机数生成器,您可能会或可能不会获得任何结果,这不会影响计划)
declare @one varchar(100) = '379', @two varchar(200) = '726'
select * from wibble(@one, @two) -- 1
select * from wibble('379', '726') -- 2
select * from wibble(@one, @two) OPTION (RECOMPILE) -- 3
select * from wibble(@one, @two) -- 4
警告。以下是 MY 系统上发生的情况,您的里程可能会有所不同...
-- 1(和--4)是最贵的。
SQL Server 创建一个通用计划,因为它不知道参数是什么(是的,它们已定义,但该计划是针对 wibble(@one, @two) 的,此时,参数值是“未知的” ) https://www.brentozar.com/pastetheplan/?id=rJtIRwx_r
-- 2 有不同的计划
这里sql server知道参数是什么,所以可以创建具体的plan,和--1有很大区别 https://www.brentozar.com/pastetheplan/?id=rJa9APldS
-- 3 与--2 有相同的计划
进一步测试,添加 OPTION (RECOMPILE) 让 SQL Server 为 wibble(@one, @two) 的特定执行创建特定计划,因此我们得到与 --2 相同的计划
--4 是为了完整地表明在所有关于通用计划的混乱仍然存在
因此,在这个简单的示例中,我们使用相同的值调用参数化 TVF,这些值作为参数或内联传递,根据 OP 生成不同的执行计划和不同的执行时间
设置
use tempdb
GO
drop table if EXISTS Orders
GO
create table Orders (
OrderID int primary key,
UserName varchar(50),
PhoneNumber1 varchar(50),
)
-- generate 300000 with randon "phone" numbers
;WITH TallyTable AS (
SELECT TOP 300000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS [N]
FROM dbo.syscolumns tb1,dbo.syscolumns tb2
)
insert into Orders
select n, 'user' + cast(n as varchar(10)), cast(CRYPT_GEN_RANDOM(3) as int)
FROM TallyTable;
GO
drop function if exists wibble
GO
create or alter function wibble (
@one varchar(4) = '%'
, @two varchar(4) = '%'
)
returns table
as
return select * from Orders
where PhoneNumber1 like '%' + @one + '%'
and PhoneNumber1 like '%' + @two + '%'
or (SUBSTRING(@one, 1, 1) = '^' AND PhoneNumber1 NOT LIKE SUBSTRING(@two, 2, LEN(@two)))
and (select 1) = 1
GO
【讨论】:
我认为这很好地说明了问题中突出显示的行为如何/为何发生【参考方案2】:通过跟进 Ross Presser 对 udfProgressHistory 复杂性的观察,问题得以解决(我不会说“已解决”)。这从连接到自身的表 tblProgressHistory 中吸取数据。该表每年添加一次。我认为今年额外的 2K 记录一定是在使用特定执行计划时导致成本突然增加的原因。我删除了超过 2K 的冗余记录,我们又回到了亚秒级执行。
【讨论】:
以上是关于SQL Server 执行速度因向内联表函数提供参数的方式而异的主要内容,如果未能解决你的问题,请参考以下文章
如何检索 sql server 内联表值函数的返回值的元数据?
如何在 SQL Server 中将拆分函数转换为内联表值 udf?