为什么在表值函数内调用标量函数比在 TVF 外调用要慢?

jmo*_*eno 4 performance sql-server sql-server-2008-r2 functions

我正在编写一个表值函数,调用该函数所需的时间是直接运行代码的 10 倍。我将此追溯到对 TVF 内的多行标量函数的调用。从 TVF 内部调用时,对标量函数的调用过慢。标量函数接受 3 个 int 参数并返回单个 int 结果。

什么会导致它在 TVF 中变慢?

首先,TVF 基本上是一种数据透视表,只返回一行,有 13 列。

标量函数是一个多行标量,它在两个表中的一个中查找一组匹配的键(即输入是 columnA,输出是 columnB,其中 input = columnA 和 active)。使用稍微更改的 where 子句再次搜索表 1,然后是表 2,最后是表 1。

用法最初是一个带有连接子句的 cte 查询:

  With cte as ( select count(*) over (partition by c.c1) recs,
                       c.setId, c.c1, c.c2, n.name
                From tbl c
                     Inner join tbl n on n.id = schma.scal(c.c1, c.c2, c.c3)
                Where c.setId=@setId and c.endDt is null
   )
Run Code Online (Sandbox Code Playgroud)

这个 cte 最初被调用了 11 次,从(大约一半的时间)返回到 3 行(使用像 c1=42 这样的 where 子句)。所以,我想,嘿,它被调用了很多,因为总共只有 4-15 行,而基表是 ~1,000,000,我将通过将值放入表中将其减少到只有 15 次调用变量,然后进行更新。这实际上并没有更快,但它确实让我证明是对标量函数的调用导致速度变慢。

那看起来像:

   Insert into @cte(recs, setId, c1, c2)
    select count(*) over (partition by c.c1) recs, c.setId, c.c1, c.c2
    From tbl c
    Where c.setId=@setId and c.endDt is null;

    Update c
     Set nId =schma.scal(c.c1, c.c2, c.c3)
    From @cte c
Run Code Online (Sandbox Code Playgroud)

正如我所说,@cte 有 4 到 15 行(我最常用的 setId 中有 11 行)。在此更新之后,然后在此更新之前先返回,这就是我得出结论,这是导致问题的标量函数的方式。基本上是几十毫秒,一秒多,再过几毫秒就完成了。

来自 SSMS 的查询计划没有用,我正在考虑是否可以使用 DMV 中的计划获得更多详细信息。

知道 TVF 可用于替换标量,并且这可以提高性能,我试了一下,它奏效了(在原始 TVF 中调用时,整个查询的时间在 ~100 毫秒范围内)。

我仍然在挠头,为什么从 TVF 内部呼叫比从外部呼叫需要更长的时间。

在 TVF 外部,我使用标量获得的时间与在原始 TFV 中使用新 TFV 的时间相当。

Han*_*non 5

当作为查询的一部分调用时,标量函数被称为每行一次

考虑以下示例。

  1. 为我们的测试创建一个新的空白数据库:

    USE master;
    IF EXISTS (SELECT 1 FROM sys.databases d WHERE d.name = 'mv')
    BEGIN
        ALTER DATABASE mv SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
        DROP DATABASE mv;
    END
    GO
    CREATE DATABASE mv;
    GO
    
    Run Code Online (Sandbox Code Playgroud)
  2. 创建一个表、一个多语句函数和一个表值函数:

    USE mv;
    GO
    CREATE TABLE dbo.t
    (
        t_id int NOT NULL
            CONSTRAINT PK_t
            PRIMARY KEY CLUSTERED
    );
    GO
    
    CREATE FUNCTION dbo.t_func
    (
        @t_id int
    )
    RETURNS bit
    WITH SCHEMABINDING
    AS
    BEGIN
        DECLARE @r bit;
        IF EXISTS (SELECT 1 FROM dbo.t WHERE t.t_id = @t_id)
            SET @r = 1
        ELSE
            SET @r = 0;
        RETURN @r;
    END
    GO
    
    CREATE FUNCTION dbo.t_tvf
    (
        @min_t_id int
        , @max_t_id int
    )
    RETURNS TABLE 
    WITH SCHEMABINDING
    AS
    RETURN (
        SELECT t_id = t.t_id
            , e = dbo.t_func(dbo.t.t_id)
        FROM dbo.t
        WHERE t.t_id >= @min_t_id
            AND t.t_id <= @max_t_id
    );
    GO
    
    Run Code Online (Sandbox Code Playgroud)
  3. 在表中插入一些示例数据:

    INSERT INTO dbo.t (t_id)
    SELECT ROW_NUMBER() OVER (ORDER BY c.id, c.colid)
    FROM sys.syscolumns c;
    GO
    
    Run Code Online (Sandbox Code Playgroud)
  4. 创建一个表来存储函数执行统计信息,并用一个显示多语句函数的执行计数的起始行填充它,t_func

    CREATE TABLE dbo.function_stats
    (
        run_num int NOT NULL
        , object_name sysname NOT NULL
        , execution_count int NULL 
        , CONSTRAINT PK_function_stats
            PRIMARY KEY CLUSTERED (run_num, object_name)
    );
    GO
    INSERT INTO dbo.function_stats (run_num, object_name, execution_count)
    SELECT 1
        , o.name
        , COALESCE(fs.execution_count, 0)
    FROM sys.objects o 
        LEFT JOIN sys.dm_exec_function_stats fs ON fs.object_id = o.object_id
    WHERE o.name = 't_func';
    GO
    
    Run Code Online (Sandbox Code Playgroud)
  5. 对 TVF 运行查询:

    SELECT t.*
    FROM dbo.t_tvf(1, 2) t;
    GO
    
    Run Code Online (Sandbox Code Playgroud)
  6. 现在捕获执行统计信息:

    INSERT INTO dbo.function_stats (run_num, object_name, execution_count)
    SELECT 2
        , o.name
        , COALESCE(fs.execution_count, 0)
    FROM sys.objects o 
        LEFT JOIN sys.dm_exec_function_stats fs ON fs.object_id = o.object_id
    WHERE o.name = 't_func';
    
    Run Code Online (Sandbox Code Playgroud)
  7. 函数统计结果:

    SELECT *
    FROM dbo.function_stats fs
    ORDER BY fs.run_num
        , fs.object_name;
    
    Run Code Online (Sandbox Code Playgroud)
????????????????????????????????????????????????
? 运行数?对象名称?执行计数?
????????????????????????????????????????????????
? 1 ? t_func ? 0 ?
? 2 ? t_func ? 2 ?
????????????????????????????????????????????????

如您所见,多语句函数执行了两次,对于 TVF 访问的源表,每行一次。

我预计辑阵语句功能正在被TVF叫很多很多次,给人的印象是它运行缓慢,而实际上它只是被称为很多次。