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

为什么SQL Server标量值函数速度变慢?,sql,sql-server,tsql,sql-server-2005,sql-function,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]

为什么标量值函数的使用次数越多,查询的运行速度就会越慢

我有这个表是用从第三方购买的数据构建的

我删掉了一些东西,把这篇文章缩短了。。。只是为了让你知道事情是如何安排的

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

--编辑
对不起,我应该说得更清楚些。我不想调整上面列出的查询。这只是一个示例,显示执行时间随着处理的记录越多而变慢。在实际应用程序中,这些函数作为where子句的一部分,用于围绕城市和州构建半径,以包括该区域中的所有记录。

对于结果集中的每一行,调用函数两次(对DB进行两次选择)

要使查询更快,请右键连接到GIS_位置并跳过以下功能:

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子句,我只是从问题中复制…

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


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

看看这是否更有效。。。或者是一个明显的内部连接

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)

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

他们不确定。

标量函数中没有导致性能指数下降的bug,这取决于执行标量函数的行数。请重试测试,并查看SQL profiler,查看CPU、读取和持续时间列。增加测试大小,以包括耗时超过一秒、两秒、五秒的测试

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.

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

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

您可能需要考虑使用内联表值函数,因为它们与查询一起被内定,并且可以被优化。您可以得到所需的封装,但需要在select语句中正确粘贴表达式

作为内联的副作用,它们不能包含任何过程代码(no declare@variable;set@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)

您可以将您的功能封装在一个内嵌TVF中,这将更快:


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

SQL Server 2019将推出

智能查询处理功能套件下的功能此功能提高了在SQL Server中调用标量UDF的查询的性能(从SQL Server 2019预览版开始)

T-SQL标量用户定义函数 在Transact-SQL中实现并返回单个数据值的用户定义函数称为T-SQL标量用户定义函数T-SQLUDF是跨SQL查询实现代码重用和模块化的一种优雅方式。某些计算(如复杂的业务规则)更容易以命令式UDF形式表示。UDF有助于构建复杂的逻辑,而不需要编写复杂SQL查询的专业知识。

由于以下原因,标量UDF的性能通常会很差

  • 迭代调用
  • 缺乏成本核算
  • 解释执行
  • 串行执行

标量udf的自动内联 标量UDF内联特性的目标是提高调用T-SQL标量UDF的查询的性能,其中UDF执行是主要瓶颈

使用此新功能,标量UDF将自动转换为标量表达式或标量子查询,并在调用查询中替代UDF运算符。然后对这些表达式和子查询进行优化因此,查询计划将不再具有用户定义的函数运算符,但其效果将在计划中观察,如视图或内联TVF。


可内联标量UDF需求 如果满足以下所有条件,则标量T-SQL UDF可以内联 是真的:

  • UDF是使用以下构造编写的:

  • 声明、设置:变量声明和赋值
  • 选择:单变量/多变量赋值的SQL查询1
  • IF/ELSE:具有任意嵌套级别的分支
  • RETURN:单个或多个RETURN语句
  • UDF:嵌套/递归函数调用2
  • 其他:关系操作,如EXISTS、ISNULL
  • UDF不会调用任何与时间相关(如GETDATE())或具有副作用3(如 NEWSEQUENTIALID()

  • UDF使用EXECUTE AS CALLER子句(如果未指定EXECUTE AS子句,则为默认行为)
  • UDF不引用表变量或表值p
    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;