Combiner une colonne de plusieurs lignes en une seule ligne

14

J'en ai customer_commentsdivisé en plusieurs lignes en raison de la conception de la base de données, et pour un rapport, je dois combiner le commentsde chaque unique iden une seule ligne. J'ai déjà essayé quelque chose avec cette liste délimitée de la clause SELECT et de l' astuce COALESCE, mais je ne me souviens pas et je ne dois pas l'avoir enregistrée. Je n'arrive pas à le faire fonctionner dans ce cas non plus, semble seulement fonctionner sur une seule ligne.

Les données ressemblent à ceci:

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

Mes résultats doivent ressembler à ceci:

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

Donc, pour chacun, row_numil n'y a vraiment qu'une seule ligne de résultats; les commentaires doivent être combinés dans l'ordre de row_num. L' SELECTastuce liée ci-dessus fonctionne pour obtenir toutes les valeurs d'une requête spécifique sur une seule ligne, mais je ne sais pas comment la faire fonctionner dans le cadre d'une SELECTinstruction qui crache toutes ces lignes.

Ma requête doit parcourir toute la table seule et sortir ces lignes. Je ne les combine pas en plusieurs colonnes, une pour chaque ligne, donc PIVOTcela ne semble pas applicable.

Ben Brocka
la source

Réponses:

18

Ceci est relativement trivial pour une sous-requête corrélée. Vous ne pouvez pas utiliser la méthode COALESCE mise en évidence dans le billet de blog que vous mentionnez, sauf si vous extrayez cela dans une fonction définie par l'utilisateur (ou si vous ne souhaitez renvoyer qu'une ligne à la fois). Voici comment je fais généralement cela:

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 vous avez un cas où les données dans les commentaires peuvent contenir des caractères dangereux-pour-XML ( >, <, &), vous devez changer ceci:

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

Pour cette approche plus élaborée:

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

(Assurez-vous d'utiliser le bon type de données de destination, varcharou nvarchar, et la bonne longueur, et préfixez tous les littéraux de chaîne avec Nsi vous utilisez nvarchar.)

Aaron Bertrand
la source
3
+1 J'ai cré un violon pour ça pour un coup d'oeil sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal
3
Oui, cela fonctionne comme un charme. @MarlonRibunal SQL Fiddle se prépare vraiment!
Ben Brocka
@NickChammas - Je vais sortir mon cou et dire que la commande est garantie en utilisant le order bydans la sous-requête. C'est construire XML en utilisant for xmlet c'est la façon de construire XML en utilisant TSQL. L'ordre des éléments dans un fichier XML est une question importante et peut être invoquée. Donc, si cette technique ne garantit pas l'ordre, la prise en charge XML dans TSQL est gravement endommagée.
Mikael Eriksson
2
J'ai validé que la requête retournera les résultats dans le bon ordre, quel que soit l'index cluster sur la table sous-jacente (même un index cluster sur row_num descdoit obéir à ce order byque Mikael a suggéré). Je vais supprimer les commentaires suggérant le contraire maintenant que la requête contient le droit order byet j'espère que @JonSeigel envisage de faire de même.
Aaron Bertrand
6

Si vous êtes autorisé à utiliser CLR dans votre environnement, il s'agit d'un cas sur mesure pour un agrégat défini par l'utilisateur.

En particulier, c'est probablement la voie à suivre si les données source ne sont pas trivialement grandes et / ou si vous avez besoin de faire ce genre de choses beaucoup dans votre application. Je soupçonne fortement le plan de requête pour la solution d' Aaron pas bien à mesure que la taille d'entrée augmente. (J'ai essayé d'ajouter un index à la table temporaire, mais cela n'a pas aidé.)

Cette solution, comme bien d'autres choses, est un compromis:

  • Politique / politique pour même utiliser l'intégration CLR dans votre environnement ou celui de votre client.
  • La fonction CLR est probablement plus rapide et évoluera mieux compte tenu d'un ensemble réel de données.
  • La fonction CLR sera réutilisable dans d'autres requêtes, et vous n'aurez pas à dupliquer (et déboguer) une sous-requête complexe à chaque fois que vous devez faire ce type de chose.
  • Straight T-SQL est plus simple que d'écrire et de gérer un morceau de code externe.
  • Peut-être que vous ne savez pas comment programmer en C # ou VB.
  • etc.

EDIT: Eh bien, je suis allé essayer de voir si c'était vraiment mieux, et il s'avère que l'exigence que les commentaires soient dans un ordre spécifique n'est actuellement pas possible de satisfaire en utilisant une fonction d'agrégation. :(

Voir SqlUserDefinedAggregateAttribute.IsInvariantToOrder . Fondamentalement, ce que vous devez faire est OVER(PARTITION BY customer_code ORDER BY row_num)mais ORDER BYn'est pas pris en charge dans leOVER clause lors de l'agrégation. Je suppose que l'ajout de cette fonctionnalité à SQL Server ouvre une boîte de vers, car ce qui devrait être modifié dans le plan d'exécution est trivial. Le lien susmentionné indique que cela est réservé pour une utilisation future, donc cela pourrait être implémenté à l'avenir (en 2005, vous n'avez probablement pas de chance, cependant).

Cela pourrait encore être accompli en emballant et en analysantrow_num valeur dans la chaîne agrégée, puis en effectuant le tri dans l'objet CLR ... ce qui semble assez hackish.

Dans tous les cas, voici le code que j'ai utilisé au cas où quelqu'un d'autre trouverait cela utile même avec la limitation. Je vais laisser la partie de piratage comme un exercice pour le lecteur. Notez que j'ai utilisé AdventureWorks (2005) pour les données de test.

Assemblage d'agrégats:

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 pour tester ( CREATE ASSEMBLY, et sp_configurepour activer CLR omis):

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
la source
1

Voici une solution basée sur un curseur qui garantit l'ordre des commentaires par row_num. (Voir mon autre réponse pour savoir comment le [dbo].[Comments]tableau a été rempli.)

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
la source
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
la source
2
Vous n'avez pas évité un curseur. Vous venez d'appeler votre curseur une boucle while à la place.
Aaron Bertrand