nvarchar 连接 / 索引 / nvarchar(max) 莫名其妙的行为

Posted

技术标签:

【中文标题】nvarchar 连接 / 索引 / nvarchar(max) 莫名其妙的行为【英文标题】:nvarchar concatenation / index / nvarchar(max) inexplicable behavior 【发布时间】:2022-01-23 23:07:22 【问题描述】:

我今天在 SQL Server(2008R2 和 2012)中遇到了一个非常奇怪的问题。我正在尝试使用连接结合select 语句来构建字符串。

我找到了解决方法,但我真的很想了解这里发生了什么以及为什么它没有给我预期的结果。谁能给我解释一下?

http://sqlfiddle.com/#!6/7438a/1

根据要求,这里还有代码:

-- base table
create table bla (
    [id] int identity(1,1) primary key,
    [priority] int,
    [msg] nvarchar(max),
    [autofix] bit
)

-- table without primary key on id column
create table bla2 (
    [id] int identity(1,1),
    [priority] int,
    [msg] nvarchar(max),
    [autofix] bit
)

-- table with nvarchar(1000) instead of max
create table bla3 (
    [id] int identity(1,1) primary key,
    [priority] int,
    [msg] nvarchar(1000),
    [autofix] bit
)

-- fill the three tables with the same values
insert into bla ([priority], [msg], [autofix])
values (1, 'A', 0),
       (2, 'B', 0)

insert into bla2 ([priority], [msg], [autofix])
values (1, 'A', 0),
       (2, 'B', 0)

insert into bla3 ([priority], [msg], [autofix])
values (1, 'A', 0),
       (2, 'B', 0)
;
declare @a nvarchar(max) = ''
declare @b nvarchar(max) = ''
declare @c nvarchar(max) = ''
declare @d nvarchar(max) = ''
declare @e nvarchar(max) = ''
declare @f nvarchar(max) = ''

-- I expect this to work and generate 'AB', but it doesn't
select @a = @a + [msg]
    from bla
    where   autofix = 0
    order by [priority] asc

-- this DOES work: convert nvarchar(4000)
select @b = @b + convert(nvarchar(4000),[msg])
    from bla
    where   autofix = 0
    order by [priority] asc

-- this DOES work: without WHERE clause
select @c = @c + [msg]
    from bla
    --where autofix = 0
    order by [priority] asc

-- this DOES work: without the order by
select @d = @d + [msg]
    from bla
    where   autofix = 0
    --order by [priority] asc

-- this DOES work: from bla2, so without the primary key on id
select @e = @e + [msg]
    from bla2
    where   autofix = 0
    order by [priority] asc

-- this DOES work: from bla3, so with msg nvarchar(1000) instead of nvarchar(max)
select @f = @f + [msg]
    from bla3
    where   autofix = 0
    order by [priority] asc

select @a as a, @b as b, @c as c, @d as d, @e as e, @f as f

【问题讨论】:

这很好,但是您能否在问题中包含一些重现问题所需的代码? SQLFiddle 非常有用,但代码不应该存在 only 那里。 你到底是什么意思?这是 SQL 中的问题,而不是其他地方.. 对吗? 我的意思是你在 SQLfiddle 上的复制品,但它在问题的代码块中。 啊。当然。将其添加到问题中。 :) 【参考方案1】:

TLDR; 这不是用于跨行连接字符串的记录/支持的方法。它有时会起作用,但有时也会失败,因为这取决于您获得的执行计划。

改为使用以下有保证的方法之一

SQL Server 2017+

SELECT @a = STRING_AGG([msg], '') WITHIN GROUP (ORDER BY [priority] ASC)
FROM bla
where   autofix = 0

SQL Server 2005+

SELECT @a = (SELECT [msg] + ''
             FROM   bla
             WHERE  autofix = 0
             ORDER  BY [priority] ASC
             FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)') 

背景

已由 VanDerNorth 链接的 KB article 确实包含该行

聚合串联查询的正确行为是 未定义。

但随后通过提供似乎确实表明确定性行为是可能的解决方法继续使水域变得混乱。

为了从聚合中获得预期的结果 连接查询,将任何 Transact-SQL 函数或表达式应用于 SELECT 列表中的列而不是 ORDER BY 子句中的列。

您有问题的查询未将任何表达式应用于ORDER BY 子句中的列。

2005年的文章Ordering guarantees in SQL Server...确实说明了

出于向后兼容的原因,SQL Server 支持 SELECT @p = @p + 1 ... ORDER BY 类型的赋值在最顶部 范围。

在连接按预期工作的计划中,带有表达式[Expr1003] = Scalar Operator([@x]+[Expr1004]) 的计算标量出现在排序上方。

在无法工作的计划中,计算标量出现在排序下方。正如this connect item 中解释的那样,从 2006 年开始,当表达式 @x = @x + [msg] 出现在它为每一行评估的排序下方时,但所有评估最终都使用 @x 的预分配值。在 2006 年的 another similar Connect Item 中,微软的回复谈到了“修复”这个问题。

Microsoft 对有关此问题的所有后续 Connect 项目(并且有很多)的回应表明,这根本无法保证

Example 1

我们不保证连接的正确性 查询(例如在数据检索中使用变量赋值) 具体顺序)。 SQL Server 2008 中的查询输出可能会发生变化 取决于计划选择、表格中的数据等。你不应该 即使语法允许您始终依赖此工作 编写一个 SELECT 语句,将有序行检索与 变量赋值。

Example 2

您看到的行为是设计使然。使用赋值操作 (本例中的串联)在带有 ORDER BY 子句的查询中有 未定义的行为。这可以从发布到发布甚至改变 由于查询计划的更改,在特定的服务器版本中。 即使有变通方法,您也不能依赖此行为。看 以下知识库文章了解更多详情:http://support.microsoft.com/kb/287515 唯一保证 机制如下:

    使用游标按特定顺序遍历行并连接值 用于带有 ORDER BY 的 xml 查询以生成连接值 使用 CLR 聚合(这不适用于 ORDER BY 子句)

Example 3

您看到的行为实际上是设计使然。这与 SQL 是一种集合操作语言。 SELECT 中的所有表达式 列表(这也包括作业)不保证是 每个输出行只执行一次。其实SQL查询 优化器尽可能少地执行它们。这 当您计算 变量基于表中的一些数据,但是当你的值 分配取决于同一变量的先前值, 结果可能出乎意料。如果查询优化器移动 表达式到查询树中的不同位置,它可能会得到 评估次数更少(或仅一次,如您的示例之一)。这 这就是为什么我们不建议使用“迭代”类型分配 计算聚合值。我们发现基于 XML 的解决方法……通常适用于 客户

Example 4

即使没有 ORDER BY,我们也不保证 @var = @var + 将为任何语句生成连接值 这会影响多行。表达式的右侧可以 在查询执行期间被评估一次或多次,并且 我所说的行为取决于计划。

Example 5

SELECT 语句的变量赋值是专有语法 (仅限 T-SQL)行为未定义或依赖于计划,如果 产生多行。如果您需要进行字符串连接 然后使用基于 SQLCLR 聚合或 FOR XML 查询的连接或 其他关系方法。

【讨论】:

很棒的信息/很高兴知道。遗憾的是,连接链接现在都死了。我尝试在新的但可怕的 UserVoice 系统上找到其中一些,但找不到它们。但是,我确实找到了两个可能很好链接到此处的较新的(我用指向此答案的链接对它们进行了评论):feedback.azure.com/forums/908035-sql-server/suggestions/… 和 feedback.azure.com/forums/908035-sql-server/suggestions/… 所以一方面微软说我们需要使用 CLR,另一方面 Azure 不支持它 - brentozar.com/archive/2016/04/…。 @RomanPekar - 在 Azure(或任何最新版本)上,只需使用 STRING_AGG @MartinSmith 是的,对于字符串连接它确实有效,但不适用于更复杂的情况 - ***.com/questions/58288057/…。拥有这个@var = @var + 1 有点像拥有匿名用户定义的聚合。好吧,不完全是,不可能使用group by 子句,但它仍然提供了一些不错的可能性。【参考方案2】:

好像有点像这个帖子:VARCHAR(MAX) acting weird when concatenating string

那里的结论: 这种字符串连接方法通常确实有效,但不能保证。 The official line in the KB article 的类似问题是“未定义聚合连接查询的正确行为。”

【讨论】:

嗯。谢谢。不过,“未定义的行为”并不能真正让我满意。接下来,您参考的知识库文章适用于 SQL Server 2000 和 7.0;现在不应该解决吗? @bartlaarhoven - 没有什么可以解决的,因为这种行为从来没有得到保证,所以你不应该依赖它。请参阅Concatenating Row Values in Transact-SQL 了解替代方法。

以上是关于nvarchar 连接 / 索引 / nvarchar(max) 莫名其妙的行为的主要内容,如果未能解决你的问题,请参考以下文章

在 NVarchar(50) 与 NVarchar(255) 列中使用索引?

对索引的 SQL 输入参数过滤默认为 nvarchar 导致全表扫描

如何计算时间预言机的算术平均值?

将Nvarchar(max)转换为nvarchar(30)的最佳/最有效的方法

使用SQL Server XML Import添加索引

在 SQL 中连接 int 和 nvarchar 列