it-swarm.com.de

Kombinieren Sie Spalten aus mehreren Zeilen zu einer einzelnen Zeile

Ich habe einige customer_comments Aufgrund des Datenbankdesigns in mehrere Zeilen aufgeteilt, und für einen Bericht muss ich das comments aus jedem eindeutigen id in einer Zeile kombinieren. Ich habe zuvor versucht, mit diesem durch SELECT-Klausel und COALESCE getrennte Liste Trick zu arbeiten, aber ich kann mich nicht daran erinnern und muss ihn nicht gespeichert haben. Ich kann es auch in diesem Fall nicht zum Laufen bringen, 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_num Gibt es also wirklich nur eine Ergebnisreihe. Die Kommentare sollten in der Reihenfolge row_num kombiniert werden. Der oben verknüpfte Trick SELECT funktioniert, um alle Werte für eine bestimmte Abfrage als eine Zeile abzurufen, aber ich kann nicht herausfinden, wie sie als Teil einer SELECT -Anweisung funktioniert, die alle spuckt diese Zeilen aus.

Meine Abfrage muss die gesamte Tabelle alleine durchlaufen und diese Zeilen ausgeben. Ich kombiniere sie nicht in mehreren Spalten, eine für jede Zeile, daher scheint PIVOT nicht anwendbar zu sein.

14
Ben Brocka

Dies ist im Zusammenhang mit einer korrelierten Unterabfrage relativ trivial. Sie können die in dem von Ihnen erwähnten Blog-Beitrag hervorgehobene COALESCE-Methode 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, in dem die Daten in Kommentaren unsichere XML-Zeichen enthalten könnten (>, <, &) 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 varchar oder nvarchar und die richtige Länge verwenden und allen Zeichenfolgenliteralen N voranstellen, wenn Sie nvarchar verwenden .)

18
Aaron Bertrand

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

Insbesondere ist dies wahrscheinlich der richtige Weg, wenn die Quelldaten nicht trivial groß sind und/oder Sie diese Art von Dingen in Ihrer Anwendung häufig ausführen müssen. Ich vermute sehr, dass der Abfrageplan für Aarons Lösung nicht gut skaliert, 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:

  • Richtlinien/Richtlinien für die Verwendung von CLR-Integration in Ihrer oder der Umgebung Ihres Kunden.
  • Die CLR-Funktion ist wahrscheinlich schneller und lässt sich bei einem realen Datensatz besser skalieren.
  • Die CLR-Funktion kann in anderen Abfragen wiederverwendet werden, und Sie müssen nicht jedes Mal eine komplexe Unterabfrage duplizieren (und debuggen), wenn Sie diese Art von Dingen ausführen müssen.
  • Straight T-SQL ist einfacher als das Schreiben und Verwalten eines externen Codes.
  • Vielleicht wissen Sie nicht, wie man in C # oder VB programmiert.
  • usw.

EDIT: Nun, ich habe versucht zu sehen, ob dies tatsächlich besser ist, und es stellt sich heraus, dass die Kommentare derzeit in einer bestimmten Reihenfolge vorliegen müssen Mit einer Aggregatfunktion nicht zufriedenstellend. :(

Siehe SqlUserDefinedAggregateAttribute.IsInvariantToOrder . Grundsätzlich müssen Sie OVER(PARTITION BY customer_code ORDER BY row_num) aber ORDER BY wird beim Aggregieren in der Klausel OVER nicht unterstützt. Ich gehe davon aus, dass das Hinzufügen dieser Funktionalität zu SQL Server eine Dose Würmer öffnet, da das, was im Ausführungsplan geändert werden müsste, trivial ist. Der oben genannte Link besagt, dass dies für die zukünftige Verwendung reserviert ist, sodass dies in Zukunft implementiert werden könnte (im Jahr 2005 haben Sie jedoch wahrscheinlich kein Glück).

Dies könnte noch durch Packen und Parsen des row_num Wert in die aggregierte Zeichenfolge und dann die Sortierung innerhalb des CLR-Objekts ... was ziemlich hackisch erscheint.

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

Gesamtversammlung:

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 Assembly, und sp_configure zum Aktivieren der 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
6
Jon Seigel

Hier ist eine Cursor-basierte Lösung, die die Reihenfolge der Kommentare nach row_num. (Siehe meine andere Antwort für wie die [dbo].[Comments] Tabelle wurde ausgefüllt.)

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
1
Jon Seigel
-- 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
0
Gary