多语句 TVF 与内联 TVF 性能

Han*_*non 18 sql-server sql-server-2012 set-returning-functions

比较关于回文问题的一些答案(仅限 10k+ 用户,因为我删除了答案),我得到了令人困惑的结果。

我提出了一个多语句、模式绑定的 TVF,我认为它比运行标准函数要快。我还认为多语句 TVF 会被“内联”,尽管我在这方面是错误的,如下所示。这个问题是关于这两种TVF风格的性能差异。首先,您需要查看代码。

这是多语句 TVF:

IF OBJECT_ID('dbo.IsPalindrome') IS NOT NULL
DROP FUNCTION dbo.IsPalindrome;
GO
CREATE FUNCTION dbo.IsPalindrome
(
    @Word NVARCHAR(500)
) 
RETURNS @t TABLE
(
    IsPalindrome BIT NOT NULL
)
WITH SCHEMABINDING
AS
BEGIN
    DECLARE @IsPalindrome BIT;
    DECLARE @LeftChunk NVARCHAR(250);
    DECLARE @RightChunk NVARCHAR(250);
    DECLARE @StrLen INT;
    DECLARE @Pos INT;
    SET @RightChunk = '';
    SET @IsPalindrome = 0;
    SET @StrLen = LEN(@Word) / 2;
    IF @StrLen % 2 = 1 SET @StrLen = @StrLen - 1;
    SET @Pos = LEN(@Word);
    SET @LeftChunk = LEFT(@Word, @StrLen);
    WHILE @Pos > (LEN(@Word) - @StrLen)
    BEGIN
        SET @RightChunk = @RightChunk + SUBSTRING(@Word, @Pos, 1)
        SET @Pos = @Pos - 1;
    END
    IF @LeftChunk = @RightChunk SET @IsPalindrome = 1;
    INSERT INTO @t VALUES (@IsPalindrome);
    RETURN
END
GO
Run Code Online (Sandbox Code Playgroud)

内联TVF:

IF OBJECT_ID('dbo.InlineIsPalindrome') IS NOT NULL
DROP FUNCTION dbo.InlineIsPalindrome;
GO
CREATE FUNCTION dbo.InlineIsPalindrome
(
    @Word NVARCHAR(500)
)
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN (
    WITH Nums AS
    (
      SELECT
        N = number
      FROM
        dbo.Numbers
    )
    SELECT
      IsPalindrome =
        CASE
          WHEN EXISTS
          (
            SELECT N
            FROM Nums
            WHERE N <= L / 2
              AND SUBSTRING(S, N, 1) <> SUBSTRING(S, 1 + L - N, 1)
          )
          THEN 0
          ELSE 1
        END
    FROM
      (SELECT LTRIM(RTRIM(@Word)), LEN(@Word)) AS v (S, L)
);
GO
Run Code Online (Sandbox Code Playgroud)

Numbers上述函数中的表定义为:

CREATE TABLE dbo.Numbers
(
    Number INT NOT NULL 
);
Run Code Online (Sandbox Code Playgroud)

注意: numbers 表没有任何索引,也没有主键,包含 1,000,000 行。

一个试验台临时表:

IF OBJECT_ID('tempdb.dbo.#Words') IS NOT NULL
DROP TABLE #Words;
GO
CREATE TABLE #Words 
(
    Word VARCHAR(500) NOT NULL
);

INSERT INTO #Words(Word) 
SELECT o.name + REVERSE(w.name)
FROM sys.objects o
CROSS APPLY (
    SELECT o.name
    FROM sys.objects o
) w;
Run Code Online (Sandbox Code Playgroud)

在我的测试系统上,上述INSERT结果导致#Words表中插入了 16,900 行。

为了测试这两种变体,我SET STATISTICS IO, TIME ON;并使用以下内容:

SELECT w.Word
    , p.IsPalindrome
FROM #Words w
    CROSS APPLY dbo.IsPalindrome(w.Word) p
ORDER BY w.Word;


SELECT w.Word
    , p.IsPalindrome
FROM #Words w
    CROSS APPLY dbo.InlineIsPalindrome(w.Word) p
ORDER BY w.Word;
Run Code Online (Sandbox Code Playgroud)

我预计该InlineIsPalindrome版本会明显更快,但是以下结果不支持该假设。

多语句 TVF:

表'#A1CE04C3'。扫描计数 16896,逻辑读取 16900,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表 'Worktable'。扫描计数 0,逻辑读取 0,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表 '#Words'。扫描计数 1,逻辑读取 88,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。

SQL Server 执行时间:
CPU 时间 = 1700 毫秒,已用时间 = 2022 毫秒。
SQL Server 解析和编译时间:
CPU 时间 = 0 毫秒,已用时间 = 0 毫秒。

内联 TVF:

表“数字”。扫描计数 1,逻辑读取 1272030,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表 'Worktable'。扫描计数 0,逻辑读取 0,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表 '#Words'。扫描计数 1,逻辑读取 88,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。

SQL Server 执行时间:
CPU 时间 = 137874 毫秒,已用时间 = 139415 毫秒。
SQL Server 解析和编译时间:
CPU 时间 = 0 毫秒,已用时间 = 0 毫秒。

执行计划如下所示:

在此处输入图片说明

在此处输入图片说明

在这种情况下,为什么内联变体比多语句变体慢得多?

为了回应@AaronBertrand 的评论,我修改了dbo.InlineIsPalindrome函数以限制 CTE 返回的行以匹配输入单词的长度:

CREATE FUNCTION dbo.InlineIsPalindrome
(
    @Word NVARCHAR(500)
)
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN (
    WITH Nums AS
    (
      SELECT
        N = number
      FROM
        dbo.Numbers
      WHERE 
        number <= LEN(@Word)
    )
    SELECT
      IsPalindrome =
        CASE
          WHEN EXISTS
          (
            SELECT N
            FROM Nums
            WHERE N <= L / 2
              AND SUBSTRING(S, N, 1) <> SUBSTRING(S, 1 + L - N, 1)
          )
          THEN 0
          ELSE 1
        END
    FROM
      (SELECT LTRIM(RTRIM(@Word)), LEN(@Word)) AS v (S, L)
);
Run Code Online (Sandbox Code Playgroud)

正如@MartinSmith 所建议的那样,我在表中添加了一个主键和聚集索引dbo.Numbers,这当然有帮助,并且更接近人们期望在生产环境中看到的内容。

重新运行上面的测试现在会得到以下统计信息:

CROSS APPLY dbo.IsPalindrome(w.Word) p

(17424 行受影响)
表“#B1104853”。扫描计数 17420,逻辑读取 17424,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表 'Worktable'。扫描计数 0,逻辑读取 0,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表 '#Words'。扫描计数 1,逻辑读 90,物理读 0,预读 0,lob 逻辑读 0,lob 物理读 0,lob 预读 0。

SQL Server 执行时间:
CPU 时间 = 1763 毫秒,已用时间 = 2192 毫秒。

dbo.FunctionIsPalindrome(w.Word)

(受影响的 17424 行)
表“工作表”。扫描计数 0,逻辑读取 0,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表 '#Words'。扫描计数 1,逻辑读 90,物理读 0,预读 0,lob 逻辑读 0,lob 物理读 0,lob 预读 0。

SQL Server 执行时间:
CPU 时间 = 328 毫秒,已用时间 = 424 毫秒。

CROSS APPLY dbo.InlineIsPalindrome(w.Word) p

(受影响的 17424 行)
表“数字”。扫描计数 1,逻辑读取 237100,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表 'Worktable'。扫描计数 0,逻辑读取 0,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。
表 '#Words'。扫描计数 1,逻辑读 90,物理读 0,预读 0,lob 逻辑读 0,lob 物理读 0,lob 预读 0。

SQL Server 执行时间:
CPU 时间 = 17737 毫秒,已用时间 = 17946 毫秒。

我正在 SQL Server 2012 SP3, v11.0.6020, Developer Edition 上对此进行测试。

这是我的数字表的定义,带有主键和聚集索引:

CREATE TABLE dbo.Numbers
(
    Number INT NOT NULL 
        CONSTRAINT PK_Numbers
        PRIMARY KEY CLUSTERED
);

;WITH n AS
(
    SELECT v.n 
    FROM (
        VALUES (1) 
            ,(2) 
            ,(3) 
            ,(4) 
            ,(5) 
            ,(6) 
            ,(7) 
            ,(8) 
            ,(9) 
            ,(10)
        ) v(n)
)
INSERT INTO dbo.Numbers(Number)
SELECT ROW_NUMBER() OVER (ORDER BY n1.n)
FROM n n1
    , n n2
    , n n3
    , n n4
    , n n5
    , n n6;
Run Code Online (Sandbox Code Playgroud)

Mar*_*ith 12

您的数字表是一个堆,每次都可能被完全扫描。

添加集群主键Number并尝试以下forceseek提示以获得所需的搜索。

据我所知,这个提示是需要的,因为 SQL Server 只是估计表的 27% 将匹配谓词(30% 为 the <=,减少到 27% <>)。因此,它只需要读取 3-4 行就可以找到匹配的行,并且可以退出半连接。因此,扫描选项的成本非常低廉。但事实上,如果任何回文确实存在,那么它必须读取整个表格,所以这不是一个好的计划。

CREATE FUNCTION dbo.InlineIsPalindrome
(
    @Word NVARCHAR(500)
)
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN (
    WITH Nums AS
    (
      SELECT
        N = number
      FROM
        dbo.Numbers WITH(FORCESEEK)
    )
    SELECT
      IsPalindrome =
        CASE
          WHEN EXISTS
          (
            SELECT N
            FROM Nums
            WHERE N <= L / 2
              AND SUBSTRING(S, N, 1) <> SUBSTRING(S, 1 + L - N, 1)
          )
          THEN 0
          ELSE 1
        END
    FROM
      (SELECT LTRIM(RTRIM(@Word)), LEN(@Word)) AS v (S, L)
);
GO
Run Code Online (Sandbox Code Playgroud)

有了这些变化,它为我飞行(需要 228 毫秒)

在此处输入图片说明