为啥 SQL Server 标量值函数变慢?
Posted
技术标签:
【中文标题】为啥 SQL Server 标量值函数变慢?【英文标题】:Why do SQL Server Scalar-valued functions get slower?为什么 SQL Server 标量值函数变慢? 【发布时间】:2010-10-22 10:48:15 【问题描述】:为什么标量值函数似乎会导致查询在连续使用的次数越多时累积地越慢?
我有这张表是用从第 3 方购买的数据构建的。
我已经删减了一些内容以使这篇文章更短......但只是为了让您了解事情的设置方式。
CREATE TABLE [dbo].[GIS_Location](
[ID] [int] IDENTITY(1,1) NOT NULL, --PK
[Lat] [int] NOT NULL,
[Lon] [int] NOT NULL,
[Postal_Code] [varchar](7) NOT NULL,
[State] [char](2) NOT NULL,
[City] [varchar](30) NOT NULL,
[Country] [char](3) NOT NULL,
CREATE TABLE [dbo].[Address_Location](
[ID] [int] IDENTITY(1,1) NOT NULL, --PK
[Address_Type_ID] [int] NULL,
[Location] [varchar](100) NOT NULL,
[State] [char](2) NOT NULL,
[City] [varchar](30) NOT NULL,
[Postal_Code] [varchar](10) NOT NULL,
[Postal_Extension] [varchar](10) NULL,
[Country_Code] [varchar](10) NULL,
然后我有两个查找 LAT 和 LON 的函数。
CREATE FUNCTION [dbo].[usf_GIS_GET_LAT]
(
@City VARCHAR(30),
@State CHAR(2)
)
RETURNS INT
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE @LAT INT
SET @LAT = (SELECT TOP 1 LAT FROM GIS_Location WITH(NOLOCK) WHERE [State] = @State AND [City] = @City)
RETURN @LAT
END
CREATE FUNCTION [dbo].[usf_GIS_GET_LON]
(
@City VARCHAR(30),
@State CHAR(2)
)
RETURNS INT
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE @LON INT
SET @LON = (SELECT TOP 1 LON FROM GIS_Location WITH(NOLOCK) WHERE [State] = @State AND [City] = @City)
RETURN @LON
END
当我运行以下...
SET STATISTICS TIME ON
SELECT
dbo.usf_GIS_GET_LAT(City,[State]) AS Lat,
dbo.usf_GIS_GET_LON(City,[State]) AS Lon
FROM
Address_Location WITH(NOLOCK)
WHERE
ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC)
SET STATISTICS TIME OFF
100 ~= 8 毫秒、200 ~= 32 毫秒、400 ~= 876 毫秒
--编辑 对不起,我应该更清楚。我不想调整上面列出的查询。这只是一个示例,显示执行时间越慢,它处理的记录越多。在现实世界的应用程序中,这些函数用作 where 子句的一部分,用于围绕城市和州构建一个半径,以包含该区域内的所有记录。
【问题讨论】:
不要把NOLOCK提示撒在SO中不需要的样本上,NOLOCK的东西真的和这个问题无关。 如果你不能摆脱“真正的查询”中的功能,那么它总是很慢。举一个更好的例子,在 WHERE 中使用函数,我们可以给你一些想法...... 【参考方案1】:很抱歉我迟到了,但我想为未来的 Profiler 受害者分享我的答案。几天前,一台生产服务器(sql server 2012 sp4 enterprise)中的所有标量函数都变慢了,一些通常需要几秒钟才能完成的存储过程,它们开始在几分钟内运行,在一种情况下是几小时。
最后,使用分析器创建的跟踪是造成这种情况的根本原因。跟踪已启动,但运行此跟踪的笔记本电脑已关闭,而之前没有停止跟踪。就像奇迹一样,用户 sa 自动停止了跟踪(为了记录,sa 帐户被禁用并重命名)--“SQL 跟踪已停止。跟踪 ID = '3'。登录名 = 'sa'。”这会自动解决性能问题。
所以,检查在慢速服务器上运行的分析器跟踪或扩展事件
希望这对将来的某些人有所帮助。
【讨论】:
【参考方案2】:通常标量函数比内联 TVF 函数慢得多。幸运的是,在很多情况下它都会改变。
SQL Server 2019 将引入Scalar UDF Inlining:
智能查询处理套件下的一项功能。 此功能提高了在 SQL Server 中调用标量 UDF 的查询的性能(从 SQL Server 2019 预览版开始)
T-SQL 标量用户定义函数
在 Transact-SQL 中实现并返回单个数据值的用户定义函数称为 T-SQL 标量用户定义函数。 T-SQL UDF 是实现跨 SQL 查询的代码重用和模块化的一种优雅方式。一些计算(例如复杂的业务规则)更容易以命令式 UDF 形式表达。 UDF 有助于构建复杂的逻辑,而不需要编写复杂 SQL 查询的专业知识。
由于以下原因,标量 UDF 通常最终表现不佳。
迭代调用 缺乏成本核算 解释执行 串行执行
标量 UDF 的自动内联
标量 UDF 内联功能的目标是提高调用 T-SQL 标量 UDF 的查询的性能,其中 UDF 执行是主要瓶颈。
使用此新功能,标量 UDF 会自动转换为标量表达式或标量子查询,在调用查询中替换 UDF 运算符。然后优化这些表达式和子查询。 因此,查询计划将不再具有用户定义的函数运算符,但其效果将在计划中观察到,如视图或内联 TVF。
内联标量 UDF 要求
如果满足以下所有条件,标量 T-SQL UDF 可以内联 是真的:
UDF 使用以下结构编写:
DECLARE、SET:变量声明和赋值。 SELECT:带有单个/多个变量分配的 SQL 查询1。 IF/ELSE:具有任意嵌套级别的分支。 RETURN:单个或多个返回语句。 UDF:嵌套/递归函数调用2。 其他:关系运算,例如 EXISTS、ISNULL。
UDF 不调用任何与时间相关(例如 GETDATE())或具有副作用3(例如 NEWSEQUENTIALID())。
UDF 使用 EXECUTE AS CALLER 子句(未指定 EXECUTE AS 子句时的默认行为)。 UDF 不引用表变量或表值参数。 调用标量 UDF 的查询未在其 GROUP BY 子句中引用标量 UDF 调用。 UDF 不是本机编译的(支持互操作)。 UDF 未在计算列或检查约束定义中使用。 UDF 不引用用户定义的类型。 没有向 UDF 添加签名。 UDF 不是分区函数。
检查函数是否可内联:
SELECT OBJECT_NAME([object_id]) AS name, is_inlineable
FROM sys.sql_modules
WHERE [object_id] = OBJECT_ID('schema.function_name')
在数据库级别启用/禁用功能:
ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = ON/OFF;
附录
Microsoft Research - Project Froid
【讨论】:
【参考方案3】:在大多数情况下,最好避免引用表的标量值函数,因为(正如其他人所说)它们基本上是每行需要运行一次的黑盒,并且无法通过查询计划引擎进行优化。因此,即使关联的表有索引,它们也倾向于线性扩展。
您可能需要考虑使用内联表值函数,因为它们是与查询内联计算的,并且可以进行优化。你得到你想要的封装,但是在选择语句中粘贴表达式的性能。
作为内联的副作用,它们不能包含任何程序代码(不声明@variable;设置@variable = ..;返回)。但是,它们可以返回多行和多列。
你可以像这样重写你的函数:
create function usf_GIS_GET_LAT(
@City varchar (30),
@State char (2)
)
returns table
as return (
select top 1 lat
from GIS_Location with (nolock)
where [State] = @State
and [City] = @City
);
GO
create function usf_GIS_GET_LON (
@City varchar (30),
@State char (2)
)
returns table
as return (
select top 1 LON
from GIS_Location with (nolock)
where [State] = @State
and [City] = @City
);
使用它们的语法也有些不同:
select
Lat.Lat,
Lon.Lon
from
Address_Location with (nolock)
cross apply dbo.usf_GIS_GET_LAT(City,[State]) AS Lat
cross apply dbo.usf_GIS_GET_LON(City,[State]) AS Lon
WHERE
ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC)
【讨论】:
虽然这是解决 OP 性能问题的一个很好的解决方案,但它并没有真正回答这个问题:“为什么标量函数会非线性退化?” (您甚至在回答中说:“它们倾向于线性扩展”)只是因为我看到与 OP 相同的行为并且非常好奇为什么它是非线性的。 @tbone,这个问题从来没有提到他们非线性退化。它们应该相对于返回的行数线性缩放,因为它们每行运行一次。请参阅 sam saffron 的答案以查看它们线性缩放的示例。 他发布的统计数据显示非线性:100 ~= 8 ms、200 ~= 32 ms、400 ~= 876 ms【参考方案4】:您可以将您的功能封装在一个内联 TVF 中,这样会更快:
http://sqlblog.com/blogs/alexander_kuznetsov/archive/2008/05/23/reuse-your-code-with-cross-apply.aspx
【讨论】:
【参考方案5】:他们没有。
标量函数中没有错误会导致其性能根据执行标量函数中的行数而呈指数级下降。再次尝试您的测试并查看 SQL 分析器,查看 CPU 和 READS 和 DURATION 列。增加你的测试规模,以包括超过一秒、两秒、五秒的测试。
CREATE FUNCTION dbo.slow
(
@ignore int
)
RETURNS INT
AS
BEGIN
DECLARE @slow INT
SET @slow = (select count(*) from sysobjects a
cross join sysobjects b
cross join sysobjects c
cross join sysobjects d
cross join sysobjects e
cross join sysobjects f
where a.id = @ignore)
RETURN @slow
END
go
SET STATISTICS TIME ON
select top 1 dbo.slow(id)
from sysobjects
go
select top 5 dbo.slow(id)
from sysobjects
go
select top 10 dbo.slow(id)
from sysobjects
go
select top 20 dbo.slow(id)
from sysobjects
go
select top 40 dbo.slow(id)
from sysobjects
SET STATISTICS TIME OFF
输出
SQL Server Execution Times:
CPU time = 203 ms, elapsed time = 202 ms.
SQL Server Execution Times:
CPU time = 889 ms, elapsed time = 939 ms.
SQL Server Execution Times:
CPU time = 1748 ms, elapsed time = 1855 ms.
SQL Server Execution Times:
CPU time = 3541 ms, elapsed time = 3696 ms.
SQL Server Execution Times:
CPU time = 7207 ms, elapsed time = 7392 ms.
请记住,如果您对结果集中的行运行标量函数,则标量函数将按行执行,而不会进行全局优化。
【讨论】:
【参考方案6】:看看这是否更好......或者也许是一个独特的内部连接?
select a.*,
(select top 1 g.Lat from GIS_Location g where g.City = a.City and g.State = a.State) as Lat,
(select top 1 g.Lon from GIS_Location g where g.City = a.City and g.State = a.State) as Lon
from Address_Location a
where a.ID in (select top 100 ID from Address_Location order by ID desc)
至于标量函数的表现,我不确定。
【讨论】:
【参考方案7】:简单地说,因为带有用户定义函数的 SQL 表达式的效率低于没有它们的 SQL 表达式。执行逻辑无法优化;并且每一行都必须产生函数开销(包括调用协议)。
KMike 的建议很好。 WHERE .. IN (SELECT something) 不太可能是一种有效的模式,在这种情况下可以很容易地用 JOIN 替换。
【讨论】:
【参考方案8】:您为结果集中的每一行调用该函数两次(对 DB 的两次选择命中)。
为了让您的查询更快地加入 GIS_Location 并跳过这些功能:
SELECT
g.Lat,
g.Lon
FROM
Address_Location l WITH(NOLOCK)
INNER JOIN GIS_Location g WITH(NOLOCK) WHERE l.State = g.State AND l.City = g.City
WHERE
ID IN (SELECT TOP 100 ID FROM Address_Location WITH(NOLOCK) ORDER BY ID DESC)
我不知道为什么我只是从问题中复制了 NOLOCK 或疯狂的 where 子句...
【讨论】:
数据几乎没有变化,因此 nolock 表提示减少了执行时间,因为它不必发出共享锁。疯狂的 where 子句只是对 x 条记录进行采样,因此我可以显示它越慢越慢,它处理的记录越多。这只是一个示例,而不是真实世界的应用程序。在真实的表中,我没有加入另一张表的奢侈,因为我正在处理的是一个标志范围的非规范化遗留表。 @DBAndrew,如果你不能摆脱“真实查询”中的功能,那么它总是很慢。举一个更好的例子,在 WHERE 中使用函数,我们可以给你一些想法......以上是关于为啥 SQL Server 标量值函数变慢?的主要内容,如果未能解决你的问题,请参考以下文章
在 SQL Server 中计算 GS1 校验位的标量值函数