52

I have a sql function that includes this code:

DECLARE @CodeNameString varchar(100)

SELECT CodeName FROM AccountCodes ORDER BY Sort

I need to concatenate all results from the select query into CodeNameString.

Obviously a FOREACH loop in C# code would do this, but how do I do it in SQL?

OMG Ponies
  • 325,700
  • 82
  • 523
  • 502
Matthew Jones
  • 25,644
  • 17
  • 102
  • 155
  • 1
    What **version** of SQL Server?? – marc_s Mar 04 '11 at 16:19
  • 1
    possible duplicate of [SQL Server: Can I Comma Delimit Multiple Rows Into One Column?](http://stackoverflow.com/questions/2046037/sql-server-can-i-comma-delimit-multiple-rows-into-one-column) and: http://stackoverflow.com/questions/1817985/how-do-i-create-a-comma-separated-list-using-a-sql-query – OMG Ponies Mar 04 '11 at 16:23
  • answered many times... but watch out, not all FOR XML PATH concatenations implementations will properly handle the XML special characters (<, &, >, etc) like my answer to a previous answer will: http://stackoverflow.com/questions/5031204/does-t-sql-have-an-aggregate-function-to-concatenate-strings/5031297#5031297 – KM. Mar 04 '11 at 19:26

6 Answers6

98

If you're on SQL Server 2005 or up, you can use this FOR XML PATH & STUFF trick:

DECLARE @CodeNameString varchar(100)

SELECT 
   @CodeNameString = STUFF( (SELECT ',' + CodeName 
                             FROM dbo.AccountCodes 
                             ORDER BY Sort
                             FOR XML PATH('')), 
                            1, 1, '')

The FOR XML PATH('') basically concatenates your strings together into one, long XML result (something like ,code1,code2,code3 etc.) and the STUFF puts a "nothing" character at the first character, e.g. wipes out the "superfluous" first comma, to give you the result you're probably looking for.

UPDATE: OK - I understand the comments - if your text in the database table already contains characters like <, > or &, then my current solution will in fact encode those into &lt;, &gt;, and &amp;.

If you have a problem with that XML encoding - then yes, you must look at the solution proposed by @KM which works for those characters, too. One word of warning from me: this approach is a lot more resource and processing intensive - just so you know.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • 16
    **this will NOT properly handle the XML special characters ( `<`, `&` , `>` , etc)** like my previous answer will, see here: http://stackoverflow.com/questions/5031204/does-t-sql-have-an-aggregate-function-to-concatenate-strings/5031297#5031297 – KM. Mar 04 '11 at 19:24
  • @marc_s This is dangerous. I must echo KM's comment above. – jnm2 Mar 30 '16 at 14:00
  • 1
    @jnm2: agreed - **if** someone needs those special characters, then the solution by "KM" is the way to go. **HOWEVER:** it's **A LOT** more processing intensive and thus I'd recommend using it **only** if you really must have those few special XML characters in your output – marc_s Mar 30 '16 at 14:12
  • @marc_s For most use cases, how can you know ahead of time if these characters will ever show up in the future? Making the assumption that you won't need them is what is dangerous. Can you show documentation on KM's method being a lot more resource intensive? I don't see why it would be. – jnm2 Mar 30 '16 at 14:15
  • @jnm2: just profile the two approaches in Mgmt Studio - KM's approach has an estimated subtree cost of 1.106, while mine has 0.015 - about 100x less ..... – marc_s Mar 30 '16 at 14:16
  • I'm getting ~15.6x less, but it's drowned out to insignificance whenever it's used within a real query. However, I wonder if there is a better performing way to properly handle XML characters. There's an inner join to an expensive XML Reader tvf. – jnm2 Mar 30 '16 at 14:21
  • 2
    @jnm2: yeah, I wish Microsoft would listen and finally give us a **built-in** (and optimized!) function to handle this.... – marc_s Mar 30 '16 at 14:23
  • @marc_s Found a better-performing version, only ~1.08x slower: `for xml path(''), type).value('(./text())[1]', 'nvarchar(max)')` – jnm2 Mar 30 '16 at 14:32
  • @marc_s I think 1.08x slower is worth it... I can't see any good coming from people deciding that they will not store ampersands or angle brackets. – jnm2 Mar 30 '16 at 14:42
  • @marc_s Is the `ORDER BY Sort` line important? – userSteve Apr 06 '17 at 16:45
  • @userSteve: not if you don't care about the sort order - I just had it in there since the OP also had this `ORDER BY` in his question – marc_s Apr 06 '17 at 17:06
30
DECLARE @CodeNameString varchar(max)
SET @CodeNameString=''

SELECT @CodeNameString=@CodeNameString+CodeName FROM AccountCodes ORDER BY Sort
SELECT @CodeNameString
Alex
  • 14,338
  • 5
  • 41
  • 59
  • Simple and elegant answer. Are there any limitations to the use of this method ? (NULL values are handled by James Wisemann 's variation on this answer) – Pac0 Aug 18 '17 at 14:44
  • if you use this with an integer column it will try to Add them together and fail. use CAST(int_col_name as varchar(10)) – Andrew Feb 17 '22 at 16:21
  • This answer is useful because you can format it further by concatenating any string text you want, eg, '(' + myCol + '_' + myOtherCol + ')' – Andrew Feb 17 '22 at 16:22
14

@AlexanderMP's answer is correct, but you can also consider handling nulls with coalesce:

declare @CodeNameString  nvarchar(max)
set @CodeNameString = null
SELECT @CodeNameString = Coalesce(@CodeNameString + ', ', '') + cast(CodeName as varchar) from AccountCodes  
select @CodeNameString
James Wiseman
  • 29,946
  • 17
  • 95
  • 158
  • 1
    Yes, I forgot about coalesce/isnull. But wouldn't it make better sense to check `CodeName` instead of `@CodeNameString`? One could lose data this way. Better yet, instead of coaleste, it would make sense to use plain old `Where CodeName is not null`. – Alex Mar 04 '11 at 17:52
  • this is definitely the best and most elegant solution, thanks a lot! – Federico Caccia Mar 15 '20 at 18:45
5

For SQL Server 2005 and above use Coalesce for nulls and I am using Cast or Convert if there are numeric values -

declare @CodeNameString  nvarchar(max)
select  @CodeNameString = COALESCE(@CodeNameString + ',', '')  + Cast(CodeName as varchar) from AccountCodes  ORDER BY Sort
select  @CodeNameString
Vishal
  • 12,133
  • 17
  • 82
  • 128
2

from msdn Do not use a variable in a SELECT statement to concatenate values (that is, to compute aggregate values). Unexpected query results may occur. This is because all expressions in the SELECT list (including assignments) are not guaranteed to be executed exactly once for each output row

The above seems to say that concatenation as done above is not valid as the assignment might be done more times than there are rows returned by the select

peter
  • 21
  • 1
0

Here is another real life example that works fine at least with 2008 release (and later).

This is the original query which uses simple max() to get at least one of the values:

SELECT option_name, Field_M3_name, max(Option_value) AS "Option value", max(Sorting) AS "Sorted"
FROM Value_list group by Option_name, Field_M3_name
ORDER BY option_name, Field_M3_name

Improved version, where the main improvement is that we show all values comma separated:

SELECT from1.keys, from1.option_name, from1.Field_M3_name,

 Stuff((SELECT DISTINCT ', ' + [Option_value] FROM Value_list from2
  WHERE COALESCE(from2.Option_name,'') + '|' + COALESCE(from2.Field_M3_name,'') = from1.keys FOR XML PATH(''),TYPE)
  .value('text()[1]','nvarchar(max)'),1,2,N'') AS "Option values",

 Stuff((SELECT DISTINCT ', ' + CAST([Sorting] AS VARCHAR) FROM Value_list from2
  WHERE COALESCE(from2.Option_name,'') + '|' + COALESCE(from2.Field_M3_name,'') = from1.keys FOR XML PATH(''),TYPE)
  .value('text()[1]','nvarchar(max)'),1,2,N'') AS "Sorting"

FROM ((SELECT DISTINCT COALESCE(Option_name,'') + '|' + COALESCE(Field_M3_name,'') AS keys, Option_name, Field_M3_name FROM Value_list)
-- WHERE
) from1
ORDER BY keys

Note that we have solved all possible NULL case issues that I can think of and also we fixed an error that we got for numeric values (field Sorting).

drs
  • 5,679
  • 4
  • 42
  • 67
Timo Riikonen
  • 477
  • 5
  • 8