为什么SQL Server标量值函数会变慢?

时间:2009-04-28 22:01:43

标签: sql sql-server tsql sql-server-2005 sql-function

为什么标量值函数似乎会导致查询在连续使用次数的情况下累积运行得更慢?

我的这张表是使用从第三方购买的数据构建的。

我已经删除了一些东西以缩短这篇文章的内容......但只是让你了解事情是如何设置的。

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 ms,200~ = 32 ms,400~ = 876 ms

- 编辑 对不起我应该更清楚了。我不打算调整上面列出的查询。这只是一个示例,显示执行时间越慢,它所经历的记录就越多。在现实世界的应用程序中,这些函数被用作where子句的一部分,用于在城市和州周围建立半径以包括该区域中的所有记录。

8 个答案:

答案 0 :(得分:26)

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

您可能需要考虑使用内联表值函数,因为它们是与查询内联求值的,并且可以进行优化。您可以获得所需的封装,但是可以在select语句中粘贴表达式。

作为内联的副作用,它们不能包含任何过程代码(没有声明@variable;设置@variable = ..; return)。但是,它们可以返回多行和多列。

您可以重写这样的函数:

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)

答案 1 :(得分:6)

他们没有。

标量函数中没有任何错误导致其性能以指数方式降级,具体取决于执行标量函数中的行数。再次尝试测试并查看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.

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

答案 2 :(得分:3)

答案 3 :(得分:2)

对结果集中的每一行调用该函数两次(对数据库选择两次)。

使您的查询更快地加入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子句,我只是从问题中复制过来......

答案 4 :(得分:0)

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

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

答案 5 :(得分: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)

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

答案 6 :(得分:0)

通常,标量函数比内联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:

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

答案 7 :(得分:0)

对不起,我参加这个聚会很晚,但是我想为未来的Profiler Victims分享我的答案。几天前,一个生产服务器(SQL Server 2012 sp4 Enterprise)中的所有标量函数变慢了,某些存储过程通常需要几秒钟才能完成,在某些情况下,它们开始运行需要几分钟,几小时。

最后,使用探查器创建的跟踪是造成此问题的根本原因。跟踪已启动,但随后在运行该跟踪的便携式计算机上已关闭,而之前没有停止跟踪。就像奇迹一样,跟踪由用户sa自动停止(对于记录,sa帐户已被禁用并重命名)-“ SQL跟踪已停止。跟踪ID ='3'。登录名='sa'。”这会自动解决性能问题。

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

希望这对以后的工作有所帮助。