Kombinieren Sie die Spalte aus mehreren Zeilen zu einer einzelnen Zeile

14

Ich habe einige customer_commentsaufgrund des Datenbankdesigns in mehrere Zeilen aufgeteilt, und für einen Bericht muss ich die commentseinzelnen Elemente idin einer Zeile kombinieren . Ich habe zuvor versucht, mit dieser abgegrenzten Liste aus der SELECT-Klausel und dem COALESCE- Trick etwas zu arbeiten , kann mich aber nicht daran erinnern und muss sie nicht gespeichert haben. Ich kann es auch in diesem Fall nicht zum Laufen bringen, es scheint nur in einer einzelnen Zeile zu funktionieren.

Die Daten sehen folgendermaßen aus:

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

Meine Ergebnisse müssen so aussehen:

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

Für jeden row_numgibt es also nur eine Reihe von Ergebnissen. Die Kommentare sollten in der Reihenfolge von zusammengefasst werden row_num. Der oben genannte verknüpfte SELECTTrick funktioniert, um alle Werte für eine bestimmte Abfrage als eine Zeile abzurufen, aber ich kann nicht herausfinden, wie er als Teil einer SELECTAnweisung funktioniert, die alle diese Zeilen ausspuckt.

Meine Abfrage muss die gesamte Tabelle alleine durchgehen und diese Zeilen ausgeben. Ich kombiniere sie nicht in mehrere Spalten, eine für jede Zeile, also PIVOTscheint dies nicht zutreffend zu sein.

Ben Brocka
quelle

Antworten:

18

Dies ist im Zusammenhang mit einer korrelierten Unterabfrage relativ trivial. Sie können die COALESCE-Methode, die in dem von Ihnen erwähnten Blog-Beitrag hervorgehoben ist, nur verwenden, wenn Sie sie in eine benutzerdefinierte Funktion extrahieren (oder wenn Sie jeweils nur eine Zeile zurückgeben möchten). So mache ich das normalerweise:

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;

Wenn Sie einen Fall haben , wo die Daten in den Kommentaren unsicher-for-XML - Zeichen enthalten können ( >, <, &), sollten Sie dies ändern:

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

Zu diesem ausgefeilteren Ansatz:

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

(Stellen Sie sicher, dass Sie den richtigen Zieldatentyp varcharoder nvarcharund die richtige Länge verwenden und allen Zeichenfolgenliteralen das Präfix " Nif using" voranstellen nvarchar.)

Aaron Bertrand
quelle
3
+1 Ich habe eine Fiedel für das für einen kurzen Blick sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal
3
Ja, das funktioniert wie ein Zauber. @MarlonRibunal SQL Fiddle entwickelt sich wirklich!
Ben Brocka
@ NickChammas - Ich werde meinen Hals rausstrecken und sagen, dass die Bestellung mit der order byin der Unterabfrage garantiert ist . Dies ist das Erstellen von XML mit for xmlund das ist der Weg, XML mit TSQL zu erstellen. Die Reihenfolge der Elemente in XML-Dateien ist eine wichtige Angelegenheit, auf die Sie sich verlassen können. Wenn diese Technik die Reihenfolge nicht garantiert, ist die XML-Unterstützung in TSQL schwer beschädigt.
Mikael Eriksson
2
Ich habe überprüft, ob die Abfrage Ergebnisse in der richtigen Reihenfolge zurückgibt, unabhängig vom Clustered-Index für die zugrunde liegende Tabelle (selbst ein Clustered-Index für row_num descmuss den order byvon Mikael vorgeschlagenen Werten entsprechen). Ich werde Kommentare entfernen, die andernfalls vorschlagen, dass die Abfrage das Richtige enthält, order byund hoffe, dass @JonSeigel das Gleiche erwägt.
Aaron Bertrand
6

Wenn Sie CLR in Ihrer Umgebung verwenden dürfen, ist dies ein maßgeschneiderter Fall für ein benutzerdefiniertes Aggregat.

Dies ist wahrscheinlich der richtige Weg, wenn die Quelldaten nicht trivial groß sind und / oder Sie in Ihrer Anwendung häufig solche Aufgaben ausführen müssen. Ich vermute sehr, dass der Abfrageplan für Aarons Lösung nicht gut skaliert wird, wenn die Eingabegröße zunimmt. (Ich habe versucht, der temporären Tabelle einen Index hinzuzufügen, aber das hat nicht geholfen.)

Diese Lösung ist, wie viele andere Dinge, ein Kompromiss:

  • Politik / Richtlinie für die Verwendung der CLR-Integration in Ihrer oder der Umgebung Ihres Kunden.
  • Die CLR-Funktion ist wahrscheinlich schneller und skaliert bei einem realen Datensatz besser.
  • Die CLR-Funktion kann in anderen Abfragen wiederverwendet werden, und Sie müssen eine komplexe Unterabfrage nicht jedes Mal duplizieren (und debuggen), wenn Sie dies tun müssen.
  • Straight T-SQL ist einfacher als das Schreiben und Verwalten von externem Code.
  • Vielleicht wissen Sie nicht, wie man in C # oder VB programmiert.
  • etc.

EDIT: Nun, ich habe versucht herauszufinden, ob dies tatsächlich besser ist, und es stellt sich heraus, dass die Anforderungen, dass die Kommentare in einer bestimmten Reihenfolge vorliegen, derzeit mit einer Aggregatfunktion nicht erfüllt werden können. :(

Siehe SqlUserDefinedAggregateAttribute.IsInvariantToOrder . Im Grunde, was Sie tun müssen , ist OVER(PARTITION BY customer_code ORDER BY row_num)aber ORDER BYnicht in der unterstützte OVERKlausel , wenn die Aggregation. Ich gehe davon aus, dass das Hinzufügen dieser Funktionalität zu SQL Server eine Dose Würmer öffnet, da es trivial ist, was im Ausführungsplan geändert werden müsste. Der oben genannte Link besagt, dass dies für die zukünftige Verwendung reserviert ist, so dass dies in Zukunft implementiert werden könnte (2005 haben Sie jedoch wahrscheinlich kein Glück).

Dies könnte immer noch erreicht werden, indem der row_numWert in die aggregierte Zeichenfolge gepackt und analysiert wird und dann die Sortierung innerhalb des CLR-Objekts durchgeführt wird ... was ziemlich hackisch erscheint.

In jedem Fall ist unten der Code aufgeführt, den ich verwendet habe, falls jemand dies trotz der Einschränkung für nützlich hält. Ich lasse den Hacking-Teil als Übung für den Leser. Beachten Sie, dass ich AdventureWorks (2005) für Testdaten verwendet habe.

Aggregatmontage:

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 zum Testen ( CREATE ASSEMBLYund sp_configurezum Aktivieren von CLR weggelassen):

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
quelle
1

Hier ist eine Cursor-basierte Lösung, die die Reihenfolge der Kommentare nach garantiert row_num. (Sehen Sie sich meine andere Antwort an, um zu erfahren, wie die [dbo].[Comments]Tabelle gefüllt wurde.)

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
quelle
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
quelle
2
Sie haben einen Cursor nicht gemieden. Sie haben Ihren Cursor stattdessen nur eine while-Schleife genannt.
Aaron Bertrand