2450

Consider a database table holding names, with three rows:

Peter
Paul
Mary

Is there an easy way to turn this into a single string of Peter, Paul, Mary?

10
  • 29
    For answers specific to SQL Server, try this question. Commented Oct 12, 2008 at 0:03
  • 22
    For MySQL, check out Group_Concat from this answer Commented May 6, 2011 at 19:48
  • 33
    I wish the next version of SQL Server would offer a new feature to solve multi-row string concatination elegantly without the silliness of FOR XML PATH. Commented Oct 2, 2014 at 11:47
  • 4
    Not SQL, but if this is a once-only thing, you can paste the list into this in-browser tool convert.town/column-to-comma-separated-list Commented May 27, 2015 at 7:56
  • 4
    In Oracle you can use the LISTAGG(COLUMN_NAME) from 11g r2 before that there is an unsupported function called WM_CONCAT(COLUMN_NAME) which does the same. Commented Jul 6, 2017 at 6:32

51 Answers 51

1
2
5

This can be useful too

create table #test (id int,name varchar(10))
--use separate inserts on older versions of SQL Server
insert into #test values (1,'Peter'), (1,'Paul'), (1,'Mary'), (2,'Alex'), (3,'Jack')

DECLARE @t VARCHAR(255)
SELECT @t = ISNULL(@t + ',' + name, name) FROM #test WHERE id = 1
select @t
drop table #test

returns

Peter,Paul,Mary
Sign up to request clarification or add additional context in comments.

2 Comments

Unfortunately this behavior seems not to be officially supported. MSDN says: "If a variable is referenced in a select list, it should be assigned a scalar value or the SELECT statement should only return one row." And there are people who observed problems: sqlmag.com/sql-server/multi-row-variable-assignment-and-order
Don't forget to look stackoverflow.com/a/42778050/333153 On newer versions of SQL Server, we can now use STRING_AGG function.
5

In SQL Server 2017 or later versions, you can concatenate text from multiple rows into a single text string in SQL Server using the STRING_AGG function. Here's an example of how you can achieve this:

Assuming you have a table called "Names" with a column "Name" containing the values Peter, Paul, and Mary in separate rows, you can use the following SQL query:

SELECT STRING_AGG(Name, ', ') AS ConcatenatedNames
FROM Names;

This query will return a single string with the names concatenated and separated by commas:

ConcatenatedNames
-----------------
Peter, Paul, Mary

Comments

4

Here is the complete solution to achieve this:

-- Table Creation
CREATE TABLE Tbl
( CustomerCode    VARCHAR(50)
, CustomerName    VARCHAR(50)
, Type VARCHAR(50)
,Items    VARCHAR(50)
)

insert into Tbl
SELECT 'C0001','Thomas','BREAKFAST','Milk'
union SELECT 'C0001','Thomas','BREAKFAST','Bread'
union SELECT 'C0001','Thomas','BREAKFAST','Egg'
union SELECT 'C0001','Thomas','LUNCH','Rice'
union SELECT 'C0001','Thomas','LUNCH','Fish Curry'
union SELECT 'C0001','Thomas','LUNCH','Lessy'
union SELECT 'C0002','JOSEPH','BREAKFAST','Bread'
union SELECT 'C0002','JOSEPH','BREAKFAST','Jam'
union SELECT 'C0002','JOSEPH','BREAKFAST','Tea'
union SELECT 'C0002','JOSEPH','Supper','Tea'
union SELECT 'C0002','JOSEPH','Brunch','Roti'

-- function creation
GO
CREATE  FUNCTION [dbo].[fn_GetItemsByType]
(   
    @CustomerCode VARCHAR(50)
    ,@Type VARCHAR(50)
)
RETURNS @ItemType TABLE  ( Items VARCHAR(5000) )
AS
BEGIN

        INSERT INTO @ItemType(Items)
    SELECT  STUFF((SELECT distinct ',' + [Items]
         FROM Tbl 
         WHERE CustomerCode = @CustomerCode
            AND Type=@Type
            FOR XML PATH(''))
        ,1,1,'') as  Items



    RETURN 
END

GO

-- fianl Query
DECLARE @cols AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX)

select @cols = STUFF((SELECT distinct ',' + QUOTENAME(Type) 
                    from Tbl
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')

set @query = 'SELECT CustomerCode,CustomerName,' + @cols + '
             from 
             (
                select  
                    distinct CustomerCode
                    ,CustomerName
                    ,Type
                    ,F.Items
                    FROM Tbl T
                    CROSS APPLY [fn_GetItemsByType] (T.CustomerCode,T.Type) F
            ) x
            pivot 
            (
                max(Items)
                for Type in (' + @cols + ')
            ) p '

execute(@query) 

1 Comment

An explanation would be in order. E.g., what is the gist/idea? You should also fix the indentation. Please respond by editing (changing) your answer, not here in comments (without "Edit:", "Update:", or similar - the answer should appear as if it was written today).
4

This method applies to the Teradata Aster database only as it uses its NPATH function.

Again, we have table Students

SubjectID       StudentName
----------      -------------
1               Mary
1               John
1               Sam
2               Alaina
2               Edward

Then with NPATH it is just single SELECT:

SELECT * FROM npath(
  ON Students
  PARTITION BY SubjectID
  ORDER BY StudentName
  MODE(nonoverlapping)
  PATTERN('A*')
  SYMBOLS(
    'true' as A
  )
  RESULT(
    FIRST(SubjectID of A) as SubjectID,
    ACCUMULATE(StudentName of A) as StudentName
  )
);

Result:

SubjectID       StudentName
----------      -------------
1               [John, Mary, Sam]
2               [Alaina, Edward]

Comments

4

SQL Server 2005 or later

CREATE TABLE dbo.Students
(
    StudentId INT
    , Name VARCHAR(50)
    , CONSTRAINT PK_Students PRIMARY KEY (StudentId)
);

CREATE TABLE dbo.Subjects
(
    SubjectId INT
    , Name VARCHAR(50)
    , CONSTRAINT PK_Subjects PRIMARY KEY (SubjectId)
);

CREATE TABLE dbo.Schedules
(
    StudentId INT
    , SubjectId INT
    , CONSTRAINT PK__Schedule PRIMARY KEY (StudentId, SubjectId)
    , CONSTRAINT FK_Schedule_Students FOREIGN KEY (StudentId) REFERENCES dbo.Students (StudentId)
    , CONSTRAINT FK_Schedule_Subjects FOREIGN KEY (SubjectId) REFERENCES dbo.Subjects (SubjectId)
);

INSERT dbo.Students (StudentId, Name) VALUES
    (1, 'Mary')
    , (2, 'John')
    , (3, 'Sam')
    , (4, 'Alaina')
    , (5, 'Edward')
;

INSERT dbo.Subjects (SubjectId, Name) VALUES
    (1, 'Physics')
    , (2, 'Geography')
    , (3, 'French')
    , (4, 'Gymnastics')
;

INSERT dbo.Schedules (StudentId, SubjectId) VALUES
    (1, 1)        --Mary, Physics
    , (2, 1)    --John, Physics
    , (3, 1)    --Sam, Physics
    , (4, 2)    --Alaina, Geography
    , (5, 2)    --Edward, Geography
;

SELECT
    sub.SubjectId
    , sub.Name AS [SubjectName]
    , ISNULL( x.Students, '') AS Students
FROM
    dbo.Subjects sub
    OUTER APPLY
    (
        SELECT
            CASE ROW_NUMBER() OVER (ORDER BY stu.Name) WHEN 1 THEN '' ELSE ', ' END
            + stu.Name
        FROM
            dbo.Students stu
            INNER JOIN dbo.Schedules sch
                ON stu.StudentId = sch.StudentId
        WHERE
            sch.SubjectId = sub.SubjectId
        ORDER BY
            stu.Name
        FOR XML PATH('')
    ) x (Students)
;

Comments

4

Not that I have done any analysis on performance as my list had less than 10 items but I was amazed after looking through the 30 odd answers I still had a twist on a similar answer already given similar to using COALESCE for a single group list and didn't even have to set my variable (defaults to NULL anyhow) and it assumes all entries in my source data table are non blank:

DECLARE @MyList VARCHAR(1000), @Delimiter CHAR(2) = ', '
SELECT @MyList = CASE WHEN @MyList > '' THEN @MyList + @Delimiter ELSE '' END + FieldToConcatenate FROM MyData

I am sure COALESCE internally uses the same idea. Let’s hope Microsoft don't change this on me.

Comments

4

It's now 2024, and a lot of the Answers shown here from previous years are a bit out-of-date. With SQL Server, you can now easily combine fields using STRING_AGG.

Let's look at a simple example.

In my database, I have a table of Users, a table of possible Roles, and a third table showing which Users have which Roles.

enter image description here

Here's how I would get a list of user names, followed by a comma-seperated list of their roles:

SELECT usr.Id, usr.UserName, STRING_AGG(rol.Name, ', ') AS 'Roles'
FROM dbo.[User] usr
LEFT JOIN [dbo].[RoleUser] ru
ON ru.UserId = usr.Id
LEFT JOIN [dbo].[Role] rol
ON rol.Id = ru.RoleId
GROUP BY usr.Id, usr.UserName
ORDER BY usr.Id

And here's what it would look like:

enter image description here

1 Comment

and if you want to order the Roles column, you can do STRING_AGG(rol.Name, ' , ') WITHIN GROUP (ORDER BY rol.Name) AS 'Roles'
3

One way you could do it in SQL Server would be to return the table content as XML (for XML raw), convert the result to a string and then replace the tags with ", ".

Comments

3

Below is a simple PL/SQL procedure to implement the given scenario using "basic loop" and "rownum"

Table definition

CREATE TABLE "NAMES" ("NAME" VARCHAR2(10 BYTE))) ;

Let's insert values into this table

INSERT INTO NAMES VALUES('PETER');
INSERT INTO NAMES VALUES('PAUL');
INSERT INTO NAMES VALUES('MARY');

Procedure starts from here

DECLARE 

MAXNUM INTEGER;
CNTR INTEGER := 1;
C_NAME NAMES.NAME%TYPE;
NSTR VARCHAR2(50);

BEGIN

SELECT MAX(ROWNUM) INTO MAXNUM FROM NAMES;

LOOP

SELECT NAME INTO  C_NAME FROM 
(SELECT ROWNUM RW, NAME FROM NAMES ) P WHERE P.RW = CNTR;

NSTR := NSTR ||','||C_NAME;
CNTR := CNTR + 1;
EXIT WHEN CNTR > MAXNUM;

END LOOP;

dbms_output.put_line(SUBSTR(NSTR,2));

END;

Result

PETER,PAUL,MARY

1 Comment

The question is asking for an answer specific to SQL Server. If there is a PL/SQL question, you might answer there, instead. However, check out wm_concat, first, and see whether that is an easier method.
3

In PostgreSQL - array_agg

SELECT array_to_string(array_agg(DISTINCT rolname), ',') FROM pg_catalog.pg_roles;

Or STRING_AGG

SELECT STRING_AGG(rolname::text,',') FROM pg_catalog.pg_roles;

2 Comments

But the question specified SQL Server, not PostgresSQL
Same function for DuckDB.
1
SELECT PageContent = Stuff(
    (   SELECT PageContent
        FROM dbo.InfoGuide
        WHERE CategoryId = @CategoryId
          AND SubCategoryId = @SubCategoryId
        for xml path(''), type
    ).value('.[1]','nvarchar(max)'),
    1, 1, '')
FROM dbo.InfoGuide info

1 Comment

An explanation would be in order. Please respond by editing (changing) your answer, not here in comments (without "Edit:", "Update:", or similar - the answer should appear as if it was written today).
1

Although it's too late, and already has many solutions. Here is simple solution for MySQL:

SELECT t1.id,
        GROUP_CONCAT(t1.id) ids
 FROM table t1 JOIN table t2 ON (t1.id = t2.id)
 GROUP BY t1.id

2 Comments

This question is specific to SQL server, so this answer is unlikely to be found by those who need it. Is there a mysql-specific question about the same thing?
@jpaugh: Not an excuse, but the 4th answer (2nd if deleted answers are not counted), posted on day two, with 140 upvotes, was also about MySQL: Darryl Hein's answer. Other answers are for Oracle and PostgreSQL. What is the best way to proceed? Flag them as "not an answer"? Or something else?
1

With a recursive query you can do it:

-- Create example table
CREATE TABLE tmptable (NAME VARCHAR(30)) ;

-- Insert example data
INSERT INTO tmptable VALUES('PETER');
INSERT INTO tmptable VALUES('PAUL');
INSERT INTO tmptable VALUES('MARY');

-- Recurse query
with tblwithrank as (
select * , row_number() over(order by name) rang , count(*) over() NbRow
from tmptable
),
tmpRecursive as (
select *, cast(name as varchar(2000)) as AllName from tblwithrank  where rang=1
union all
select f0.*,  cast(f0.name + ',' + f1.AllName as varchar(2000)) as AllName 
from tblwithrank f0 inner join tmpRecursive f1 on f0.rang=f1.rang +1 
)
select AllName from tmpRecursive
where rang=NbRow

Comments

1

Use this:

ISNULL(SUBSTRING(REPLACE((select ',' FName as 'data()' from NameList for xml path('')), ' ,',', '), 2, 300), '') 'MyList'

Where the "300" could be any width taking into account the maximum number of items you think will show up.

1 Comment

If you ever find yourself having to guess ahead of time how many rows will be in your results, you're doing it wrong.
1

There are a couple of ways in Oracle:

    create table name
    (first_name varchar2(30));

    insert into name values ('Peter');
    insert into name values ('Paul');
    insert into name values ('Mary');

Solution is 1:

    select substr(max(sys_connect_by_path (first_name, ',')),2) from (select rownum r, first_name from name ) n start with r=1 connect by prior r+1=r
    o/p=> Peter,Paul,Mary

Solution is 2:

    select  rtrim(xmlagg (xmlelement (e, first_name || ',')).extract ('//text()'), ',') first_name from name
    o/p=> Peter,Paul,Mary

Comments

1

With the 'TABLE' type it is extremely easy. Let's imagine that your table is called Students and it has column name.

declare @rowsCount INT
declare @i INT = 1
declare @names varchar(max) = ''

DECLARE @MyTable TABLE
(
  Id int identity,
  Name varchar(500)
)
insert into @MyTable select name from Students
set @rowsCount = (select COUNT(Id) from @MyTable)

while @i < @rowsCount
begin
 set @names = @names + ', ' + (select name from @MyTable where Id = @i)
 set @i = @i + 1
end
select @names

This example was tested with SQL Server 2008 R2.

Comments

1

Here I am showing about if it is more than 2 columns table data, then how to solve the problem.

If you have SQL Server Management Studio 2017 or later, you can very easily solve it using

STRING_AGG

Concatenates the values of string expressions and places separator values between them. The separator isn't added at the end of string.

Below is the SQL query

DECLARE @tbl TABLE (id INT, _name CHAR(1), days VARCHAR(10), daysfrequency VARCHAR(10), scheduletime VARCHAR(10));
INSERT INTO @tbl (id,_name,days,daysfrequency,scheduletime) VALUES
(1, 'a', 'Day4','Monthly','22:10'),
(1, 'a', 'Thu', 'Weekly', '07:30'),
(1, 'a', 'Fri', 'Daily',  '23:10'),
(2, 'b', 'Mon', 'Weekly', '20:00'),
(2, 'b', 'Tue', 'Weekly', '23:10'),
(2, 'b', 'Wed', 'Weekly', '18:10'),
(2, 'b', 'Thu', 'Weekly', '10:23'),
(2, 'b', 'Fri', 'Weekly', '1:23');


SELECT  t.id,
        t._name,
        STRING_AGG(t.days, ', ') WITHIN GROUP (ORDER BY t._name ASC) AS days,
        t1.daysfrequency,
        STRING_AGG(t.scheduletime, ', ') WITHIN GROUP (ORDER BY t._name ASC) AS scheduletime
FROM @tbl t INNER JOIN
(
    SELECT id, _name, STRING_AGG(daysfrequency, ', ') WITHIN GROUP (ORDER BY _name ASC) AS daysfrequency FROM
    (
        SELECT DISTINCT id, _name, daysfrequency FROM @tbl
    ) a
    GROUP BY id, _name
) t1
ON t.id = t1.id AND t._name = t1._name
GROUP BY t.id, t._name, t1.daysfrequency;

enter image description here

Comments

0

We can use RECUSRSIVITY, WITH CTE, union ALL as follows

declare @mytable as table(id int identity(1,1), str nvarchar(100))
insert into @mytable values('Peter'),('Paul'),('Mary')

declare @myresult as table(id int,str nvarchar(max),ind int, R# int)

;with cte as(select id,cast(str as nvarchar(100)) as str, cast(0 as int) ind from @mytable
union all
select t2.id,cast(t1.str+',' +t2.str as nvarchar(100)) ,t1.ind+1 from cte t1 inner join @mytable t2 on t2.id=t1.id+1)
insert into @myresult select *,row_number() over(order by ind) R# from cte

select top 1 str from @myresult order by R# desc

Comments

0

First of all you should declare a table variable and fill it with your table data and after that, with a WHILE loop, select row one by one and add its value to a nvarchar(max) variable.

    Go
    declare @temp table(
        title nvarchar(50)
    )
    insert into @temp(title)
    select p.Title from dbo.person p
    --
    declare @mainString nvarchar(max)
    set @mainString = '';
    --
    while ((select count(*) from @temp) != 0)
    begin
        declare @itemTitle nvarchar(50)
        set @itemTitle = (select top(1) t.Title from @temp t)
    
        if @mainString = ''
        begin
            set @mainString = @itemTitle
        end
        else
        begin
            set @mainString = concat(@mainString,',',@itemTitle)
        end
    
        delete top(1) from @temp
    
    end
    print @mainString

3 Comments

Looks like a good answer, but please explain how this code works.
This seems like it would have very inefficient bounds — how would such work over a million rows?
A while loop? Isn't that unSQLish?
-1

I don't have the reputation to comment on the answers warning about using varchar(max), so posting as an answer with full supporting documentation.

If your variable is defined such as varchar(max), you will run into performance issues if the resulting variable length is greater than 256k. It'll append each value in <2 ms up to that point when utilized with a table select, then the updates will take 200x times as long each time you append once you cross that 256k threshold, increasing in time as the size increases.

To work around that limit, create a temp table that you insert rows into instead, then use the XML method covered in many other posts to create the resulting value.

Demonstrating the performance problem:

-- Takes < 45 sec to run to build a variable 512k in size using the variable appending method
SET NOCOUNT ON
DECLARE @txt nvarchar(max)='',
        @StartTime datetime2,
        @MS int,
        @k int
DECLARE @Stats TABLE (
    id int NOT NULL IDENTITY PRIMARY KEY,
    k int,
    ms int
)

WHILE Len(@txt) / 1024 < 512
BEGIN
    SET @StartTime=SYSDATETIME()
    SELECT TOP 28 @txt=@txt + cast(rowguid as char(36))
    FROM AdventureWorks2022.Person.Person
    SET @MS=DATEDIFF(ms,@StartTime,SYSDATETIME())
    SET @k=Len(@txt) / 1024
    IF NOT EXISTS (SELECT 1 FROM @Stats WHERE k=@k)
        INSERT INTO @Stats VALUES (@k,@MS)
END

SELECT ms FROM @Stats ORDER BY id

Graph showing how long it took to append data to a variable in increments of 1k in size

The below SQL creates a variable with over a MB of data ranging from 80ms to 150ms total on the same laptop as the above query. You will have to unencode any < and > and check for any other character encoding in your result. If this process is inside a loop, be sure to delete all rows from the temp table after extracting it's data. Also note your delimiter will have a trailing space added to it.

-- Takes < 1 sec to run
SET NOCOUNT ON
DECLARE @txt nvarchar(max)='',
    @StartTime datetime2,
    @MS int
DROP TABLE IF EXISTS #Hold
CREATE TABLE #Hold (
    id int NOT NULL IDENTITY PRIMARY KEY,
    txt nvarchar(102)
)

SET @StartTime=SYSDATETIME()

INSERT INTO #Hold
SELECT cast(rowguid as char(36)) + N','
FROM AdventureWorks2022.Sales.SalesOrderHeader

SET @txt=(SELECT txt AS 'data()' FROM #Hold ORDER BY id FOR XML PATH(''))

SET @MS=DATEDIFF(ms,@StartTime,SYSDATETIME())

SELECT @MS as MS, Len(@txt) as TxtLen

2 Comments

What's wrong with STRING_AGG or FOR XML, the top two answers already posted?
@Charlieface There's nothing wrong with STRING_AGG or FOR XML as I stated in the 3rd paragraph. This is about using those methods with a lot of data - that if it exceeds 256K, the performance will absolutely TANK. I found a proc using this that took hours to run after hitting the 256K mark but seconds after I figured out this technique.
-5
   declare @phone varchar(max)='' 
   select @phone=@phone + mobileno +',' from  members
   select @phone

4 Comments

Why not +', ' As OP wanted and also you don't delete last ';'. I think this answer is same and also this answer ;).
I had this problem and I found answer but I want Concatenate with ';' so I paste it here, last element is empty
When you post your answer here, It should be related to the question And result of your code should be Null, because you start with @phone IS Null and adding to Null will be Null in SQL Server, I think you forgot something like adding = '' after your first line ;).
No, I post answer after check it and result was not null
1
2

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.