482

How do I get:

id       Name       Value
1          A          4
1          B          8
2          C          9

to

id          Column
1          A:4, B:8
2          C:9
Adriano Carneiro
  • 57,693
  • 12
  • 90
  • 123
Eldila
  • 15,426
  • 23
  • 58
  • 62
  • 24
    This type of problem is solved easily on MySQL with its `GROUP_CONCAT()` aggregate function, but solving it on Microsoft SQL Server is more awkward. See the following SO question for help: "[How to get multiple records against one record based on relation?](http://stackoverflow.com/questions/102317/how-to-get-multiple-records-against-one-record-based-on-relation)" – Bill Karwin Nov 07 '08 at 19:21
  • 1
    Everyone with a microsoft account should vote for a simpler solution on connect: https://connect.microsoft.com/SQLServer/feedback/details/427987/olap-function-for-string-concatenation – Jens Mühlenhoff Jul 31 '15 at 10:45
  • 1
    You can use the SQLCLR Aggregates found here as a substitute until T-SQL is enhanced: http://groupconcat.codeplex.com – Orlando Colamatteo Mar 19 '16 at 03:29
  • 1
    Duplicate of https://stackoverflow.com/questions/194852/concatenate-many-rows-into-a-single-text-string – Salman A Feb 04 '18 at 18:23

23 Answers23

623

No CURSOR, WHILE loop, or User-Defined Function needed.

Just need to be creative with FOR XML and PATH.

[Note: This solution only works on SQL 2005 and later. Original question didn't specify the version in use.]

CREATE TABLE #YourTable ([ID] INT, [Name] CHAR(1), [Value] INT)

INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'A',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)

SELECT 
  [ID],
  STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) 
    FROM #YourTable 
    WHERE (ID = Results.ID) 
    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
  ,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID

DROP TABLE #YourTable
Kevin Fairchild
  • 10,891
  • 6
  • 33
  • 52
  • 7
    why would one nolock a temp table? – Amy B Nov 07 '08 at 19:33
  • 3
    This is the coolest SQL thing I've seen in my life. Any idea if it's "fast" for large data sets? It doesn't start to crawl like a cursor would or anything, does it? I wish more people would vote this craziness up. – user12861 Nov 07 '08 at 21:27
  • 1
    @mbrierst, this example was not made with efficiency in mind. Mainly just wanted to show an alternate solution. When you ask how fast it is for large data sets, what's your definition of 'large' and 'fast'? I'm sure there's a more elegant way to rewrite it with better efficiency, though. – Kevin Fairchild Nov 07 '08 at 21:40
  • Very cool. Query plan indicates that it performs only two table scans and then a nested loop. udf probably cannot be optimized similarly. Don't normally use FOR XML much, I should learn more about its tricks. – Cade Roux Nov 07 '08 at 21:49
  • I'm not at all sure there's a way to write it more efficiently, if anyone knows one please step up. And it's not less elegant than the hideous pivot statements that have been added to the language. I liked this so much I found some of your other answers and voted up the ones that I liked. – user12861 Nov 07 '08 at 21:58
  • 6
    Eh. I just hate the sub-query style of it. JOINS are so much nicer. Just don't think I can utilize that in this solution. Anyhow, I'm glad to see there are other SQL dorks on here aside from me who like learning stuff like this. Kudos to you all :) – Kevin Fairchild Nov 07 '08 at 22:02
  • I hate subqueries too. Good point. But the craziness of FOR XML PATH has blinded me to everything else in your query. Anyway, on to the rest of my life. And I wish an SQL dork besides me would come work at my company for once. – user12861 Nov 07 '08 at 22:10
  • 6
    A slightly cleaner way of doing the string manipulation: STUFF((SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) FROM #YourTable WHERE (ID = Results.ID) FOR XML PATH ('')),1,2,'') AS NameValues – Jonathan Sayce Oct 18 '11 at 10:54
  • Wonderful and insane :-) I used a slightly different version to get all tables in a database: SELECT STUFF ( ( SELECT ', ' + [Name] FROM sys.objects WHERE [type] = 'U' FOR XML PATH ('') ) ,1 ,2 ,'' ) AS [Tables] FROM sys.objects WHERE [type] = 'U' GROUP BY [type] – Shabi_669 Jul 11 '13 at 21:55
  • 3
    Just to note something I've found. Even in a case insensitive environment, the .value part of the query NEEDS to be lower case. I'm guessing this is because it's XML, which is case sensitive – Jaloopa Jul 30 '13 at 15:22
  • 4
    I find it embarrassing for SQLServer that this is the only solution to this problem without using variables. – Jim Pedid Feb 26 '14 at 23:09
  • 1
    Remember: make the subquery's WHERE include all columns in the GROUP BY that are necessary for the same degree of uniqueness. For example, if you have `GROUP BY ID, ORDERNO`, then you might need `WHERE (ID = Results.ID) AND (ORDERNO = Results.ORDERNO)` – Doug_Ivison Aug 22 '14 at 19:17
  • 5
    This is really more a workaround than answer, what if you have a very large query with no easy ID column and many + joins + sub-queries + group by conditions in a view? Copy paste the entire query into the "FOR XML" clause(for each column you wish to join), is that really the best solution that SQL Server has to offer? I think the real answer is that until [2017](http://stackoverflow.com/a/42168649/2912011) string concatenation has not been natively supported by SQL Server. Very disappointing :( – David Rogers Apr 20 '17 at 22:22
  • 2
    @DavidRogers, perhaps your queries need further simplified before you use this technique. In any event, I was glad to see that STRING_AGG WITHIN GROUP has been added to SQL 2017 and highly recommend that for people using that version or later down the road. Since that functionality wasn't available at the time this question was answered and SQL 2017 is still in Preview release and not in use by the majority of users trying to find a solution to this issue on Pre-2017 SQL Server, I stand by this as an 'answer' rather than a 'workaround'. :) – Kevin Fairchild Apr 21 '17 at 13:24
317

If it is SQL Server 2017 or SQL Server Vnext, SQL Azure you can use STRING_AGG as below:

SELECT id, STRING_AGG(CONCAT(name, ':', [value]), ', ')
FROM #YourTable 
GROUP BY id
Kannan Kandasamy
  • 13,405
  • 3
  • 25
  • 38
56

using XML path will not perfectly concatenate as you might expect... it will replace "&" with "&amp;" and will also mess with <" and "> ...maybe a few other things, not sure...but you can try this

I came across a workaround for this... you need to replace:

FOR XML PATH('')
)

with:

FOR XML PATH(''),TYPE
).value('(./text())[1]','VARCHAR(MAX)')

...or NVARCHAR(MAX) if thats what youre using.

why the hell doesn't SQL have a concatenate aggregate function? this is a PITA.

Sumit Pathak
  • 671
  • 1
  • 6
  • 25
Allen
  • 927
  • 8
  • 19
  • 2
    I have scoured the net looking for the best way to NOT encode the output. Thank you SO much! This is the definitive answer - until MS adds proper support for this, like a CONCAT() aggregate function. What I do is throw this into an Outer-Apply that returns my concatenated field. I'm not a fan of adding nested-selects into my select-statements. – MikeTeeVee Mar 15 '13 at 19:01
  • I agreed, without using Value, we can run into problems where the text is an XML encoded character. Please find my blog covering scenarios for grouped concatenation in SQL server. https://blog.vcillusion.co.in/understanding-the-grouped-concatenation-sql-server/ – vCillusion Jun 02 '18 at 22:23
44

I ran into a couple of problems when I tried converting Kevin Fairchild's suggestion to work with strings containing spaces and special XML characters (&, <, >) which were encoded.

The final version of my code (which doesn't answer the original question but may be useful to someone) looks like this:

CREATE TABLE #YourTable ([ID] INT, [Name] VARCHAR(MAX), [Value] INT)

INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'Oranges & Lemons',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'1 < 2',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)

SELECT  [ID],
  STUFF((
    SELECT ', ' + CAST([Name] AS VARCHAR(MAX))
    FROM #YourTable WHERE (ID = Results.ID) 
    FOR XML PATH(''),TYPE 
     /* Use .value to uncomment XML entities e.g. &gt; &lt; etc*/
    ).value('.','VARCHAR(MAX)') 
  ,1,2,'') as NameValues
FROM    #YourTable Results
GROUP BY ID

DROP TABLE #YourTable

Rather than using a space as a delimiter and replacing all the spaces with commas, it just pre-pends a comma and space to each value then uses STUFF to remove the first two characters.

The XML encoding is taken care of automatically by using the TYPE directive.

Liam
  • 27,717
  • 28
  • 128
  • 190
Jonathan Sayce
  • 9,359
  • 5
  • 37
  • 51
26

Another option using Sql Server 2005 and above

---- test data
declare @t table (OUTPUTID int, SCHME varchar(10), DESCR varchar(10))
insert @t select 1125439       ,'CKT','Approved'
insert @t select 1125439       ,'RENO','Approved'
insert @t select 1134691       ,'CKT','Approved'
insert @t select 1134691       ,'RENO','Approved'
insert @t select 1134691       ,'pn','Approved'

---- actual query
;with cte(outputid,combined,rn)
as
(
  select outputid, SCHME + ' ('+DESCR+')', rn=ROW_NUMBER() over (PARTITION by outputid order by schme, descr)
  from @t
)
,cte2(outputid,finalstatus,rn)
as
(
select OUTPUTID, convert(varchar(max),combined), 1 from cte where rn=1
union all
select cte2.outputid, convert(varchar(max),cte2.finalstatus+', '+cte.combined), cte2.rn+1
from cte2
inner join cte on cte.OUTPUTID = cte2.outputid and cte.rn=cte2.rn+1
)
select outputid, MAX(finalstatus) from cte2 group by outputid
cyberkiwi
  • 261
  • 3
  • 2
19

Eight years later... Microsoft SQL Server vNext Database Engine has finally enhanced Transact-SQL to directly support grouped string concatenation. The Community Technical Preview version 1.0 added the STRING_AGG function and CTP 1.1 added the WITHIN GROUP clause for the STRING_AGG function.

Reference: https://msdn.microsoft.com/en-us/library/mt775028.aspx

Shem Sargent
  • 301
  • 2
  • 5
16

Install the SQLCLR Aggregates from http://groupconcat.codeplex.com

Then you can write code like this to get the result you asked for:

CREATE TABLE foo
(
 id INT,
 name CHAR(1),
 Value CHAR(1)
);

INSERT  INTO dbo.foo
    (id, name, Value)
VALUES  (1, 'A', '4'),
        (1, 'B', '8'),
        (2, 'C', '9');

SELECT  id,
    dbo.GROUP_CONCAT(name + ':' + Value) AS [Column]
FROM    dbo.foo
GROUP BY id;
  • I used it a few years ago, the syntax is much cleaner than all "XML Path" tricks and it works very well. I strongly recommend it when SQL CLR functions are an option. – AFract Sep 13 '16 at 08:24
13

SQL Server 2005 and later allow you to create your own custom aggregate functions, including for things like concatenation- see the sample at the bottom of the linked article.

Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • 5
    Unfortunately this requires (?) using CLR assemblies .. which is another issues to deal with :-/ –  Jul 11 '12 at 20:27
  • 1
    Just the example uses CLR for the actual concatenation implementation but this is not required. You could make the concatenation aggregate function use FOR XML so at least it's neater to call it in future! – Shiv Sep 26 '16 at 04:45
9

This is just an addition to Kevin Fairchild's post (very clever by the way). I would have added it as a comment, but I don't have enough points yet :)

I was using this idea for a view I was working on, however the items I was concatinating contained spaces. So I modified the code slightly to not use spaces as delimiters.

Again thanks for the cool workaround Kevin!

CREATE TABLE #YourTable ( [ID] INT, [Name] CHAR(1), [Value] INT ) 

INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (1, 'A', 4) 
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (1, 'B', 8) 
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (2, 'C', 9) 

SELECT [ID], 
       REPLACE(REPLACE(REPLACE(
                          (SELECT [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) as A 
                           FROM   #YourTable 
                           WHERE  ( ID = Results.ID ) 
                           FOR XML PATH (''))
                        , '</A><A>', ', ')
                ,'<A>','')
        ,'</A>','') AS NameValues 
FROM   #YourTable Results 
GROUP  BY ID 

DROP TABLE #YourTable 
Phillip
  • 229
  • 2
  • 14
8

An example would be

In Oracle you can use LISTAGG aggregate function.

Original records

name   type
------------
name1  type1
name2  type2
name2  type3

Sql

SELECT name, LISTAGG(type, '; ') WITHIN GROUP(ORDER BY name)
FROM table
GROUP BY name

Result in

name   type
------------
name1  type1
name2  type2; type3
Community
  • 1
  • 1
Michal B.
  • 5,676
  • 6
  • 42
  • 70
8

This kind of question is asked here very often, and the solution is going to depend a lot on the underlying requirements:

https://stackoverflow.com/search?q=sql+pivot

and

https://stackoverflow.com/search?q=sql+concatenate

Typically, there is no SQL-only way to do this without either dynamic sql, a user-defined function, or a cursor.

Community
  • 1
  • 1
Cade Roux
  • 88,164
  • 40
  • 182
  • 265
  • 2
    Not true. cyberkiwi's solution using cte:s is pure sql without any vendor-specific hackery. – Björn Lindqvist Jul 25 '13 at 11:24
  • 1
    At the time of the question and answer, I would not have counted recursive CTEs as terribly portable, but they are supported now by Oracle. The best solution is going to depend upon the platform. For SQL Server it is most likely the FOR XML technique or a customer CLR aggregate. – Cade Roux Jul 25 '13 at 15:11
  • 1
    the ultimate answer for all questions? http://stackoverflow.com/search?q=[whatever the question] – Junchen Liu Dec 08 '16 at 11:07
7

Just to add to what Cade said, this is usually a front-end display thing and should therefore be handled there. I know that sometimes it's easier to write something 100% in SQL for things like file export or other "SQL only" solutions, but most of the times this concatenation should be handled in your display layer.

Bridge
  • 29,818
  • 9
  • 60
  • 82
Tom H
  • 46,766
  • 14
  • 87
  • 128
  • 11
    Grouping is a front-end display thing now? There are plenty of valid scenarios for concatenating one column in a grouped result set. – MGOwen Aug 03 '16 at 03:21
5

Don't need a cursor... a while loop is sufficient.

------------------------------
-- Setup
------------------------------

DECLARE @Source TABLE
(
  id int,
  Name varchar(30),
  Value int
)

DECLARE @Target TABLE
(
  id int,
  Result varchar(max) 
)


INSERT INTO @Source(id, Name, Value) SELECT 1, 'A', 4
INSERT INTO @Source(id, Name, Value) SELECT 1, 'B', 8
INSERT INTO @Source(id, Name, Value) SELECT 2, 'C', 9


------------------------------
-- Technique
------------------------------

INSERT INTO @Target (id)
SELECT id
FROM @Source
GROUP BY id

DECLARE @id int, @Result varchar(max)
SET @id = (SELECT MIN(id) FROM @Target)

WHILE @id is not null
BEGIN
  SET @Result = null

  SELECT @Result =
    CASE
      WHEN @Result is null
      THEN ''
      ELSE @Result + ', '
    END + s.Name + ':' + convert(varchar(30),s.Value)
  FROM @Source s
  WHERE id = @id

  UPDATE @Target
  SET Result = @Result
  WHERE id = @id

  SET @id = (SELECT MIN(id) FROM @Target WHERE @id < id)
END

SELECT *
FROM @Target
Amy B
  • 108,202
  • 21
  • 135
  • 185
  • 2
    See: [Bad Habits to Kick : Thinking a WHILE loop isn't a CURSOR](https://sqlblog.org/2012/01/26/bad-habits-to-kick-thinking-a-while-loop-isnt-a-cursor) – marc_s Mar 09 '15 at 19:17
  • @marc_s perhaps a better criticism is that PRIMARY KEY should be declared on the table variables. – Amy B Mar 10 '15 at 02:19
  • @marc_s On further inspection, that article is a sham - as are almost all discussions of performance without IO measurement. I did learn about LAG - so thanks for that. – Amy B Mar 10 '15 at 04:34
4

Let's get very simple:

SELECT stuff(
    (
    select ', ' + x from (SELECT 'xxx' x union select 'yyyy') tb 
    FOR XML PATH('')
    )
, 1, 2, '')

Replace this line:

select ', ' + x from (SELECT 'xxx' x union select 'yyyy') tb

With your query.

Ullas
  • 11,450
  • 4
  • 33
  • 50
Marquinho Peli
  • 4,795
  • 4
  • 24
  • 22
3

You can improve performance significant the following way if group by contains mostly one item:

SELECT 
  [ID],

CASE WHEN MAX( [Name]) = MIN( [Name]) THEN 
MAX( [Name]) NameValues
ELSE

  STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) 
    FROM #YourTable 
    WHERE (ID = Results.ID) 
    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
  ,1,2,'') AS NameValues

END

FROM #YourTable Results
GROUP BY ID
josliber
  • 43,891
  • 12
  • 98
  • 133
Eduard
  • 31
  • 1
3

didn't see any cross apply answers, also no need for xml extraction. Here is a slightly different version of what Kevin Fairchild wrote. It's faster and easier to use in more complex queries:

   select T.ID
,MAX(X.cl) NameValues
 from #YourTable T
 CROSS APPLY 
 (select STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX))
    FROM #YourTable 
    WHERE (ID = T.ID) 
    FOR XML PATH(''))
  ,1,2,'')  [cl]) X
  GROUP BY T.ID
Mordechai
  • 718
  • 1
  • 8
  • 23
3

Using the Stuff and for xml path operator to concatenate rows to string :Group By two columns -->

CREATE TABLE #YourTable ([ID] INT, [Name] CHAR(1), [Value] INT)

INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'A',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',5)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)

-- retrieve each unique id and name columns and concatonate the values into one column
SELECT 
  [ID], 
  STUFF((
    SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) -- CONCATONATES EACH APPLICATION : VALUE SET      
    FROM #YourTable 
    WHERE (ID = Results.ID and Name = results.[name] ) 
    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
  ,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID


SELECT 
  [ID],[Name] , --these are acting as the group by clause
  STUFF((
    SELECT ', '+  CAST([Value] AS VARCHAR(MAX)) -- CONCATONATES THE VALUES FOR EACH ID NAME COMBINATION 
    FROM #YourTable 
    WHERE (ID = Results.ID and Name = results.[name] ) 
    FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
  ,1,2,'') AS  NameValues
FROM #YourTable Results
GROUP BY ID, name

DROP TABLE #YourTable
CJurkus
  • 31
  • 3
1

Using Replace Function and FOR JSON PATH

SELECT T3.DEPT, REPLACE(REPLACE(T3.ENAME,'{"ENAME":"',''),'"}','') AS ENAME_LIST
FROM (
 SELECT DEPT, (SELECT ENAME AS [ENAME]
        FROM EMPLOYEE T2
        WHERE T2.DEPT=T1.DEPT
        FOR JSON PATH,WITHOUT_ARRAY_WRAPPER) ENAME
    FROM EMPLOYEE T1
    GROUP BY DEPT) T3

For sample data and more ways click here

Mahesh
  • 61
  • 1
  • 8
1

If you have clr enabled you could use the Group_Concat library from GitHub

Manfred Wippel
  • 1,946
  • 1
  • 15
  • 14
1

For all my healthcare folks out there:

 
SELECT
s.NOTE_ID
,STUFF ((
        SELECT
           [note_text] + ' ' 
        FROM
            HNO_NOTE_TEXT s1
        WHERE
            (s1.NOTE_ID = s.NOTE_ID)
        ORDER BY [line] ASC
         FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
         ,
        1,
        2,
        '') AS NOTE_TEXT_CONCATINATED
FROM
    HNO_NOTE_TEXT s
    GROUP BY NOTE_ID
 
Aus_10
  • 670
  • 7
  • 15
0

Another example without the garbage: ",TYPE).value('(./text())[1]','VARCHAR(MAX)')"

WITH t AS (
    SELECT 1 n, 1 g, 1 v
    UNION ALL 
    SELECT 2 n, 1 g, 2 v
    UNION ALL 
    SELECT 3 n, 2 g, 3 v
)
SELECT g
        , STUFF (
                (
                    SELECT ', ' + CAST(v AS VARCHAR(MAX))
                    FROM t sub_t
                    WHERE sub_t.g = main_t.g
                    FOR XML PATH('')
                )
                , 1, 2, ''
        ) cg
FROM t main_t
GROUP BY g

Input-output is

*************************   ->  *********************
*   n   *   g   *   v   *       *   g   *   cg      *
*   -   *   -   *   -   *       *   -   *   -       *
*   1   *   1   *   1   *       *   1   *   1, 2    *
*   2   *   1   *   2   *       *   2   *   3       *
*   3   *   2   *   3   *       *********************
*************************   
Syzako
  • 13
  • 5
0

I used this approach which may be easier to grasp. Get a root element, then concat to choices any item with the same ID but not the 'official' name

  Declare @IdxList as Table(id int, choices varchar(max),AisName varchar(255))
  Insert into @IdxLIst(id,choices,AisName)
  Select IdxId,''''+Max(Title)+'''',Max(Title) From [dbo].[dta_Alias] 
 where IdxId is not null group by IdxId
  Update @IdxLIst
    set choices=choices +','''+Title+''''
    From @IdxLIst JOIN [dta_Alias] ON id=IdxId And Title <> AisName
    where IdxId is not null
    Select * from @IdxList where choices like '%,%'
Ken Lassesen
  • 125
  • 9
0

Hello it is now possible to concatenate string in group by with SQL Server, by using STRING_AGG function. Here an example

--- Create table

create table test_string_agg(
id int,
str_text varchar(100)
)

-- Insert

insert into test_string_agg 
values (1,'Text1'),(1,'Text2'),(1,'Text3'), (2,'Text4')

--Request

select id, STRING_AGG(str_text, ';') as t_string_agg from test_string_agg
group by id

--Result

-------------------------------
|  id     | t_string_agg
----------------------------------
|   1     | Text1;Text2;Text3
---------------------------------
|   2     | Text4
-----------------------------------