nvarchar连接/索引/nvarchar(max)的不可解释行为

15
我今天在SQL Server(包括2008R2和2012版本)遇到了一个非常奇怪的问题。我试图使用连接操作符和select语句来构建一个字符串。
我发现最终的字符串只包含了一个输入字符串,而不是我期望的所有字符串的连接结果。
我已经找到了解决方法,但我真的很想知道这里发生了什么,为什么它没有给我期望的结果。有人能解释一下吗?

http://sqlfiddle.com/#!18/d6228/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非常有用,但代码不应该仅存在于那里。 - Aaron Bertrand
你的意思是什么?这是SQL中的一个问题,不是其他地方...对吗? - bartlaarhoven
我的意思是您在SQLfiddle上的示例,但请将其放在问题的代码块中。 - Aaron Bertrand
啊,好的。已经添加到问题中了。 :) - bartlaarhoven
2个回答

33
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)') 

背景

KB article已经被VanDerNorth链接,其中包含以下内容:

聚合拼接查询的正确行为是未定义的。

但是,随后提供了一个解决方法,似乎表明确定性行为是可能的。

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

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

2005年的文章Ordering guarantees in SQL Server...确实指出:

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

在拼接工作正常的计划中,具有表达式[Expr1003] = Scalar Operator([@x]+[Expr1004])的计算标量位于排序操作之上。

在拼接失败的计划中,计算标量位于排序操作之下。就像2006年这个连接项所解释的那样,当表达式@x = @x + [msg]出现在排序操作之下时,它会为每一行计算但所有的计算都使用了@x的预先分配值。在另一个类似的连接项中,来自Microsoft的回复是"修复"此问题。

关于此问题的所有后续Connect条目中,微软的回应(有很多)都指出这并不是完全可靠的。

示例1

我们不保证连接查询的正确性(例如在特定顺序中使用变量赋值与数据检索)。查询输出可能因 SQL Server 2008 的计划选择、表中的数据等而发生变化。尽管语法允许您编写混合有序行检索和变量赋值的 SELECT 语句,但您不应该依赖此功能能够一致地工作。

示例2

您所看到的行为是设计上的。在带有ORDER BY子句的查询中使用赋值操作(例如拼接)具有未定义的行为。这可能会因为查询计划的更改而从一个版本到另一个版本甚至在特定的服务器版本中发生变化。即使有解决方法,您也不能依赖此行为。请参见下面的KB文章以了解更多详情:
http://support.microsoft.com/kb/287515 唯一保证的机制是以下几种:

  1. 使用游标按特定顺序循环遍历行并连接值
  2. 使用带有ORDER BY的for xml查询生成连接的值
  3. 使用CLR聚合(这将无法使用ORDER BY子句)

示例3

您所看到的行为实际上是按设计来的。这与SQL是一种集合操作语言有关。SELECT列表中的所有表达式(包括赋值)都不能保证对每个输出行执行一次。事实上,SQL查询优化器会尽可能少地执行它们。当您根据表中的某些数据计算变量的值时,这将产生预期的结果,但是当分配的值取决于同一变量的先前值时,结果可能会非常意外。如果查询优化器将表达式移动到查询树中的不同位置,则可能会评估较少次数(或仅一次,如您示例中的一个)。这就是为什么我们不建议使用“迭代”类型的赋值来计算聚合值。我们发现基于XML的解决方法通常对客户很有效。示例4 即使没有使用ORDER BY,我们也不能保证@var = @var+会为影响多行的任何语句生成连接值。在查询执行期间,表达式的右侧可以被评估一次或多次,并且行为是计划相关的。 示例5 使用SELECT语句进行变量赋值是专有语法(仅限T-SQL),如果产生多行,则行为未定义或计划相关。如果需要进行字符串连接,则使用SQLCLR聚合函数或基于FOR XML查询的连接或其他关系方法。

3
很棒的信息/好知道。很遗憾,现在所有的连接都已失效。我试着在新的但可怕的UserVoice系统上找到它们中的一些,但没有找到。但是,我发现了另外两个较新的链接,可能适合在这里添加链接(我在它们上面评论并附上了这个答案的链接):https://feedback.azure.com/forums/908035-sql-server/suggestions/34298851-select-powered-variable-assignment-result-invalid和https://feedback.azure.com/forums/908035-sql-server/suggestions/35104033-concatenating-varchar-max-columns-into-a-string-s - Solomon Rutzky
一方面,微软表示我们需要使用CLR,另一方面,它在Azure中不受支持-https://www.brentozar.com/archive/2016/04/breaking-news-literally-sql-clr-support-removed-azure-sql-db。 - Roman Pekar
1
@RomanPekar - 在Azure(或任何最新版本)中,只需使用STRING_AGG - Martin Smith
@MartinSmith 是的,对于字符串连接它确实有效,但对于更复杂的情况则不然 - https://stackoverflow.com/questions/58288057/sql-server-aggregate-if-only-one-distinct-value-nulls-without-ansi-warnings。使用 @var = @var + 1 就像是使用匿名用户定义的聚合函数。虽然不能使用 group by 子句,但仍然可以打开一些不错的可能性。 - Roman Pekar

2

嗯,谢谢。但是“未定义行为”并没有真正满足我的需求。此外,您提到的 KB 文章适用于 SQL Server 2000 和 7.0 版本;现在不应该已经修复了吗? - bartlaarhoven
2
@bartlaarhoven - 没有什么需要修复的,因为行为从未得到保证,所以您不应该依赖它。请参阅在Transact-SQL中连接行值以获取替代方法。 - Martin Smith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接