将多行中的列合并为单行


14

customer_comments由于数据库设计的缘故,我将某些内容分成了多行,而对于一份报告,我需要将comments每个唯一字段组合id成一行。我以前尝试过使用SELECT子句和COALESCE技巧在此定界列表中进行操作,但是我无法记住它并且一定不能保存它。在这种情况下,我似乎也无法使其工作,只能在单行上工作。

数据如下所示:

id  row_num  customer_code comments
-----------------------------------
1   1        Dilbert        Hard
1   2        Dilbert        Worker
2   1        Wally          Lazy

我的结果需要看起来像这样:

id  customer_code comments
------------------------------
1   Dilbert        Hard Worker
2   Wally          Lazy

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

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

Answers:


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;

如果你有一个情况下注释中的数据可能含有不安全换XML字符(><&),你应该改变这样的:

     FOR XML PATH('')), 1, 1, '')

对于这种更精细的方法:

     FOR XML PATH(''), TYPE).value(N'(./text())[1]', N'varchar(max)'), 1, 1, '')

(一定要使用正确的目标数据类型,varchar或者nvarchar与和正确的长度,和前缀的所有字符串字面量N,如果使用nvarchar。)


3
+1我为此花了个小提琴,以便快速查看sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal 2012年

3
是的,这就像一种魅力。@MarlonRibunal SQL小提琴真的很帅!
本·布罗卡

@NickChammas-我要伸出脖子,说可以使用order by子查询中的来保证顺序。这是使用构建XML 方式for xml,也是使用TSQL构建XML 方式。XML文件中元素的顺序很重要,可以依靠。因此,如果此技术不能保证顺序,则TSQL中的XML支持会严重中断。
Mikael Eriksson 2012年

2
我已经验证了查询将以正确的顺序返回结果,而不管基础表上的聚簇索引如何(即使聚簇索引也row_num desc必须遵循order byMikael的建议)。我将删除注释,表明现在查询已包含该权限,order by并希望@JonSeigel考虑这样做。
亚伦·伯特兰

6

如果允许您在环境中使用CLR,则这是针对用户定义的聚合的量身定制案例。

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

与许多其他事物一样,此解决方案是一个折衷方案:

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

编辑:好吧,我去尝试看看这实际上是否更好,事实证明,使用聚合函数当前无法满足注释按特定顺序的要求。:(

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

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

无论如何,以下是我使用的代码,以防任何人即使有限制也觉得有用。我将把hacking部分留给读者作为练习。请注意,我使用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);
        }
    }
}

用于测试的T-SQL(CREATE ASSEMBLYsp_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

1

这是一个基于游标的解决方案,通过保证了评论的顺序row_num。(见我的其他回答了如何[dbo].[Comments]在人口表。)

SET NOCOUNT ON

DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
    SELECT
        CustomerCode,
        Comments
        FROM [dbo].[Comments]
        ORDER BY
            CustomerCode,
            RowNum

DECLARE @curCustomerCode int
DECLARE @lastCustomerCode int
DECLARE @curComment nvarchar(25)
DECLARE @comments nvarchar(MAX)

DECLARE @results table
(
    CustomerCode int NOT NULL,
    AllComments nvarchar(MAX) NOT NULL
)


OPEN cur

FETCH NEXT FROM cur INTO
    @curCustomerCode, @curComment

SET @lastCustomerCode = @curCustomerCode


WHILE @@FETCH_STATUS = 0
BEGIN

    IF (@lastCustomerCode != @curCustomerCode)
    BEGIN
        INSERT INTO @results(CustomerCode, AllComments)
            VALUES(@lastCustomerCode, @comments)

        SET @lastCustomerCode = @curCustomerCode
        SET @comments = NULL
    END

    IF (@comments IS NULL)
        SET @comments = @curComment
    ELSE
        SET @comments = @comments + N' ' + @curComment

    FETCH NEXT FROM cur INTO
        @curCustomerCode, @curComment

END

IF (@comments IS NOT NULL)
BEGIN
    INSERT INTO @results(CustomerCode, AllComments)
        VALUES(@curCustomerCode, @comments)
END

CLOSE cur
DEALLOCATE cur


SELECT * FROM @results

0
-- solution avoiding the cursor ...

DECLARE @idMax INT
DECLARE @idCtr INT
DECLARE @comment VARCHAR(150)

SELECT @idMax = MAX(id)
FROM [dbo].[CustomerCodeWithSeparateComments]

IF @idMax = 0
    return
DECLARE @OriginalTable AS Table
(
    [id] [int] NOT NULL,
    [row_num] [int] NULL,
    [customer_code] [varchar](50) NULL,
    [comment] [varchar](120) NULL
)

DECLARE @FinalTable AS Table
(
    [id] [int] IDENTITY(1,1) NOT NULL,
    [customer_code] [varchar](50) NULL,
    [comment] [varchar](120) NULL
)

INSERT INTO @FinalTable 
([customer_code])
SELECT [customer_code]
FROM [dbo].[CustomerCodeWithSeparateComments]
GROUP BY [customer_code]

INSERT INTO @OriginalTable
           ([id]
           ,[row_num]
           ,[customer_code]
           ,[comment])
SELECT [id]
      ,[row_num]
      ,[customer_code]
      ,[comment]
FROM [dbo].[CustomerCodeWithSeparateComments]
ORDER BY id, row_num

SET @idCtr = 1
SET @comment = ''

WHILE @idCtr < @idMax
BEGIN

    SELECT @comment = @comment + ' ' + comment
    FROM @OriginalTable 
    WHERE id = @idCtr
    UPDATE @FinalTable
       SET [comment] = @comment
    WHERE [id] = @idCtr 
    SET @idCtr = @idCtr + 1
    SET @comment = ''

END 

SELECT @comment = @comment + ' ' + comment
        FROM @OriginalTable 
        WHERE id = @idCtr

UPDATE @FinalTable
   SET [comment] = @comment
WHERE [id] = @idCtr

SELECT *
FROM @FinalTable

2
您还没有避免过光标。您只是将游标称为while循环。
亚伦·伯特兰
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.