39

I want to return the results of select Column from Table into a comma separated string using SQL Server.

The column in question is rather large (nvarchar(2000)) so the solution has to be able to handle very large result values.

Kyle West
  • 8,934
  • 13
  • 65
  • 97

9 Answers9

47

STRING_AGG was added in sql 2017

https://learn.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql?view=sql-server-2017

SELECT STRING_AGG (ColumnName, ',') AS csv 
FROM TableName
GROUP BY ColumnName2
Nathan Smith
  • 1,643
  • 15
  • 16
37
DECLARE @result nvarchar(max)
SET @result = ''

SELECT @result = @result + [Column] + N','
FROM [TABLE]

--TODO: trim last ',' if you require

PRINT @result

If Column can be null, then either exclude it first, or use ISNULL/COALESCE - otherwise a single NULL will break the entire sequence. It is more efficient to exclude it with a WHERE:

SELECT @result = @result + [Column] + N','
FROM [TABLE]
WHERE [Column] IS NOT NULL
Liam
  • 27,717
  • 28
  • 128
  • 190
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 5
    works as advertised. thanks. for those who may find this later, it's easy to knock off the last comma: `select @result = substring(@result, 1, (LEN(@result)-1))` – Kyle West Jan 18 '09 at 18:16
  • 13
    SELECT @result = COALESCE(@result + N', ', N'') + [Column] will avoid the last comma without perf penalty. – Robert Jeppesen Mar 20 '09 at 10:25
12

without the trailing comma version:

declare @s varchar(max);

select @s = isnull(@s + ', ' + lastname, lastname)
from person
order by lastname;

print @s;
Michael Buen
  • 38,643
  • 9
  • 94
  • 118
  • I'd expect that to use a few more CPU cycles, though... personally, if I needed to trim it, I'd use the string functions instead. – Marc Gravell Jan 18 '09 at 17:32
  • MSSQL strings are immutable, I'd expect that copying big strings(trimming would also yield a copy) would also incur some CPU cycles, I'm not sure though. I think if it is a one-off thing, micro-optimizations aren't warranted. – Michael Buen Jan 18 '09 at 18:03
  • 1
    I would use coalesce instead of isnull – B4ndt Nov 16 '15 at 13:39
8

I've had problems with the method discussed in the suggested answer. I took the code from SQLAuthority. But this works every time if you have problem suggested solution

SELECT SUBSTRING(
(SELECT ',' + s.ColumnName
FROM dbo.table s
ORDER BY s.ColumnName
FOR XML PATH('')),2,200000) AS CSV
GO
kariato
  • 169
  • 1
  • 4
  • For large number of values (~100k), this answer works much faster than the accepted answer although the 200000 must be increased. – crokusek Jan 14 '19 at 19:45
  • I would suggest using STUFF(@String,1,1,'') vs substring with a huge length value unless you really want to set a max length. – Pete Nov 11 '19 at 17:49
4

I've created a proc that will dynamically create a CSV out of any arbitrary table, without needing to explicitly specify the columns. This is useful if you don't want to write custom code every time you want to turn a SQL table into a CSV string.

-- Description: Turns a query into a formatted CSV.
-- Any ORDER BY clause needs to be passed in the separate ORDER BY parameter.
CREATE PROC [dbo].[spQueryToCsv] 
(
  @query nvarchar(MAX), --A query to turn into CSV format. It should not include an ORDER BY clause.
  @orderBy nvarchar(MAX) = NULL, --An optional ORDER BY clause. It should contain the words 'ORDER BY'.
  @csv nvarchar(MAX) = NULL OUTPUT --The CSV output of the procedure.
)
AS
BEGIN   
  SET NOCOUNT ON;

  IF @orderBy IS NULL BEGIN
    SET @orderBy = '';
  END

  SET @orderBy = REPLACE(@orderBy, '''', '''''');  

  DECLARE @realQuery nvarchar(MAX) = '
    DECLARE @headerRow nvarchar(MAX);
    DECLARE @cols nvarchar(MAX);

    SELECT * INTO #dynSql FROM (' + @query + ') sub;    

    SELECT @cols = ISNULL(@cols + '' + '''','''' + '', '''') + ''''''"'''' + ISNULL(REPLACE(CAST(['' + name + ''] AS nvarchar(max)), ''''"'''', ''''""''''), '''''''') + ''''"''''''
    FROM tempdb.sys.columns 
    WHERE object_id = object_id(''tempdb..#dynSql'')
    ORDER BY column_id;        

    SET @cols = ''
      SET @csv = (SELECT '' + @cols + '' FROM #dynSql ' + @orderBy + ' FOR XML PATH(''''m_m''''));
      ''
    EXEC sys.sp_executesql @cols, N''@csv nvarchar(MAX) OUTPUT'', @csv=@csv OUTPUT    

    SELECT @headerRow = ISNULL(@headerRow + '','', '''') + ''"'' + REPLACE(name, ''"'', ''""'') + ''"''
    FROM tempdb.sys.columns 
    WHERE object_id = object_id(''tempdb..#dynSql'')
    ORDER BY column_id;

    SET @headerRow = @headerRow + CHAR(13) + CHAR(10);

    SET @csv = @headerRow + @csv;    
    ';

  EXEC sys.sp_executesql @realQuery, N'@csv nvarchar(MAX) OUTPUT', @csv=@csv OUTPUT
  SET @csv = REPLACE(REPLACE(@csv, '<m_m>', ''), '</m_m>', CHAR(13) + CHAR(10))
END

GO

Usage:

DECLARE @csv nvarchar(max)
EXEC [dbo].[spQueryToCsv]  @query = 'SELECT * FROM Customers', @csv = @csv OUTPUT, @orderBy = 'ORDER BY CustomerId'
SELECT @csv

This is based on similar code I wrote to turn an arbitrary table into an HTML string.

Community
  • 1
  • 1
MgSam
  • 12,139
  • 19
  • 64
  • 95
2

Inner SELECT will create CSV, and STUFF() will trim first comma and space

SELECT STUFF( 
    (SELECT ', ',  Column FROM Table FOR XML PATH(''))
, 1, 2, '' )

You can replace the separator with anything you want, just adjust STUFF() to trim it in the beginning.

Mitko Petrov
  • 311
  • 1
  • 4
  • 12
0

If you are stuck on old databases and can't use STRING_AGG and don't want to trim after generating the string:

Option 1:

DECLARE @Comma varchar(10) = '';
DECLARE @Result varchar(MAX) = '';

SELECT @Result = @Result + @Comma + Column, @Comma = ','
FROM Table;

Option 2:

DECLARE @Result varchar(MAX) = '';

SELECT @Result = @Result + CASE WHEN LEN(@Result) > 0 THEN ',' ELSE '' END + Column
FROM Table;

I like Option 1 because it is shorter. I have not done any performance testing, so I can't say which is better on that front.

If going with Option 1, be sure that @Comma is set after @Result, otherwise you will get an extra comma at the beginning of the string.

Tested on SQL Server 2008

Trisped
  • 5,705
  • 2
  • 45
  • 58
0

I've written a udf that returns the column data as CSV

CREATE FUNCTION [dbo].[UDF_GET_COLUMNS_IDS_AS_CSV]
(

)
RETURNS VARCHAR(1000)
AS
BEGIN

DECLARE @CSV_VALUES VARCHAR(1000) = ''

--CREATE A TEMP TABLE TO STORE THE IDS
DECLARE @TEMP_TABLE TABLE(ID INT IDENTITY(1,1), COLUMN_ID INT)

--INSERT THE DATA INTO TEMP TABLE
INSERT INTO @TEMP_TABLE
SELECT COLUMNID FROM TABLE_NAME

DECLARE @LOOP_COUNTER INT =1
DECLARE @REC_COUNT INT 

SELECT @REC_COUNT = COUNT(1) FROM @TEMP_TABLE

DECLARE @TEMP_COLUMN_ID INT

--LOOP THROUGH ALL THE RECORDS
WHILE @LOOP_COUNTER <= @REC_COUNT
BEGIN
    SELECT @TEMP_COLUMN_ID = COLUMN_ID FROM @TEMP_TABLE WHERE ID = @LOOP_COUNTER

    --IF IT'S NOT THE LAST REC THEN ONLY ADD COMMA
    IF @LOOP_COUNTER < @REC_COUNT
        SET @CSV_VALUES = @CSV_VALUES + CAST(@TEMP_COLUMN_ID AS VARCHAR(100)) + ',' 
    ELSE
        SET @CSV_VALUES = @CSV_VALUES + CAST(@TEMP_COLUMN_ID AS VARCHAR(100)) 

    SET @LOOP_COUNTER = @LOOP_COUNTER + 1
END



RETURN @CSV_VALUES

END

GO
Ashish Tripathi
  • 580
  • 4
  • 18
0

Another simple approach to achieve in a single statement...

SELECT Replace((SELECT <FIELD NAME> + ', ' FROM <TABLE NAME> FOR XML PATH(''))+'{END}',', {END}','')

The {END} can be replaced with any string that wouldn't appear anywhere else in the results - it's just used to identify the last comma.