Combina colonna da più righe in un'unica riga


14

Ho un po ' customer_commentssuddiviso in più righe a causa della progettazione del database e per un rapporto ho bisogno di combinare il commentsda ogni unico idin una riga. In precedenza ho provato a lavorare con questo elenco delimitato dalla clausola SELECT e dal trucco COALESCE, ma non riesco a ricordarlo e non devo averlo salvato. Non riesco nemmeno a farlo funzionare in questo caso, sembra funzionare solo su una singola riga.

I dati si presentano così:

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

I miei risultati devono apparire così:

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

Quindi per ognuno row_numc'è davvero solo una riga di risultati; i commenti dovrebbero essere combinati nell'ordine di row_num. Il SELECTtrucco collegato sopra funziona per ottenere tutti i valori di una query specifica come una riga, ma non riesco a capire come farlo funzionare come parte di SELECTun'istruzione che sputa tutte queste righe.

La mia query deve passare da sola sull'intera tabella e generare queste righe. Non le sto combinando in più colonne, una per ogni riga, quindi PIVOTnon sembra applicabile.

Risposte:


18

Questo è relativamente banale da fare con una sottoquery correlata. Non è possibile utilizzare il metodo COALESCE evidenziato nel post di blog menzionato se non lo si estrae in una funzione definita dall'utente (o se non si desidera restituire solo una riga alla volta). Ecco come faccio in genere questo:

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 si dispone di un caso in cui i dati nei commenti potrebbero contenere caratteri non sicuri-per-XML ( >, <, &), è necessario modificare questo:

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

A questo approccio più elaborato:

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

(Assicurati di usare il giusto tipo di dati di destinazione, varcharo nvarchar, e la giusta lunghezza, e aggiungi il prefisso a tutti i valori letterali delle stringhe Nse usi nvarchar.)


3
+1 Ho creato un violino per questo per una rapida occhiata sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal

3
Sì, funziona come un fascino. @MarlonRibunal SQL Fiddle sta davvero dando forma!
Ben Brocka,

@NickChammas - Ho intenzione di sporgere il collo e dire che l'ordine è garantito usando order byla sotto query. Questo sta costruendo XML usando for xmle questo è il modo per costruire XML usando TSQL. L'ordine degli elementi in un file XML è una questione importante e su cui si può fare affidamento. Quindi se questa tecnica non garantisce l'ordine, il supporto XML in TSQL è gravemente rotto.
Mikael Eriksson,

2
Ho convalidato che la query restituirà i risultati nell'ordine corretto indipendentemente dall'indice cluster sulla tabella sottostante (anche un indice cluster su row_num descdeve obbedire order bycome suggerito da Mikael). Rimuoverò i commenti che suggeriscono altrimenti ora che la query contiene il diritto order bye spero che @JonSeigel consideri di fare lo stesso.
Aaron Bertrand

6

Se ti è permesso usare CLR nel tuo ambiente, questo è un caso su misura per un aggregato definito dall'utente.

In particolare, questo è probabilmente il modo di procedere se i dati di origine sono di dimensioni non banali e / o se è necessario eseguire molto questo tipo di cose nella propria applicazione. Sospetto fortemente che il piano di query per la soluzione di Aaron non si ridimensioni bene con l'aumentare della dimensione dell'input. (Ho provato ad aggiungere un indice alla tabella temporanea, ma non è stato d'aiuto.)

Questa soluzione, come molte altre cose, è un compromesso:

  • Politica / politica per l'uso uniforme dell'integrazione CLR nel proprio ambiente o client.
  • La funzione CLR è probabilmente più veloce e si ridimensionerà meglio dato un insieme reale di dati.
  • La funzione CLR sarà riutilizzabile in altre query e non sarà necessario duplicare (ed eseguire il debug) di una subquery complessa ogni volta che è necessario eseguire questo tipo di cose.
  • Straight T-SQL è più semplice che scrivere e gestire un pezzo di codice esterno.
  • Forse non sai come programmare in C # o VB.
  • eccetera.

EDIT: Beh, sono andato a cercare di vedere se questo fosse effettivamente meglio, e risulta che il requisito per cui i commenti siano in un ordine specifico non è attualmente possibile soddisfare utilizzando una funzione aggregata. :(

Vedi SqlUserDefinedAggregateAttribute.IsInvariantToOrder . Fondamentalmente, ciò che devi fare è OVER(PARTITION BY customer_code ORDER BY row_num)ma ORDER BYnon è supportato nella OVERclausola durante l'aggregazione. Suppongo che l'aggiunta di questa funzionalità a SQL Server apra una lattina di worm, perché ciò che dovrebbe essere modificato nel piano di esecuzione è banale. Il link di cui sopra dice che è riservato per un uso futuro, quindi potrebbe essere implementato in futuro (nel 2005 probabilmente sei sfortunato).

Questo potrebbe ancora essere realizzato impacchettando e analizzando il row_numvalore nella stringa aggregata, e quindi facendo l'ordinamento all'interno dell'oggetto CLR ... che sembra piuttosto hacker.

In ogni caso, di seguito è riportato il codice che ho usato nel caso in cui qualcun altro lo ritenga utile anche con la limitazione. Lascerò la parte di hacking come esercizio per il lettore. Si noti che ho usato AdventureWorks (2005) per i dati di test.

Assemblaggio aggregato:

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 per il test ( CREATE ASSEMBLYe sp_configureper abilitare CLR omesso):

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

Ecco una soluzione basata sul cursore che garantisce l'ordine dei commenti per row_num. (Vedi la mia altra risposta per come è [dbo].[Comments]stata popolata la tabella.)

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
Non hai evitato un cursore. Invece hai appena chiamato il cursore un ciclo while.
Aaron Bertrand
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.