Combine a coluna de várias linhas em uma única linha

14

Eu tenho alguns customer_commentsdivididos em várias linhas devido ao design do banco de dados e, para um relatório, preciso combinar o commentsde cada exclusivo idem uma linha. Eu já havia tentado algo trabalhando com essa lista delimitada da cláusula SELECT e do truque COALESCE, mas não consigo me lembrar e não deve ter salvado. Também não consigo fazê-lo funcionar neste caso, apenas parece funcionar em uma única linha.

Os dados são assim:

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

Meus resultados precisam ficar assim:

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

Então, para cada row_numnão há realmente apenas uma linha de resultados; os comentários devem ser combinados na ordem de row_num. O link acimaSELECT truque funciona para obter todos os valores de uma consulta específica como uma linha, mas não consigo descobrir como fazê-lo funcionar como parte de uma SELECTinstrução que cospe todas essas linhas.

Minha consulta precisa percorrer toda a tabela por conta própria e gerar essas linhas. Não os estou combinando em várias colunas, uma para cada linha, portanto PIVOTnão parece aplicável.

Ben Brocka
fonte

Respostas:

18

Isso é relativamente trivial relacionado a uma subconsulta correlacionada. Você não pode usar o método COALESCE destacado na postagem do blog mencionada, a menos que você o extraia para uma função definida pelo usuário (ou a menos que deseje retornar apenas uma linha por vez). Aqui está como eu normalmente faço isso:

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;

Se você tem um caso em que os dados em comentários podem conter caracteres inseguros-para-XML ( >, <, &), você deve mudar isso:

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

Para esta abordagem mais elaborada:

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

(Certifique-se de usar o tipo de dados de destino correto, varcharou nvarchar, e o comprimento certo, e prefixe todos os literais de cadeia de caracteres Nse estiver usando nvarchar.)

Aaron Bertrand
fonte
3
+1 I creadted um violino para que para um olhar rápido sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal
3
Sim, isso funciona como um encanto. @MarlonRibunal O SQL Fiddle está realmente se formando!
Ben Brocka
@NickChammas - vou esticar o pescoço e dizer que o pedido é garantido usando o order byna subconsulta. Isso está criando XML usando for xmle essa é a maneira de criar XML usando TSQL. A ordem dos elementos nos arquivos XML é um assunto importante e pode ser confiável. Portanto, se essa técnica não garantir a ordem, o suporte a XML no TSQL será gravemente quebrado.
Mikael Eriksson
2
Eu validei que a consulta retornará resultados na ordem correta, independentemente do índice agrupado na tabela subjacente (mesmo um índice agrupado row_num descdeve obedecer ao order byque Mikael sugeriu). Vou remover os comentários que sugerem o contrário agora que a consulta contém o direito order bye espero que @JonSeigel considere fazer o mesmo.
Aaron Bertrand
6

Se você tem permissão para usar o CLR em seu ambiente, este é um caso personalizado para um agregado definido pelo usuário.

Em particular, esse é provavelmente o caminho a seguir se os dados de origem não forem trivialmente grandes e / ou você precisar fazer muito esse tipo de coisa em seu aplicativo. Suspeito fortemente que o plano de consulta da solução de Aaron não seja dimensionado bem à medida que o tamanho da entrada aumentar. (Tentei adicionar um índice à tabela temporária, mas isso não ajudou.)

Essa solução, como muitas outras coisas, é uma troca:

  • Política / política para usar o CLR Integration no seu ambiente ou no do seu cliente.
  • A função CLR é provavelmente mais rápida e será melhor dimensionada, considerando um conjunto real de dados.
  • A função CLR será reutilizável em outras consultas e você não precisará duplicar (e depurar) uma subconsulta complexa toda vez que precisar fazer esse tipo de coisa.
  • O T-SQL direto é mais simples do que escrever e gerenciar um pedaço de código externo.
  • Talvez você não saiba como programar em C # ou VB.
  • etc.

Edição: Bem, fui tentar ver se isso realmente era melhor, e verifica-se que o requisito de que os comentários estejam em uma ordem específica não é atualmente possível satisfazer usando uma função agregada. :(

Consulte SqlUserDefinedAggregateAttribute.IsInvariantToOrder . Basicamente, o que você precisa fazer é, OVER(PARTITION BY customer_code ORDER BY row_num)mas ORDER BYnão é suportado na OVERcláusula ao agregar. Suponho que adicionar essa funcionalidade ao SQL Server abra uma lata de worms, porque o que precisaria ser alterado no plano de execução é trivial. O link mencionado acima diz que isso é reservado para uso futuro; portanto, isso pode ser implementado no futuro (em 2005, você provavelmente está sem sorte).

Isso ainda pode ser feito compactando e analisando o row_numvalor na cadeia de caracteres agregada e, em seguida, fazendo a classificação dentro do objeto CLR ... o que parece bastante tolo.

De qualquer forma, abaixo está o código que eu usei caso alguém ache isso útil, mesmo com a limitação. Vou deixar a parte de hackers como um exercício para o leitor. Observe que eu usei o AdventureWorks (2005) para dados de teste.

Montagem agregada:

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 para teste ( CREATE ASSEMBLYe sp_configurepara ativar o CLR omitido):

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
Jon Seigel
fonte
1

Aqui está uma solução baseada em cursor que garante a ordem dos comentários por row_num. (Veja minha outra resposta para saber como a [dbo].[Comments]tabela foi preenchida.)

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
Jon Seigel
fonte
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
Gary
fonte
2
Você não evitou um cursor. Você acabou de chamar seu cursor de loop while.
Aaron Bertrand