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.
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 .)
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:
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
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
-- 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