J'en ai customer_comments
divisé en plusieurs lignes en raison de la conception de la base de données, et pour un rapport, je dois combiner le comments
de chaque id
unique en une seule ligne. J'ai déjà essayé quelque chose avec cette astuce liste délimitée de la clause SELECT et COALESCE mais je ne m'en souviens pas et je ne dois pas l'avoir enregistrée. Je n'arrive pas à le faire fonctionner dans ce cas non plus, ne semble fonctionner que 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 chaque row_num
il n'y a vraiment qu'une seule ligne de résultats; les commentaires doivent être combinés dans l'ordre de row_num
. L'astuce SELECT
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 instruction SELECT
qui crache tout ces rangées.
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 PIVOT
ne semble pas applicable.
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 modifier 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, varchar
ou nvarchar
, et la bonne longueur, et préfixez tous les littéraux de chaîne avec N
si vous utilisez nvarchar
.)
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 que le plan de requête pour la solution d'Aaron n'évolue 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:
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 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 BY
n'est pas pris en charge dans la clause OVER
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).
Ceci pourrait toujours être accompli en compressant et en analysant le row_num
valeur dans la chaîne agrégée, puis faire 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_configure
pour activer le 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
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]
la table a été remplie.)
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