views:

10510

answers:

8

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
A: 

I've looked for ways to do this before, and besides writing a stored proc, there isn't an easy way to do it. I resort to writing a script or using an ETL tool like Talend to do this.

Dana the Sane
+2  A: 

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

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

and

http://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.

Cade Roux
+2  A: 

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?"

Bill Karwin
A: 

Just to add to what Cade said, this is usually a front-end display thing and should therefor 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.

Tom H.
A: 

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
David B
+27  A: 

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. Originally 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]
    , REPLACE(RTRIM((SELECT [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) + ' ' FROM #YourTable WHERE (ID = Results.ID) FOR XML PATH (''))),' ',', ') AS NameValues
FROM #YourTable Results
GROUP BY ID

DROP TABLE #YourTable

UPDATE: I'm sure there's a better way to get around the string manipulation slight-of-hand going on there, but I can't think of it at the moment. If I use ', ' as the deliminator, it's going to end with a comma. My cheesy solution for this is to use a space as the deliminator. It'll still end with the deliminator (space), but RTRIM gets rid of it. Then I use REPLACE to change it from a single space to a comma and then a space. Seems a bit obtuse of a solution, but it works for the sample data :-P

Kevin Fairchild
why would one nolock a temp table?
David B
Force of habit. Suppose I can get rid of that. lol
Kevin Fairchild
Seriously (I know I just commented a second ago)... but that's brilliant. This is the first time in a LONG time I've learned something in SQL from someone else.
Timothy Khouri
lol. Thanks, Timothy. I'm a sucker for puzzles. Especially when folks say it can't be done :)
Kevin Fairchild
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.
@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
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
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.
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
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.
+1  A: 

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
A: 

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