SELECT CategoryId, ProductName
FROM Northwind..Products ;
CategoryId ProductName
----------- ----------------------------------------
1 Chai
1 Chang
...
2 Aniseed Syrup
2 Chef Anton's Cajun Seasoning
...
...
8 Spegesild
(77 row(s) affected)
The goal is to return a resultset with two columns one with the Category Identifier and the other with a concatenated list of all the Product Names separated by a delimiting character, say a comma for instance. CategoryId Product List
----------- -----------------------------------------------------------------------------
1 Chai, Chang, Chartreuse verte, Côte de Blaye, ...
2 Aniseed Syrup, Chef Anton's Cajun Seasoning, ...
3 Chocolade, Gumbär Gummibärchen, Maxilaku, ...
4 Camembert Pierrot, Flotemysost, Geitost, Gorgonzola Telino, ...
5 Filo Mix, Gnocchi di nonna Alice, Gustaf's Knäckebröd, ...
6 Alice Mutton, Mishi Kobe Niku, Pâté chinois, ...
7 Longlife Tofu, Manjimup Dried Apples, Rössle Sauerkraut, ...
8 Boston Crab Meat, Carnarvon Tigers, Escargots de Bourgogne, ...
(8 row(s) affected)
Concatenating values when the number of items is small and known upfront SELECT CategoryId,
MAX( CASE seq WHEN 1 THEN ProductName ELSE '' END ) + ', ' +
MAX( CASE seq WHEN 2 THEN ProductName ELSE '' END ) + ', ' +
MAX( CASE seq WHEN 3 THEN ProductName ELSE '' END ) + ', ' +
MAX( CASE seq WHEN 4 THEN ProductName ELSE '' END )
FROM ( SELECT p1.CategoryId, p1.ProductName,
( SELECT COUNT(*)
FROM Northwind.dbo.Products p2
WHERE p2.CategoryId = p1.CategoryId
AND p2.ProductName <= p1.ProductName )
FROM Northwind.dbo.Products p1 ) D ( CategoryId, ProductName, seq )
GROUP BY CategoryId ;
The idea above is to create a expression inside the correlated subquery that produces a rank (seq) based on the product names and then use it in the outer query. Using common table expressions and the ROW_NUMBER() function, you can re-write this as: ; WITH CTE ( CategoryId, ProductName, seq )
AS ( SELECT p1.CategoryId, p1.ProductName,
ROW_NUMBER() OVER ( PARTITION BY CategoryId ORDER BY ProductName )
FROM Northwind.dbo.Products p1 )
SELECT CategoryId,
MAX( CASE seq WHEN 1 THEN ProductName ELSE '' END ) + ', ' +
MAX( CASE seq WHEN 2 THEN ProductName ELSE '' END ) + ', ' +
MAX( CASE seq WHEN 3 THEN ProductName ELSE '' END ) + ', ' +
MAX( CASE seq WHEN 4 THEN ProductName ELSE '' END )
FROM CTE
GROUP BY CategoryId ;
Note that ROW_NUMBER() is a newly introduced feature in SQL 2005. If you are using any previous versions, you will have to use the subquery approach (You can also use a self-join, to write it a bit differently). Using the recently introduced PIVOT operator, you can write the above as following : SELECT CategoryId,
"1" + ', ' + "2" + ', ' + "3" + ', ' + "4" AS Product_List
FROM ( SELECT CategoryId, ProductName,
ROW_NUMBER() OVER (PARTITION BY CategoryId ORDER BY ProductName)
FROM Northwind.dbo.Products ) P ( CategoryId, ProductName, seq )
PIVOT ( MAX( ProductName ) FOR seq IN ( "1", "2", "3", "4" ) ) AS P_ ;
Not only the syntax appears a bit confusing, it does not appear to offer anything functionally beyond the CASE approach above. However, in rare situations, it could come in handy. WITH CTE ( CategoryId, product_list, product_name, length )
AS ( SELECT CategoryId, CAST( '' AS VARCHAR(8000) ), CAST( '' AS VARCHAR(8000) ), 0
FROM Northwind..Products
GROUP BY CategoryId
UNION ALL
SELECT p.CategoryId, CAST( product_list +
CASE WHEN length = 0 THEN '' ELSE ', ' END + ProductName AS VARCHAR(8000) ),
CAST( ProductName AS VARCHAR(8000)), length + 1
FROM CTE c
INNER JOIN Northwind..Products p
ON c.CategoryId = p.CategoryId
WHERE p.ProductName > c.product_name )
SELECT CategoryId, product_list
FROM ( SELECT CategoryId, product_list,
RANK() OVER ( PARTITION BY CategoryId ORDER BY length DESC )
FROM CTE ) D ( CategoryId, product_list, rank )
WHERE rank = 1 ;
The CASE in the recursive part of the CTE is used to eliminate the initial comma and you can use RIGHT or the SUBSTRING functions to substitute it. Also, this may not be the best performing option, however certain additional tuning could be done to make them suitable for medium sized datasets. WITH Ranked ( CategoryId, rnk, ProductName )
AS ( SELECT CategoryId,
ROW_NUMBER() OVER( PARTITION BY CategoryId ORDER BY CategoryId ),
CAST( ProductName AS VARCHAR(8000) )
FROM Northwind..Products),
AnchorRanked ( CategoryId, rnk, ProductName )
AS ( SELECT CategoryId, rnk, ProductName
FROM Ranked
WHERE rnk = 1 ),
RecurRanked ( CategoryId, rnk, ProductName )
AS ( SELECT CategoryId, rnk, ProductName
FROM AnchorRanked
UNION ALL
SELECT Ranked.CategoryId, Ranked.rnk,
RecurRanked.ProductName + ', ' + Ranked.ProductName
FROM Ranked
INNER JOIN RecurRanked
ON Ranked.CategoryId = RecurRanked.CategoryId
AND Ranked.rnk = RecurRanked.rnk + 1 )
SELECT CategoryId, MAX( ProductName )
FROM RecurRanked
GROUP BY CategoryId;
On an initial glance, this query may seem a bit expensive in comparison, however the reader is encouraged check the execution plans and make any additional tweaks as needed. SELECT p1.CategoryId,
( SELECT ProductName + ','
FROM Northwind.dbo.Products p2
WHERE p2.CategoryId = p1.CategoryId
ORDER BY ProductName
FOR XML PATH('') ) AS Products
FROM Northwind.dbo.Products p1
GROUP BY CategoryId ;
Again, the similar approach originally found in the beta newsgroups, using CROSS APPLY operator. SELECT DISTINCT CategoryId, ProductNames
FROM Products p1
CROSS APPLY ( SELECT ProductName + ','
FROM Products p2
WHERE p2.CategoryId = p1.CategoryId
ORDER BY ProductName
FOR XML PATH('') ) D ( ProductNames )
You may notice a comma at the end of the concatenated string, which you can remove using a STUFF, SUBSTRING or LEFT function. While the above methods are deemed reliable by many at the time of writing, there is no guarantee that it will stay that way given the internal workings and evaluation rules of FOR XML PATH() expression in correlated subqueries are not well documented. using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.IO;
using Microsoft.SqlServer.Server;
[Serializable]
[SqlUserDefinedAggregate(Format.UserDefined, MaxByteSize=8000)]
public struct strconcat : IBinarySerialize{
private List values;
public void Init() {
this.values = new List ();
}
public void Accumulate(SqlString value) {
this.values.Add(value.Value);
}
public void Merge(strconcat value) {
this.values.AddRange(value.values.ToArray());
}
public SqlString Terminate() {
return new SqlString(string.Join(", ", this.values.ToArray()));
}
public void Read(BinaryReader r) {
int itemCount = r.ReadInt32();
this.values = new List (itemCount);
for (int i = 0; i <= itemCount - 1; i++) {
this.values.Add(r.ReadString());
}
}
public void Write(BinaryWriter w) {
w.Write(this.values.Count);
foreach (string s in this.values) {
w.Write(s);
}
}
}
Once you build and deploy this assembly on the server, you should be able to execute your concatenation query as: SELECT CategoryId,
dbo.strconcat(ProductName)
FROM Products
GROUP BY CategoryId ;
If you are a total newbie on CLR languages, and would like to learn more about developing database solutions using CLR languages, consider starting at Introduction to Common Language Runtime (CLR) Integration CREATE FUNCTION udf_recursive ( @cid INT, @i INT )
RETURNS VARCHAR(8000) AS BEGIN
DECLARE @r VARCHAR(8000), @l VARCHAR(8000)
SELECT @i = @i - 1, @r = ProductName + ', '
FROM Products p1
WHERE CategoryId = @cid
AND @i = ( SELECT COUNT(*) FROM Products p2
WHERE p2.CategoryId = p1.CategoryId
AND p2.ProductName <= p1.ProductName ) ;
IF @i > 0 BEGIN
EXEC @l = dbo.udf_recursive @cid, @i ;
SET @r = @l + @r ;
END
RETURN @r ;
END
This function can be invoked as follows: SELECT CategoryId,
dbo.udf_recursive( CategoryId, COUNT(ProductName) )
FROM Products
GROUP BY CategoryId ;
Table valued UDF with a WHILE loop CREATE FUNCTION udf_tbl_Concat() RETURNS @t TABLE(
CategoryId INT,
Product VARCHAR(40),
list VARCHAR(8000) )
BEGIN
INSERT @t (CategoryId, Product, list)
SELECT CategoryId, MIN(ProductName), MIN(ProductName)
FROM Products
GROUP BY CategoryId
WHILE ( SELECT COUNT(Product) FROM @t ) > 0 BEGIN
UPDATE t
SET list = list + COALESCE(
( SELECT ', ' + MIN( ProductName )
FROM Products
WHERE Products.CategoryId = t.CategoryId
AND Products.ProductName > t.Product), ''),
Product = ( SELECT MIN(ProductName)
FROM Products
WHERE Products.CategoryId = t.CategoryId
AND Products.ProductName > t.Product )
FROM @t t END
RETURN
END
The usage of the above function can be like: SELECT CategoryId, list AS Products
FROM udf_tbl_Concat() ;
Dynamic SQL DECLARE @r VARCHAR(MAX), @n INT, @i INT
SELECT @i = 1,
@r = 'SELECT CategoryId, ' + CHAR(13),
@n = (SELECT TOP 1 COUNT( ProductName )
FROM Products
GROUP BY CategoryId
ORDER BY COUNT( ProductName ) DESC ) ;
WHILE @i <= @n BEGIN
SET @r = @r +
CASE WHEN @i = 1
THEN 'MAX( CASE Seq WHEN ' + CAST( @i AS VARCHAR ) + '
THEN ProductName
ELSE SPACE(0) END ) + ' + CHAR(13)
WHEN @i = @n
THEN 'MAX( CASE Seq WHEN ' + CAST( @i AS VARCHAR ) + '
THEN '', '' + ProductName
ELSE SPACE(0) END ) ' + CHAR(13)
ELSE 'MAX( CASE Seq WHEN ' + CAST( @i AS VARCHAR ) + '
THEN '', '' + ProductName
ELSE SPACE(0) END ) + ' + CHAR(13)
END ;
SET @i = @i + 1 ;
END
SET @r = @r + '
FROM ( SELECT CategoryId, ProductName,
ROW_NUMBER() OVER ( PARTITION BY CategoryId ORDER BY ProductName )
FROM Products p ) D ( CategoryId, ProductName, Seq )
GROUP BY CategoryId;'
EXEC( @r ) ;
The Cursor approach DECLARE @tbl TABLE (id INT PRIMARY KEY, list VARCHAR(8000))
SET NOCOUNT ON
DECLARE @c INT, @p VARCHAR(8000), @cNext INT, @pNext VARCHAR(40)
DECLARE c CURSOR FOR
SELECT CategoryId, ProductName
FROM Products
ORDER BY CategoryId, ProductName ;
OPEN c ;
FETCH NEXT FROM c INTO @cNext, @pNext ;
SET @c = @cNext ;
WHILE @@FETCH_STATUS = 0 BEGIN
IF @cNext > @c BEGIN
INSERT @tbl SELECT @c, @p ;
SELECT @p = @PNext, @c = @cNext ;
END ELSE
SET @p = COALESCE(@p + ',', SPACE(0)) + @pNext ;
FETCH NEXT FROM c INTO @cNext, @pNext
END
INSERT @tbl SELECT @c, @p ;
CLOSE c ;
DEALLOCATE c ;
SELECT * FROM @tbl ;
Non-reliable approaches CREATE FUNCTION udf_update_concat (@CategoryId INT)
RETURNS VARCHAR(MAX) AS
BEGIN
DECLARE @t TABLE(p VARCHAR(40));
DECLARE @r VARCHAR(MAX) ;
SET @r = SPACE(0) ;
INSERT @t ( p ) SELECT ProductName FROM Products
WHERE CategoryId = @CategoryId ;
IF @@ROWCOUNT > 0
UPDATE @t
SET @r = @r + p + ',' ;
RETURN(@r)
END
Here is how to use this function: SELECT CategoryId, dbo.udf_update_concat(CategoryId)
FROM Products
GROUP BY CategoryId ;
Again, it is important to consider that lack of physical independence that is being exploited here before using or recommending this as a usable and meaningful solution. CREATE FUNCTION dbo.udf_select_concat ( @c INT )
RETURNS VARCHAR(MAX) AS BEGIN
DECLARE @p VARCHAR(MAX) ;
SET @p = '' ;
SELECT @p = @p + ProductName + ','
FROM Products
WHERE CategoryId = @c ;
RETURN @p
END
And, as for its usage: SELECT CategoryId, dbo.udf_select_concat( CategoryId )
FROM Products
GROUP BY CategoryId ;
[@more@]
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/7667023/viewspace-1007506/ ,如需转载,请注明出处,否则将追究法律责任。