【问题标题】:Using SQL concatenation with ORDER BY将 SQL 连接与 ORDER BY 结合使用
【发布时间】:2015-05-21 07:08:00
【问题描述】:

我很困惑。您如何用 ORDER BY 解释变量连接中的这种差异?

declare @tbl table (id int);
insert into @tbl values (1), (2), (3);

declare @msg1 varchar(100) = '', @msg2 varchar(100) = '',
    @msg3 varchar(100) = '',    @msg4 varchar(100) = '';

select @msg1 = @msg1 + cast(id as varchar) from @tbl
order by id;

select @msg2 = @msg2 + cast(id as varchar) from @tbl
order by id+id;

select @msg3 = @msg3 + cast(id as varchar) from @tbl
order by id+id desc;

select TOP(100) @msg4 = @msg4 + cast(id as varchar) from @tbl
order by id+id;

select
    @msg1 as msg1,
    @msg2 as msg2,
    @msg3 as msg3,
    @msg4 as msg4;

结果

msg1  msg2  msg3  msg4
----  ----  ----  ----
123   3     1     123  

【问题讨论】:

  • 请参阅"Should questions include “tags” in their titles?",其中的共识是“不,他们不应该”!
  • 嗯,看起来像个bug
  • 这是一种奇怪的行为,因为做了一些不恰当的事情。不应根据返回多个值的行来分配变量。就个人而言,我认为这应该给出一个错误,但它可能有一个很好的理由不这样做。
  • 这早就documented不工作了:“不要在SELECT语句中使用变量来连接值(即计算聚合值)。可能会出现意外的查询结果。这是因为不能保证 SELECT 列表中的所有表达式(包括赋值)对于每个输出行都只执行一次"
  • 来自 Damien 的链接,那里链接的知识库文章直接适用于这个问题:support.microsoft.com/en-us/kb/287515。需要注意的是,在 ORDER BY 子句中使用任何运算符或表达式都会导致这种意外行为。

标签: sql sql-server tsql sql-server-2014


【解决方案1】:

正如许多人所证实的那样,这不是将一列中的所有行连接成一个变量的正确方法——即使在某些情况下它确实“有效”。如果您想查看一些替代方案,请查看this blog

According to MSDN(适用于 SQL Server 2008 到 2014 和 Azure SQL 数据库),SELECT 不应用于分配局部变量。在备注中,它描述了当您使用SELECT 时,它如何尝试表现。需要注意的有趣点:

  • 虽然通常它应该只用于将单个值返回给变量,但当表达式是列的名称时,它可以返回多个值。
  • 当表达式确实返回多个值时,将为变量分配最后一个返回的值。
  • 如果没有返回值,则变量保留其原始值(此处不直接相关,但值得注意)。

这里的前两点很关键 - 连接恰好起作用,因为SELECT @msg1 = @msg1 + cast(id as varchar) 本质上是SELECT @msg1 += cast(id as varchar),并且正如语法说明,+= 是该表达式上可接受的复合赋值运算符。请注意,不应期望 VARCHAR 继续支持此操作并进行字符串连接 - 仅仅因为它恰好在某些情况下工作并不意味着它适用于生产代码。

关于根本原因的底线是在 select 表达式上运行的 Compute Scalar 是使用原始 id 列还是 id 列的表达式。您可能找不到任何文档说明为什么优化器可能会为每个查询选择特定计划,但每个示例都强调了不同的用例,这些用例允许从列中评估 msg 值(因此返回并连接多行)或表达式(因此只有最后一列)。

  1. @msg1 为“123”,因为Compute Scalar(变量赋值的逐行计算)发生在Sort 之后。这允许标量计算在通过+= 复合运算符连接它们的 id 列上返回多个值。我怀疑是否有具体的文档说明原因,但似乎优化器选择在标量计算之前进行排序,因为 order by 是一列而不是表达式。

  2. @msg2 是 '3' 因为Compute Scalar 在排序之前完成,这使得每行中的@msg2 只是 ('' + id) - 所以永远不会连接,只是ID。同样,可能没有任何文档说明优化器选择这个的原因,但似乎由于 order by 是一个表达式,也许它需要在 order by 中执行 (id+id) 作为标量计算的一部分,然后才能进行排序。此时,您的原始列不再引用源列,而是已被表达式替换。因此,正如 MSDN 所述,您的第一列指向一个表达式,而不是一列,因此行为将结果集的最后一个值分配给 SELECT 中的变量。由于您对 ASC 进行了排序,因此您在此处得到“3”。

  3. @msg3 为“1”,原因与示例 2 相同,但您订购了 DESC。同样,这成为评估中的表达式 - 不是原始列,因此赋值获得 DESC 顺序的最后一个值,因此您得到 '1'。

  4. @msg4 再次为“123”,因为TOP 操作强制对ORDER BY 进行初始标量评估,以便它可以确定您的前 100 条记录。这与示例 2 和 3 不同,其中标量计算包含 order by 和 select 计算,这导致每个示例都是一个表达式,而不是引用原始列。示例 4 的 TOP 将 ORDER BY 和 SELECT 计算分开,因此在应用 SORT (TOP N SORT) 之后,它会为 SELECT 列执行标量计算,此时您仍在引用原始列(不是列的表达式),因此它返回允许连接发生的多行。

来源:

【讨论】:

  • 非常详细的答案。谢谢
【解决方案2】:

SQL Server 将计算结果,然后对它们进行排序,然后返回它们。在分配变量的情况下,只有第一个结果将用于填充您的变量。您正在从排序的结果集中接收第一个值,它可以移动 SQL Server 扫描记录的顺序以及结果中的位置。

TOP 总是会生成特殊的查询计划,因为它会立即强制 SQL Server 坚持结果的自然顺序,而不是生成会在统计上减少它必须读取的记录数量的查询计划。

要解释这些差异,您必须参考 SQL Server 如何决定对值进行隐式排序以优化查询。

查询 1

插入 -> 表格插入 -> 恒定扫描

查询 2

选择 -> 计算标量 -> 排序 -> 表扫描

查询 3 和 4

选择 -> 排序 -> 计算标量 -> 表扫描

查询 5 和 6(使用 TOP)

SELECT -> 计算标量 -> 排序(前 N 个)-> 计算标量 -> 表 扫描

我添加了查询 6:

select top (100)
    @msg5 = @msg5 + cast(id as varchar)
from @tbl
order by id+id desc

【讨论】:

  • 执行计划不同,最终结果应该不一样。
  • @GiorgiNakeuri 通常你是对的,但这不是你的正常情况。由于只使用第一个结果,SQL Server 如何决定结果将影响使用哪个结果。
  • @GiorgiNakeuri 如果您在定义的操作中工作,是的。这不是这里的情况——不要指望当你使用肮脏的黑客时东西不会破坏:)
  • 对不起,我合并了两个不应该的查询计划,我的答案现在应该更清楚了。
【解决方案3】:

我所看到的只是执行计划有所不同。它们都以 SELECT 开头,以 Table Scan 结尾。区别在于计算标量和排序之间。

@Msg1 has Compute Scalar then Sort. Results: 123
@Msg2 has Sort then Compute Scalar. Results: 3
@Msg3 has Sort then Compute Scalar. Results: 1

第四个是不同的,因为顶部。它仍然以 select 开始,以 table scan 结束,但中间有所不同。它使用不同的排序。

@Msg4 has Compute Scalar then Sort(Top N Sort) then Compute Scalar

【讨论】:

    【解决方案4】:

    您不应该在返回多于一行的选择中设置变量。考虑这段代码:

    select top 1 @msg1 = @msg1 + cast(id as varchar) from @tbl
    order by id;
    
    select top 1 @msg2 = @msg2 + cast(id as varchar) from @tbl
    order by id+id;
    
    select top 1 @msg3 = @msg3 + cast(id as varchar) from @tbl
    order by id+id desc;
    
    select top 1 @msg4 = @msg4 + cast(id as varchar) from @tbl
    order by id+id;
    

    分别产生 1、1、3 和 1。

    我很惊讶它没有引起异常,我很确定它曾经完全禁止这种情况。

    基本点仍然相同:SQL 引擎不只是像您所期望的那样,一个一个按程序执行某些命令。它将构建一个为尽可能高效而量身定制的执行计划(给定许多约束)。

    另一方面,分配变量本质上是过程性的,需要明确的执行/评估顺序才能正常工作。

    您正在组合这两种方法 - select id from @tbl order by id 是非程序查询,但 select @id = id from @tbl order by id 是程序 @id = id 和非常非程序选择的混合。

    【讨论】:

    • 这是一种将同一列的多行连接成一个变量的技术,是有效的。 OP 询问为什么这种技术的行为会根据 order by 而有所不同。
    • @JasonW 我想看看这方面的参考资料。即使 SQL 服务器决定并行化查询,您是否真的希望它以合理的方式运行?或者当它分布在服务器集群或分区表和索引上时?
    • @JasonW - 就“不会产生错误”而言,“有效”,是的。就“按照 OP 预期 做的事情”而言,“有效”并没有那么多。请参阅我对该问题的评论中的参考。
    • 我并不是说这是最好的方法,甚至不是好方法 - 只是它是许多人使用的方法。
    • @JasonW 某些有时起作用的东西,当星星是正确的时候。这不是一种“技术”,而是一种肮脏的技巧。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-26
    • 2014-09-26
    • 1970-01-01
    相关资源
    最近更新 更多