对于大字符串,“+”比“CONCAT”慢吗?

got*_*tqn 23 sql-server t-sql concat standard-edition sql-server-2022

我一直认为CONCAT函数实际上是+(字符串连接)的包装,并带有一些额外的检查,以使我们的生活更轻松。

我还没有找到任何关于这些功能是如何实现的内部细节。至于性能,当数据在循环中连接时,调用似乎会产生开销CONCAT(这似乎很正常,因为有额外的 NULL 句柄)。

几天前,一位开发人员修改了一些字符串连接代码(从+到 ,CONCAT)因为不喜欢语法并告诉我它变得更快。

为了检查情况,我使用了以下代码:

DECLARE @V1 NVARCHAR(MAX)
       ,@V2 NVARCHAR(MAX)
       ,@V3 NVARCHAR(MAX);

DECLARE @R NVARCHAR(MAX);

SELECT  @V1 = REPLICATE(CAST('V1' AS NVARCHAR(MAX)), 50000000)
       ,@V2 = REPLICATE(CAST('V2' AS NVARCHAR(MAX)), 50000000)
       ,@V3 = REPLICATE(CAST('V3' AS NVARCHAR(MAX)), 50000000);
Run Code Online (Sandbox Code Playgroud)

这是变体一:

SELECT @R = CAST('' AS NVARCHAR(MAX)) + '{some small text}' + ISNULL(@V1, '{}') + ISNULL(@V2, '{}') + ISNULL(@V3, '{}'); 
SELECT LEN(@R); -- 1200000017
Run Code Online (Sandbox Code Playgroud)

这是变体二:

SELECT @R = CONCAT('{some small text}',ISNULL(@V1, '{}'), ISNULL(@V2, '{}'), ISNULL(@V3, '{}'))
SELECT LEN(@R); -- 1200000017
Run Code Online (Sandbox Code Playgroud)

对于较小的字符串,没有差异。在某些时候,CONCAT变体会变得更快:

在此输入图像描述

我想知道是否有人可以分享任何内部结构或解释其行为,因为似乎可能存在一条规则,最好使用CONCAT.

版本:

Microsoft SQL Server 2022 (RTM-CU8) (KB5029666) - 16.0.4075.1 (X64) 2023 年 8 月 23 日 14:04:50 版权所有 (C) 2022 Windows Server 2019 Standard 10.0(内部版本 17763)上的 Microsoft Corporation 标准版(64 位) :)(管理程序)


确切的脚本如下所示:

DECLARE @V1 NVARCHAR(MAX)
       ,@V2 NVARCHAR(MAX)
       ,@V3 NVARCHAR(MAX);

DECLARE @R NVARCHAR(MAX);

SELECT  @V1 = REPLICATE(CAST('V1' AS NVARCHAR(MAX)), 50000000)
       ,@V2 = REPLICATE(CAST('V2' AS NVARCHAR(MAX)), 50000000)
       ,@V3 = REPLICATE(CAST('V3' AS NVARCHAR(MAX)), 50000000);



--SELECT @R = CAST('' AS NVARCHAR(MAX)) + '{some small text}' + ISNULL(@V1, '{}') + ISNULL(@V2, '{}') + ISNULL(@V3, '{}'); -- 00:00:45 -- 00:01:22 -- 00:01:20
--SELECT LEN(@R); -- 300000017

SELECT @R = CONCAT('{some small text}',ISNULL(@V1, '{}'), ISNULL(@V2, '{}'), ISNULL(@V3, '{}')) -- 00:00:11 -- 00:00:16 -- 00:00:10
SELECT LEN(@R); -- 300000017
Run Code Online (Sandbox Code Playgroud)

我正在更改 REPLICATE 函数的最后一个参数,以便为连接生成更大的字符串。然后,我将每个变体执行三次。

Mar*_*ith 28

对于大字符串,“+”比“CONCAT”慢吗?

看起来是这样。从查看tempdb为存储此 LOB 数据而分配和取消分配的页面来看,每个实例+最终都会创建一个新字符串并为其分配和取消分配页面tempdb(其中只需要最后一个)。

CONCAT仅使用最终结果大小所需的页面,并且不会因为+通过为(并写入)最终不需要的中间字符串分配页面而添加更多页面而降低性能。

方法 新分配的页面 新释放的页面 新使用的页面
+ 249280 249280 0
康卡特 99696 99696 0

@V4对于我下面的代码 - 其中包括与问题相比的额外串联 - 最终结果的数据长度是 800,000,034 字节 (762.94 MiB)。该CONCAT方法分配了 99,696 页。平均每页 8024.4 字节。SQL Server 使用 8KiB 页面,但需要一些空间用于页头(和其他)开销。

因为+它分配了 249,280 个页面(~1.9 GiB),您可以通过尝试下面的中间表达式来了解它是如何构建到这个数字的(每个步骤都使用合理数量的附加页面来确定其结果的数据长度,但这也是如此)前面步骤的页面使用情况不是代替),因此分配的“额外”页面的比例随着+添加更多页面而增加。

表达 数据长度 分配的页数 数据长度/已分配页数 差异 数据长度/差异
CAST('' AS NVARCHAR(MAX)) + '{一些小文本}' 34 0 0
CAST('' AS NVARCHAR(MAX)) + '{一些小文本}' + ISNULL(@V1, '{}') 200000034 24920 8025.683547 24920 8025.683547
CAST('' AS NVARCHAR(MAX)) + '{一些小文本}' + ISNULL(@V1, '{}') + ISNULL(@V2, '{}') 400000034 74768 5349.882757 49848 8024.39484
CAST('' AS NVARCHAR(MAX)) + '{一些小文本}' + ISNULL(@V1, '{}') + ISNULL(@V2, '{}') + ISNULL(@V3, '{}' ) 600000034 149552 4011.982682 74784 8023.107001
CAST('' AS NVARCHAR(MAX)) + '{一些小文本}' + ISNULL(@V1, '{}') + ISNULL(@V2, '{}') + ISNULL(@V3, '{}' ) + ISNULL(@V4, '{}') 800000034 249280 3209.242755 99728 8021.81969

正如大卫·布朗 - 微软在评论中所说......

这是有意义的,因为+它是一个二元运算符,并且CONCAT是一个 N-ary函数。因此,无需特殊优化a+b+c即可评估为(a+b)+c,需要两个新字符串。虽然CONCAT(A,B,C)可以更轻松地构建一个新字符串

这种区别也体现在截断行为中。

对于下面第一种情况,连接的前两个字符串是非最大数据类型,结果被截断为8000字节。然后将其连接到一个max类型,得到的字符串是max,最终连接成功且没有截断。

CONCAT可以看到它有一个MAX类型作为参数传入,并避免了这种初始截断。

DECLARE @X VARCHAR(MAX) = ''

--14000
SELECT  DATALENGTH(REPLICATE('A', 6000) + REPLICATE('B', 6000) + @X + REPLICATE('C', 6000)) 

--18000
SELECT  DATALENGTH(CONCAT(REPLICATE('A', 6000), REPLICATE('A', 6000) , @X, REPLICATE('C', 6000)))
Run Code Online (Sandbox Code Playgroud)

用于查看页面分配的脚本

在下面的脚本中,我没有分配给@R变量,因为这本身需要分配页面,并且我只关注串联方法使用的页面。

DECLARE @TestConcat BIT = 0 /*Set to 0 for "+" and 1 for "Concat"*/
       ,@internal_objects_alloc_page_count1 BIGINT
       ,@internal_objects_alloc_page_count2 BIGINT
       ,@internal_objects_dealloc_page_count1 BIGINT
       ,@internal_objects_dealloc_page_count2 BIGINT
       ,@V1 NVARCHAR(MAX) = REPLICATE(CAST('V1' AS NVARCHAR(MAX)), 50000000)
       ,@V2 NVARCHAR(MAX) = REPLICATE(CAST('V2' AS NVARCHAR(MAX)), 50000000)
       ,@V3 NVARCHAR(MAX) = REPLICATE(CAST('V3' AS NVARCHAR(MAX)), 50000000)
       ,@V4 NVARCHAR(MAX) = REPLICATE(CAST('V4' AS NVARCHAR(MAX)), 50000000);

--Initial state after above variables assigned and before concatenation
SELECT @internal_objects_alloc_page_count1 = internal_objects_alloc_page_count, 
       @internal_objects_dealloc_page_count1 = internal_objects_dealloc_page_count
from sys.dm_db_task_space_usage
WHERE session_id = @@spid;

IF @TestConcat = 1
    SELECT DATALENGTH(CONCAT('{some small text}',ISNULL(@V1, '{}'), ISNULL(@V2, '{}'), ISNULL(@V3, '{}'), ISNULL(@V4, '{}')));
ELSE
    SELECT DATALENGTH(CAST('' AS NVARCHAR(MAX)) + '{some small text}' + ISNULL(@V1, '{}') + ISNULL(@V2, '{}') + ISNULL(@V3, '{}') + ISNULL(@V4, '{}'));

--State after concatenation completed
SELECT @internal_objects_alloc_page_count2 = internal_objects_alloc_page_count, 
       @internal_objects_dealloc_page_count2 = internal_objects_dealloc_page_count
from sys.dm_db_task_space_usage
WHERE session_id = @@spid;

-- Diff between the two states 
SELECT newly_allocated_pages = @internal_objects_alloc_page_count2-@internal_objects_alloc_page_count1 ,
       newly_deallocated_pages = @internal_objects_dealloc_page_count2-@internal_objects_dealloc_page_count1 ,
       new_in_use_pages = (@internal_objects_alloc_page_count2 - @internal_objects_dealloc_page_count2) - (@internal_objects_alloc_page_count1 - @internal_objects_dealloc_page_count1);
Run Code Online (Sandbox Code Playgroud)

  • @J.Mini - 它无法将 `+` ... `+` ... `+` 快速重写为 `CONCAT(..., ..., ...)`因为它们对“NULL”以及截断和其他东西有不同的行为(例如参见“SELECT '1' + '2' + 3, CONCAT('1', '2', 3)`) - 所以它需要具有相同行为的“N 元”版本,不会更改结果 (3认同)