85

Possible Duplicates:
Implode type function in SQL Server 2000?
Concatenate row values T-SQL

I have a view which I'm querying that looks like this:

BuildingName    PollNumber
------------    ----------
Foo Centre      12        
Foo Centre      13
Foo Centre      14
Bar Hall        15
Bar Hall        16
Baz School      17

I need to write a query that groups BuildingNames together and displays a list of PollNumbers like this:

BuildingName    PollNumbers
------------    -----------
Foo Centre      12, 13, 14
Bar Hall        15, 16
Baz School      17

How can I do this in T-SQL? I'd rather not resort to writing a stored procedure for this, since it seems like overkill, but I'm not exactly a database person. It seems like an aggregate function like SUM() or AVG() is what I need, but I don't know if T-SQL has one. I'm using SQL Server 2005.

Community
  • 1
  • 1
Brant Bobby
  • 14,956
  • 14
  • 78
  • 115
  • Yes, this question has been asked multiple times on SO. http://stackoverflow.com/questions/1874966/concatenate-row-values-t-sql or http://stackoverflow.com/questions/3121079/how-to-concatenate-multiple-rows – Lamak Feb 17 '11 at 15:53
  • Ah, my bad. Chalk this up as a case of not using the right search keywords then. :) Voting to close. – Brant Bobby Feb 17 '11 at 15:58
  • answered many times... but watch out, not all `FOR XML PATH concatenations` implementations will properly handle the XML special characters (`<`, `&`, `>`, etc) like my sample code (below) will... – KM. Feb 17 '11 at 16:02
  • Also a duplicate of: http://stackoverflow.com/questions/273238/how-to-use-group-by-to-concatenate-strings-in-sql-server – Danny Varod Mar 07 '13 at 12:48
  • 5
    SqlServer 2017 now has [STRING_AGG](https://learn.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql) that aggregates multiple strings into one using a given separator. – John Aug 31 '17 at 10:42

2 Answers2

129

for SQL Server 2017 and up use:

STRING_AGG()

set nocount on;
declare @YourTable table (RowID int, HeaderValue int, ChildValue varchar(5))
insert into @YourTable VALUES (1,1,'CCC')
insert into @YourTable VALUES (2,2,'B<&>B')
insert into @YourTable VALUES (3,2,'AAA')
insert into @YourTable VALUES (4,3,'<br>')
insert into @YourTable VALUES (5,3,'A & Z')
set nocount off
SELECT
    t1.HeaderValue
        ,STUFF(
                   (SELECT
                        ', ' + t2.ChildValue
                        FROM @YourTable t2
                        WHERE t1.HeaderValue=t2.HeaderValue
                        ORDER BY t2.ChildValue
                        FOR XML PATH(''), TYPE
                   ).value('.','varchar(max)')
                   ,1,2, ''
              ) AS ChildValues
    FROM @YourTable t1
    GROUP BY t1.HeaderValue

SELECT
    HeaderValue, STRING_AGG(ChildValue,', ')
    FROM @YourTable
    GROUP BY HeaderValue

OUTPUT:

HeaderValue 
----------- -------------
1           CCC
2           B<&>B, AAA
3           <br>, A & Z

(3 rows affected)

for SQL Server 2005 and up to 2016, you need to do something like this:

--Concatenation with FOR XML and eleminating control/encoded character expansion "& < >"
set nocount on;
declare @YourTable table (RowID int, HeaderValue int, ChildValue varchar(5))
insert into @YourTable VALUES (1,1,'CCC')
insert into @YourTable VALUES (2,2,'B<&>B')
insert into @YourTable VALUES (3,2,'AAA')
insert into @YourTable VALUES (4,3,'<br>')
insert into @YourTable VALUES (5,3,'A & Z')
set nocount off
SELECT
    t1.HeaderValue
        ,STUFF(
                   (SELECT
                        ', ' + t2.ChildValue
                        FROM @YourTable t2
                        WHERE t1.HeaderValue=t2.HeaderValue
                        ORDER BY t2.ChildValue
                        FOR XML PATH(''), TYPE
                   ).value('.','varchar(max)')
                   ,1,2, ''
              ) AS ChildValues
    FROM @YourTable t1
    GROUP BY t1.HeaderValue

OUTPUT:

HeaderValue ChildValues
----------- -------------------
1           CCC
2           AAA, B<&>B
3           <br>, A & Z

(3 row(s) affected)

Also, watch out, not all FOR XML PATH concatenations will properly handle XML special characters like my above example will.

KM.
  • 101,727
  • 34
  • 178
  • 212
  • Thanks for this - the varchar (or nvarchar) conversion is almost always overlooked in examples of this "trick" – Tao Oct 16 '13 at 17:26
  • I red about using ('(./text())[1]','varchar(max)') rather ('.','varchar(max)') but i don't know what is point?! You can view this: http://stackoverflow.com/questions/273238/how-to-use-group-by-to-concatenate-strings-in-sql-server – QMaster Feb 19 '14 at 09:44
  • @QMaster see: http://social.msdn.microsoft.com/Forums/sqlserver/en-US/c4d29985-4cef-4811-8d68-c4d3fa9365ca/using-text-directive-with-for-xml-path?forum=sqlxml – KM. Feb 19 '14 at 13:44
  • Great post, thanks for pointing out the handling of special XML characters. –  Mar 13 '15 at 08:55
  • 8
    `.value('(./text())[1]', 'nvarchar(max)')` performs significantly better than `.value('.', 'nvarchar(max)')`, in my tests by 13.2x. – jnm2 Mar 30 '16 at 14:33
  • 1
    It looks like there might be a function called STRING_AGG in version 2017 for this? – user3334690 Jul 03 '18 at 17:13
  • @user3334690: For me this was the most helpful information. I'm trying SQL Server 2017 for a while, now and just came across this problem. It worked instantly. For the sake of the actual question: syntax would be `SELECT BuildingName, STRING_AGG(PollNumber, ', ') FROM YourTable GROUP BY BuildingName`. Thanks :) – Chris Tophski Jul 16 '18 at 22:03
  • The STUFF works but it's is horribly slow! I can't believe there's no analytical function in SQL Server 2016 and earlier to handle this. – Koshera Oct 02 '19 at 22:35
36

There is no built in function in Sql Server, but it can be achieved by writing a user defined aggregate. This article mentions such a function as part of the SQL Server samples: http://msdn.microsoft.com/en-us/library/ms182741.aspx

As an example I include the code for a Concatenate aggregate. To use it, create a database project in Visual Studio, add new SqlAggregate and replace the code with the sample below. Once deployed you should find a new assembly in your database and an aggregate function Concatenate

using System;
using System.Data.SqlTypes;
using System.IO;
using System.Text;
using Microsoft.SqlServer.Server;

[Serializable]
[SqlUserDefinedAggregate(Format.UserDefined, IsInvariantToNulls = true, IsInvariantToDuplicates = false, IsInvariantToOrder = false, MaxByteSize = 8000, Name = "Concatenate")]
public class Concatenate : IBinarySerialize
{
    private StringBuilder _intermediateResult;

    internal string IntermediateResult {
        get
        {
            return _intermediateResult.ToString();
        } 
    }

    public void Init()
    {
        _intermediateResult = new StringBuilder();
    }

    public void Accumulate(SqlString value)
    {
        if (value.IsNull) return;
        _intermediateResult.Append(value.Value);
    }

    public void Merge(Concatenate other)
    {
        if (null == other)
            return;

        _intermediateResult.Append(other._intermediateResult);
    }

    public SqlString Terminate()
    {
        var output = string.Empty;

        if (_intermediateResult != null && _intermediateResult.Length > 0)
            output = _intermediateResult.ToString(0, _intermediateResult.Length - 1);

        return new SqlString(output);
    }

    public void Read(BinaryReader reader)
    {
        if (reader == null) 
            throw new ArgumentNullException("reader");

        _intermediateResult = new StringBuilder(reader.ReadString());
    }

    public void Write(BinaryWriter writer)
    {
        if (writer == null) 
            throw new ArgumentNullException("writer");

        writer.Write(_intermediateResult.ToString());
    }
}

To use it, you can simply write an aggregate query:

create table test(
  id int identity(1,1) not null
    primary key
, class tinyint not null
, name nvarchar(120) not null )

insert into test values 
(1, N'This'),
(1, N'is'),
(1, N'just'),
(1, N'a'),
(1, N'test'),
(2, N','),
(3, N'do'),
(3, N'not'),
(3, N'be'),
(3, N'alarmed'),
(3, N','),
(3, N'this'),
(3, N'is'),
(3, N'just'),
(3, N'a'),
(3, N'test')


select dbo.Concatenate(name + ' ')
from test
group by class

drop table test

The output of the query is:

-- Output
-- ===================
-- This is just a test
-- ,
-- do not be alarmed , this is just a test

I packaged up the class and the aggregate as a script which you can find here: https://gist.github.com/FilipDeVos/5b7b4addea1812067b09

Filip De Vos
  • 11,568
  • 1
  • 48
  • 60
  • 11
    +1 this is much more helpful for me than the chosen answer. It's exactly what I need. – Isaac Fife Sep 21 '12 at 21:39
  • This answer could be improved with an answer that takes the example given in the link and shows the exact sql for such a function that does not require any weird dependencies... – Serj Sagan Jun 24 '14 at 16:58
  • 1
    @SerjSagan I added the code + an example and link to an installation script so it is clearer for people reading this answer. – Filip De Vos Sep 30 '14 at 18:14
  • Does this require [SqlFacet(MaxSize = -1)] as described here: http://stackoverflow.com/a/23031812/85196 ? – Mike Mar 04 '15 at 20:29
  • 2
    With this approach, how do I control the order in which items are aggregated? Seems the order will be determined by the clustered index. – Mike Mar 04 '15 at 20:34
  • 1
    You can't, unfortunately user defined aggregates do not support `order by` in windowing functions. An option to force order is to use an index hint `WITH (INDEX(pk_test))` – Filip De Vos Mar 05 '15 at 11:24
  • @Mike SQLCLR methods don't really need `SqlFacet` attributes as these facets are only used for the "deploy" from inside Visual Studio. They help visual studio to create the `create aggregate` statement. – Filip De Vos Mar 12 '15 at 08:35
  • 2
    This is a great way to add maintainability barriers to your product. – marknuzz Oct 21 '15 at 23:32
  • 1
    Why? You shouldn't edit anything directly on your server anyway and the build/installer doesn't care if it is installing sql scripts or binaries. – Filip De Vos Oct 22 '15 at 10:26
  • In my environment (SQL 2008R2, .NET 2.0) the last character was missing in the concatenated results. I've added an extra space as workaround: writer.Write(_intermediateResult.ToString() + " "). Would anybody know, what's the cause of this problem and how to solve it in a cleaner way? – Jan Šotola May 12 '16 at 15:17
  • @JanŠotola I'm finding the same thing. It does give it the rather nice property that you if you do select dbo.Concatenate(field + '|') (for example) you lose the trailing '|'. Or in the example in the answer "+ ' '" – Gaz May 18 '16 at 10:43
  • @FilipDeVos SQL Azure, for instance, doesn't support SQLCLR, so there you have it. – John Aug 31 '17 at 10:45
  • SQL Azure should be considered as relational cloud storage and not a substitute for a full fledged SQL Server instance. You should be concatenating in the business layer of the application. The examples here are for those dreaded "Legacy" SQL Server deployments with "bad" schemas that required concatenating. – Filip De Vos Aug 31 '17 at 13:59