Combina columnas de múltiples filas en una sola fila


14

Tengo algunos customer_commentsdivididos en varias filas debido al diseño de la base de datos, y para un informe necesito combinar el commentsde cada único iden una fila. Previamente intenté algo trabajando con esta lista delimitada de la cláusula SELECT y el truco COALESCE, pero no puedo recordarlo y no debo haberlo guardado. Parece que tampoco puedo hacer que funcione en este caso, solo parece funcionar en una sola fila.

Los datos se ven así:

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

Mis resultados deben verse así:

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

Entonces, para cada row_numuno solo hay una fila de resultados; los comentarios deben combinarse en el orden de row_num. El SELECTtruco vinculado anterior funciona para obtener todos los valores para una consulta específica como una fila, pero no puedo entender cómo hacer que funcione como parte de una SELECTdeclaración que escupe todas estas filas.

Mi consulta tiene que recorrer toda la tabla por sí sola y generar estas filas. No los estoy combinando en varias columnas, una para cada fila, por lo PIVOTque no parece aplicable.

Respuestas:


18

Esto es relativamente trivial con una subconsulta correlacionada. No puede usar el método COALESCE resaltado en la publicación de blog que menciona a menos que extraiga eso en una función definida por el usuario (o a menos que solo desee devolver una fila a la vez). Así es como normalmente hago esto:

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;

Si usted tiene un caso en el que los datos de comentarios podrían contener caracteres inseguros-para-XML ( >, <, &), debe cambiar esta situación:

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

Para este enfoque más elaborado:

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

(Asegúrese de usar el tipo de datos de destino correcto, varcharo nvarchar, y la longitud correcta, y prefije todos los literales de cadena con Nsi se usa nvarchar).


3
1 Me creadted un violín de que para un vistazo rápido sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal

3
Sí, esto funciona como un encanto. ¡@MarlonRriage SQL Fiddle realmente se está formando!
Ben Brocka

@NickChammas: voy a sacar el cuello y decir que el pedido está garantizado usando el order byen la subconsulta. Esto es construir XML usando for xmly esa es la manera de construir XML usando TSQL. El orden de los elementos en un archivo XML es un asunto importante y se puede confiar en él. Entonces, si esta técnica no garantiza el orden, entonces el soporte XML en TSQL está severamente roto.
Mikael Eriksson

2
He validado que la consulta devolverá los resultados en el orden correcto independientemente del índice agrupado en la tabla subyacente (incluso un índice agrupado row_num descdebe obedecer a lo order byque sugirió Mikael). Voy a eliminar los comentarios que sugieran lo contrario ahora que la consulta contiene el derecho order byy espero que @JonSeigel considere hacer lo mismo.
Aaron Bertrand

6

Si puede usar CLR en su entorno, este es un caso personalizado para un agregado definido por el usuario.

En particular, este es probablemente el camino a seguir si los datos de origen no son trivialmente grandes y / o necesita hacer mucho este tipo de cosas en su aplicación. Sospecho firmemente que el plan de consulta para la solución de Aaron no escalará bien a medida que aumente el tamaño de entrada. (Intenté agregar un índice a la tabla temporal, pero eso no ayudó).

Esta solución, como muchas otras cosas, es una compensación:

  • Política / política para incluso usar CLR Integration en su entorno o el de su cliente.
  • La función CLR es probablemente más rápida y escalará mejor dado un conjunto real de datos.
  • La función CLR será reutilizable en otras consultas, y no tendrá que duplicar (y depurar) una subconsulta compleja cada vez que necesite hacer este tipo de cosas.
  • Straight T-SQL es más simple que escribir y administrar un fragmento de código externo.
  • Quizás no sabes cómo programar en C # o VB.
  • etc.

EDITAR: Bueno, fui a tratar de ver si esto era realmente mejor, y resulta que el requisito de que los comentarios estén en un orden específico actualmente no es posible satisfacerlos usando una función agregada. :(

Ver SqlUserDefinedAggregateAttribute.IsInvariantToOrder . Básicamente, lo que debe hacer es OVER(PARTITION BY customer_code ORDER BY row_num)pero ORDER BYno se admite en la OVERcláusula al agregar. Supongo que agregar esta funcionalidad a SQL Server abre una lata de gusanos, porque lo que debería cambiarse en el plan de ejecución es trivial. El enlace mencionado anteriormente dice que está reservado para uso futuro, por lo que esto podría implementarse en el futuro (sin embargo, en 2005 probablemente no tenga suerte).

Esto podría todavía ser realizado por el embalaje y analizar el row_numvalor agregado en la cadena, y luego hacer el tipo dentro del objeto CLR ... que parece bastante hacker.

En cualquier caso, a continuación se muestra el código que usé en caso de que alguien más lo encuentre útil, incluso con la limitación. Dejaré la parte de piratería como un ejercicio para el lector. Tenga en cuenta que utilicé AdventureWorks (2005) para los datos de prueba.

Asamblea 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 probar ( CREATE ASSEMBLYy sp_configurepara habilitar 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

1

Aquí hay una solución basada en el cursor que garantiza el orden de los comentarios row_num. (Vea mi otra respuesta sobre cómo se [dbo].[Comments]rellenó la tabla).

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
No has evitado un cursor. Acabas de llamar a tu cursor un bucle while.
Aaron Bertrand
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.