I broke this down using pivot
and not exists
. I really would handle this in the presentation layer though.
--load test data
declare @table table (c1 int, c2 int, c3 int)
insert into @table
values
(1,1,1)
,(1,1,1)
,(2,3,2)
,(4,2,4)
,(5,4,6)
,(7,5,8)
,(9,7,11)
,(11,9,13)
,(14,16,15)
--get our unique values in a cte to pivot later
;with cte as(
select
--here we add a RN so that we can use pivot without losing values
r = row_number() over (partition by Col order by (select 1))
,i.*
from
(
--for each column, we get the unique values where they don't exist in the other two columns
--we union them together, but give them 1 /2 / 3 column identifier
select
1 as Col, c1.c1 as val
from
(select distinct t1.c1 from @table t1
where not exists (select 1 from @table t2 where t2.c2 = t1.c1)
and not exists (select 1 from @table t3 where t3.c3 = t1.c1)) c1
union
select
2 as col, c2.c2
from
(select distinct t1.c2 from @table t1
where not exists (select 1 from @table t2 where t2.c1 = t1.c2)
and not exists (select 1 from @table t3 where t3.c3 = t1.c2)) c2
union
select
3 as col, c3.c3
from
(select distinct t1.c3 from @table t1
where not exists (select 1 from @table t2 where t2.c1 = t1.c3)
and not exists (select 1 from @table t3 where t3.c2 = t1.c3)) c3
) i
)
--simple pivot
select
[1], [2], [3]
from cte
pivot
(max(Val) for Col in ([1],[2],[3]))
p
RETURNS
+------+------+----+
| 1 | 2 | 3 |
+------+------+----+
| 14 | 3 | 6 |
| NULL | 16 | 8 |
| NULL | NULL | 13 |
| NULL | NULL | 15 |
+------+------+----+
Not sure what code you're using now or what version of SQL Server you're using, but FOR XML PATH
goes pretty far back. Here's how I would do it:
DECLARE @t TABLE(area int, city char(1), state char(1));
INSERT @t(area,city,state) VALUES
(1,'a','a'),(2,'a','a'),(3,'b','a'),(4,'b','a'),
(1,'a','b'),(2,'a','b'),(3,'c','b'),(4,'c','b');
;WITH x AS
(
SELECT
area = STUFF((SELECT ',' + CONVERT(varchar(11),area)
FROM @t WHERE state = t.state GROUP BY area
FOR XML PATH(''),TYPE).value(N'.[1]','nvarchar(max)'),1,1,''),
city = STUFF((SELECT ',' + city
FROM @t WHERE state = t.state GROUP BY city
FOR XML PATH(''),TYPE).value(N'.[1]','nvarchar(max)'),1,1,''),
state
FROM @t AS t
)
SELECT area,city,state
FROM x
GROUP BY area,city,state
ORDER BY area,city,state;
There is certainly a way to do this with STRING_AGG()
in more modern versions (2016 SP1+), but it won't be much less messy.
;WITH x AS
(
SELECT city,state FROM @t GROUP BY state,city
),
y AS
(
SELECT x.state, city = STRING_AGG(x.city,',')
FROM x GROUP BY state
)
SELECT area = STRING_AGG(t.area, ','), y.city, y.state
FROM y INNER JOIN @t AS t ON t.state = y.state
GROUP BY y.city, y.state
ORDER BY area, y.city, y.state;
Best Answer
This might be the cleaner approach you're after. Basically, check if the variable has been initialized yet. If it hasn't, set it to the empty string, and append the first city (no leading comma). If it has, then append a comma, then append the city.
Of course, that only works for populating a variable per state. If you are pulling the list for each state one at a time, there is a better solution in one shot:
Results:
To order by city name within each state:
In Azure SQL Database or SQL Server 2017+, you can use the new
STRING_AGG()
function:And ordered by city name: