17

为什么标量值函数似乎会导致查询在连续使用的次数越多时累积地运行得越慢?

我有这张表是用从第 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 子句的一部分,以在城市和州周围建立一个半径,以包括该地区的所有记录。

4

8 回答 8

31

在大多数情况下,最好避免引用表的标量值函数,因为(正如其他人所说)它们基本上是每行都需要运行一次的黑盒,并且无法通过查询计划引擎进行优化。因此,即使关联的表有索引,它们也倾向于线性扩展。

您可能需要考虑使用内联表值函数,因为它们是与查询内联评估的,并且可以进行优化。你得到了你想要的封装,但是在选择语句中粘贴表达式的性能。

作为内联的副作用,它们不能包含任何程序代码(没有声明@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)
于 2009-05-01T00:53:29.920 回答
8

他们不。

标量函数中没有错误会导致其性能根据执行标量函数中的行数而呈指数级下降。再次尝试您的测试并查看 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.

请记住,如果您对结果集中的行运行标量函数,则标量函数将按行执行,而不进行全局优化。

于 2009-04-29T02:00:38.967 回答
3

您可以将您的功能包装在一个内联 TVF 中,这会更快:

http://sqlblog.com/blogs/alexander_kuznetsov/archive/2008/05/23/reuse-your-code-with-cross-apply.aspx

于 2009-05-19T15:26:31.407 回答
2

您为结果集中的每一行调用该函数两次(对 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 子句...

于 2009-04-28T22:11:28.987 回答
0

简单地说,因为带有用户定义函数的 SQL 表达式比没有它们的 SQL 表达式效率低。执行逻辑无法优化;并且必须为每一行产生函数开销(包括调用协议)。

KMike 的建议很好。WHERE .. IN (SELECT something) 不太可能是一种有效的模式,在这种情况下可以很容易地用 JOIN 代替。

于 2009-04-28T22:18:56.283 回答
0

看看这是否效果更好......或者也许是一个独特的内部连接?

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)

至于标量函数的性能,我不确定。

于 2009-04-28T22:25:30.887 回答
0

通常标量函数比内联 TVF 函数慢得多。幸运的是,在许多情况下它会改变。

SQL Server 2019 将引入 标量 UDF 内联

智能查询处理套件功能下的一个功能。此功能提高了在 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 使用以下结构编写:

    1. DECLARE, SET:变量声明和赋值。
    2. SELECT:带有单/多变量赋值的 SQL 查询1。
    3. IF/ELSE:具有任意嵌套级别的分支。
    4. RETURN:单个或多个返回语句。
    5. UDF:嵌套/递归函数调用2。
    6. 其他:关系运算,如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;

附录

微软研究院——Froid 项目

于 2018-11-07T15:36:08.317 回答
0

很抱歉我迟到了这个聚会,但我想为未来的 Profiler 受害者分享我的答案。几天前,一台生产服务器(sql server 2012 sp4 enterprise)中的所有标量函数变慢了,一些通常需要几秒钟才能完成的存储过程,它们开始在几分钟内运行,在一种情况下是几小时。

最后,使用分析器创建的跟踪是造成这种情况的根本原因。跟踪已启动,但运行此跟踪的笔记本电脑已关闭,而之前没有停止跟踪。就像奇迹一样,用户 sa 自动停止了跟踪(为了记录,sa 帐户被禁用并重命名)--“SQL 跟踪已停止。跟踪 ID = '3'。登录名 = 'sa'。” 这会自动解决性能问题。

因此,检查在慢速服务器上运行的分析器跟踪或扩展事件

希望这对将来的某个人有所帮助。

于 2019-08-28T20:37:04.263 回答