将多行中的列合并为单行

Ben*_*cka 14 sql-server-2005 sql-server query

customer_comments由于数据库设计,我将一些拆分为多行,对于报告,我需要将comments每个唯一的数据id合并为一行。我以前尝试过使用SELECT 子句和 COALESCE技巧中的此分隔列表进行一些操作,但我不记得它并且一定没有保存它。在这种情况下,我似乎也无法让它工作,似乎只能在一行上工作。

数据如下所示:

id  row_num  customer_code comments
-----------------------------------
1   1        Dilbert        Hard
1   2        Dilbert        Worker
2   1        Wally          Lazy
Run Code Online (Sandbox Code Playgroud)

我的结果需要如下所示:

id  customer_code comments
------------------------------
1   Dilbert        Hard Worker
2   Wally          Lazy
Run Code Online (Sandbox Code Playgroud)

所以对于每row_num一个实际上只有一行结果;注释应按 的顺序组合row_num。上面链接的SELECT技巧可用于将特定查询的所有值作为一行获取,但我无法弄清楚如何使其作为SELECT将所有这些行吐出的语句的一部分工作。

我的查询必须自己遍历整个表并输出这些行。我没有将它们组合成多列,每一行一个,所以PIVOT似乎不适用。

Aar*_*and 18

这对于相关子查询来说相对简单。您不能使用您提到的博客文章中突出显示的 COALESCE 方法,除非您将其提取到用户定义的函数中(或者除非您一次只想返回一行)。以下是我通常这样做的方式:

DECLARE @x TABLE 
(
  id INT, 
  row_num INT, 
  customer_code VARCHAR(32), 
  comments VARCHAR(32)
);

INSERT @x SELECT 1,1,'Dilbert','Hard'
UNION ALL SELECT 1,2,'Dilbert','Worker'
UNION ALL SELECT 2,1,'Wally','Lazy';

SELECT id, customer_code, comments = STUFF((SELECT ' ' + comments 
    FROM @x AS x2 WHERE id = x.id
     ORDER BY row_num
     FOR XML PATH('')), 1, 1, '')
FROM @x AS x
GROUP BY id, customer_code
ORDER BY id;
Run Code Online (Sandbox Code Playgroud)

如果您遇到注释中的数据可能包含对 XML 不安全的字符 ( >, <, &) 的情况,您应该更改以下内容:

     FOR XML PATH('')), 1, 1, '')
Run Code Online (Sandbox Code Playgroud)

对于这种更精细的方法:

     FOR XML PATH(''), TYPE).value(N'(./text())[1]', N'varchar(max)'), 1, 1, '')
Run Code Online (Sandbox Code Playgroud)

(确保使用正确的目标数据类型,varcharnvarchar,和正确的长度,N如果使用,则在所有字符串文字前加上前缀nvarchar。)

  • +1 我为此创建了一个小提琴,以便快速浏览 http://sqlfiddle.com/#!3/e4ee5/2 (3认同)
  • 是的,这就像一个魅力。@MarlonRibunal SQL Fiddle 真的成型了! (3认同)
  • 我已经验证,无论基础表上的聚集索引如何,查询都会以正确的顺序返回结果(即使是 `row_num desc` 上的聚集索引也必须遵守 Mikael 建议的 `order by`)。由于查询包含正确的`order by`,我将删除其他暗示的评论,并希望@JonSeigel 考虑这样做。 (2认同)

Jon*_*gel 6

如果您被允许在您的环境中使用 CLR,那么这是为用户定义的聚合量身定制的案例。

特别是,如果源数据非常大和/或您需要在应用程序中大量执行此类操作,这可能是一种可行的方法。我强烈怀疑Aaron 解决方案的查询计划不会随着输入大小的增长而很好地扩展。(我尝试向临时表添加索引,但这没有帮助。)

这个解决方案,像许多其他事情一样,是一种权衡:

  • 甚至在您或您客户的环境中使用 CLR 集成的政治/政策。
  • CLR 函数可能更快,并且在给定一组真实数据的情况下会更好地扩展。
  • CLR 函数可在其他查询中重用,并且每次需要执行此类操作时都不必复制(和调试)复杂的子查询。
  • 直接 T-SQL 比编写和管理一段外部代码更简单。
  • 也许您不知道如何使用 C# 或 VB 进行编程。
  • 等等。

编辑:嗯,我去尝试看看这是否真的更好,结果表明当前无法使用聚合函数满足评论按特定顺序排列的要求。:(

请参阅SqlUserDefinedAggregateAttribute.IsInvariantToOrder。基本上,你需要做的是什么OVER(PARTITION BY customer_code ORDER BY row_num),但ORDER BY中不支持OVER子句汇总时。我假设将这个功能添加到 SQL Server 会打开一堆蠕虫,因为需要在执行计划中更改的内容是微不足道的。前面提到的链接说这是保留供将来使用,因此可以在将来实施(不过,在 2005 年,您可能不走运)。

可能仍然通过打包和解析来实现row_num价值为聚集串,然后该干嘛CLR对象中的那种......这似乎是相当的hackish。

无论如何,下面是我使用的代码,以防其他人发现即使有限制也很有用。我将把黑客部分留给读者作为练习。请注意,我使用 AdventureWorks (2005) 作为测试数据。

聚合组装:

using System;
using System.IO;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

namespace MyCompany.SqlServer
{
    [Serializable]
    [SqlUserDefinedAggregate
    (
        Format.UserDefined,
        IsNullIfEmpty = false,
        IsInvariantToDuplicates = false,
        IsInvariantToNulls = true,
        IsInvariantToOrder = false,
        MaxByteSize = -1
    )]
    public class StringConcatAggregate : IBinarySerialize
    {
        private string _accum;
        private bool _isEmpty;

        public void Init()
        {
            _accum = string.Empty;
            _isEmpty = true;
        }

        public void Accumulate(SqlString value)
        {
            if (!value.IsNull)
            {
                if (!_isEmpty)
                    _accum += ' ';
                else
                    _isEmpty = false;

                _accum += value.Value;
            }
        }

        public void Merge(StringConcatAggregate value)
        {
            Accumulate(value.Terminate());
        }

        public SqlString Terminate()
        {
            return new SqlString(_accum);
        }

        public void Read(BinaryReader r)
        {
            this.Init();

            _accum = r.ReadString();
            _isEmpty = _accum.Length == 0;
        }

        public void Write(BinaryWriter w)
        {
            w.Write(_accum);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用于测试的 T-SQL(CREATE ASSEMBLY,并sp_configure省略启用 CLR):

CREATE TABLE [dbo].[Comments]
(
    CustomerCode int NOT NULL,
    RowNum int NOT NULL,
    Comments nvarchar(25) NOT NULL
)

INSERT INTO [dbo].[Comments](CustomerCode, RowNum, Comments)
    SELECT
        DENSE_RANK() OVER(ORDER BY FirstName),
        ROW_NUMBER() OVER(PARTITION BY FirstName ORDER BY ContactID),
        Phone
        FROM [AdventureWorks].[Person].[Contact]
GO

CREATE AGGREGATE [dbo].[StringConcatAggregate]
(
    @input nvarchar(MAX)
)
RETURNS nvarchar(MAX)
EXTERNAL NAME StringConcatAggregate.[MyCompany.SqlServer.StringConcatAggregate]
GO


SELECT
    CustomerCode,
    [dbo].[StringConcatAggregate](Comments) AS AllComments
    FROM [dbo].[Comments]
    GROUP BY CustomerCode
Run Code Online (Sandbox Code Playgroud)