SQL Column Concatenation - sql

I want to concatenate the column names in my query result if the column is true. Below is the format of my data, and the format of the intended output as well as a snippet of the SQL I attempted.
DATA
ID | ColA | ColB | ColC | ColD
1 | T | F | T | F
2 | F | T | F | T
3 | T | F | T | F
4 | F | T | F | T
5 | T | F | T | F
INTENDED OUTPUT
ID | TYPE
1 | ColA,ColC
2 | ColB,ColD
3 | ColA,ColC
4 | ColB,ColD
5 | ColA,ColC
ATTEMPTED SQL
SELECT UniqueIDColumn,
CASE WHEN ColA = 1 THEN 'A' END + ', ' +
CASE WHEN ColB = 1 THEN 'B' END + ', ' +
CASE WHEN ColC = 1 THEN 'C' END + ', ' +
CASE WHEN ColD = 1 THEN 'D' END
FROM TableName

Easily achievable using Unpivot operator:
;WITH unpivoted AS
(
SELECT Id, Val, Col
FROM (SELECT ID, ColA, ColB, ColC, ColD FROM #Data) AS Up
UNPIVOT (Val FOR Col IN (ColA, ColB, ColC, ColD)) AS Upv
)
SELECT Id, STUFF((SELECT ', ' + Col FROM unpivoted WHERE Id = up.Id AND Val = 'T' FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS [Type]
FROM unpivoted up
GROUP BY Id

You have to write a function that receives two parameter, and, if the first parameter is true, returns the second parameter plus a delimiter (e.g., space), otherwise just the delimiter:
DROP FUNCTION IF EXISTS returnNameIfTrue;
DELIMITER $$
CREATE FUNCTION returnNameIfTrue(columnValue int, columnName varchar(10))
RETURNS varchar(10)
BEGIN
declare s varchar(10);
IF columnValue = 1 THEN
set s = concat(columnName , ' ');
else
set s = ' ';
END IF;
RETURN s;
END;
$$
DELIMITER ;
Then, the solution will be:
select id, rtrim(concat(returnNameIfTrue(colA, 'colA'),
returnNameIfTrue(colB, 'colB'),
returnNameIfTrue(colC, 'colC'),
returnNameIfTrue(colD, 'colD'))) as type
from tableName
Or, if you also want to get rid of additional space:
select id, replace(replace(replace(rtrim(concat(returnNameIfTrue(colA, 'colA'),
returnNameIfTrue(colB, 'colB'),
returnNameIfTrue(colC, 'colC'),
returnNameIfTrue(colD, 'colD'))), ' ', ' '), ' ', ' '), ' ', ' ') as type
from tableName

Related

'Unpivoting' a SQL table

I'm looking to 'unpivot' a table, though I'm not sure what the best way of going about it, is. Additionally, the values are separated by a ';'. I've listed a sample of what I'm looking at:
​
Column_A
Column_B
Column_C
Column_D
000
A;B;C;D
01;02;03;04
X;Y;D;E
001
A;B
05;06
S;T
002
C
07
S
​
From that, I'm looking for a way to unpivot it, but also to keep the relations it's currently in. As in, the first value in Column_B, C, and D are tied together:
​
|Column_A|Column_B|Column_C|Column_D|
|:-|:-|:-|:-|
|000|A|01|X|
|000|B|02|Y|
|000|C|03|D|
|000|D|04|E|
|001|A|05|S|
And so on.
My initial thought is to use a CTE, which I've set up as:
WITH TEST AS(
SELECT DISTINCT Column_A, Column_B, Column_C, VALUE AS Column_D
from [TABLE]
CROSS APPLY STRING_SPLIT(Column_D, ';'))
SELECT \* FROM TEST
;
Though that doesn't seem to produce the correct results, especially after stacking the CTEs and string splits.
As an update, there were really helpful solutions below. They all ran as expected, however I had one last addition. Is it possible/reasonable to ignore a row/column if it's blank? For example, skipping over Column_C where Column_A is '001'.
|Column_A|Column_B|Column_C|Column_D|
|:-|:-|:-|:-|
|000|A;B;C;D|01;02;03;04|X;Y;D;E|
|001|A;B||S;T|
|002|C|07|S|
Here is a JSON based method. SQL Server 2016 onwards.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ColA varchar(3), ColB varchar(8000), ColC varchar(8000), ColD varchar(8000));
INSERT INTO #tbl VALUES
('000','A;B;C;D','01;02;03;04','X;Y;D;E'),
('001','A;B','05;06','S;T'),
('002','C','07','S');
-- DDL and sample data population, end
WITH rs AS
(
SELECT *
, ar1 = '["' + REPLACE(ColB, ';', '","') + '"]'
, ar2 = '["' + REPLACE(ColC, ';', '","') + '"]'
, ar3 = '["' + REPLACE(ColD, ';', '","') + '"]'
FROM #tbl
)
SELECT ColA, ColB.[value] AS [ColB], ColC.[value] AS ColC, ColD.[value] AS ColD
FROM rs
CROSS APPLY OPENJSON (ar1, N'$') AS ColB
CROSS APPLY OPENJSON (ar2, N'$') AS ColC
CROSS APPLY OPENJSON (ar3, N'$') AS ColD
WHERE ColB.[key] = ColC.[key]
AND ColB.[key] = ColD.[key];
Output
+------+------+------+------+
| ColA | ColB | ColC | ColD |
+------+------+------+------+
| 000 | A | 01 | X |
| 000 | B | 02 | Y |
| 000 | C | 03 | D |
| 000 | D | 04 | E |
| 001 | A | 05 | S |
| 001 | B | 06 | T |
| 002 | C | 07 | S |
+------+------+------+------+
You can use a recursive CTE to walk through the strings. Assuming they are all the same length (i.e. same number of semicolons):
with cte as (
select a, convert(varchar(max), null) as b, convert(varchar(max), null) as c, convert(varchar(max), null) as d,
convert(varchar(max), b + ';') as rest_b, convert(varchar(max), c + ';') as rest_c, convert(varchar(max), d + ';') as rest_d,
0 as lev
from t
union all
select a,
left(rest_b, charindex(';', rest_b) - 1),
left(rest_c, charindex(';', rest_c) - 1),
left(rest_d, charindex(';', rest_d) - 1),
stuff(rest_b, 1, charindex(';', rest_b), ''),
stuff(rest_c, 1, charindex(';', rest_c), ''),
stuff(rest_d, 1, charindex(';', rest_d), ''),
lev + 1
from cte
where rest_b <> ''
)
select a, b, c, d
from cte
where lev > 0
order by a, lev;
Here is a db<>fiddle.
EDIT:
You can ensure filter to use rows that have the same number of semicolons by using:
where (length(b) - length(replace(b, ';', ''))) = (length(c) - length(replace(c, ';', ''))) and
(length(b) - length(replace(b, ';', ''))) = (length(d) - length(replace(d, ';', '')))
You could also extend c and d with a bunch of semicolons so no error occurs and the resulting values are empty strings. Extra semicolons in those columns don't matter, so you could use:
select a, convert(varchar(max), null) as b, convert(varchar(max), null) as c, convert(varchar(max), null) as d,
convert(varchar(max), b + ';') as rest_b, convert(varchar(max), c + replicate(';', length(b))) as rest_c, convert(varchar(max), d + replicate(';', length(b))) as rest_d,
0 as lev
The real problem here is your design. Hopefully the reason you're doing this is to fix your design.
Unfortunately you can't use SQL Server's inbuilt STRING_SPLIT for this, as it doesn't provide an ordinal position. As such I use DelimitedSplit8K_LEAD to separate the values into rows, and then "join" then back up. This does assume that all the columns have the same number of delimited values.
CREATE TABLE dbo.YourTable (ColA varchar(3),
ColB varchar(8000),
ColC varchar(8000),
ColD varchar(8000));
INSERT INTO dbo.YourTable
VALUES('000','A;B;C;D','01;02;03;04','X;Y;D;E'),
('001','A;B','05;06','S;T'),
('002','C','07','S');
GO
SELECT YT.ColA,
DSLB.Item AS ColB,
DSLC.Item AS ColC,
DSLD.Item AS ColD
FROM dbo.YourTable YT
CROSS APPLY dbo.DelimitedSplit8K_LEAD(YT.ColB,';') DSLB
CROSS APPLY dbo.DelimitedSplit8K_LEAD(YT.ColC,';') DSLC
CROSS APPLY dbo.DelimitedSplit8K_LEAD(YT.ColD,';') DSLD
WHERE DSLB.ItemNumber = DSLC.ItemNumber
AND DSLC.ItemNumber = DSLD.ItemNumber;
GO
DROP TABLE dbo.YourTable;

SQL check if fields contains same letters

I want to check if there is a row in my table that contains the same letters but in different order, but it must have the exact same letters, no more and no less.
For example, I have the letters "abc":
bca -> true
acb -> true
abcd -> **false**
ab -> **false**
Thanks!
You can use recursive CTEs to split the parameter 'abc' and each column value to letters and compare them:
with
recursive paramletters as (
select 'abc' col, 1 pos, substr('abc', 1, 1) letter
union all
select col, pos + 1, substr(col, pos + 1, 1)
from paramletters
where pos < length(col)
),
param as (
select group_concat(letter, '') over (order by letter) paramvalue
from paramletters
order by paramvalue desc limit 1
),
cteletters as (
select col, 1 pos, substr(col, 1, 1) letter
from tablename
union all
select col, pos + 1, substr(col, pos + 1, 1)
from cteletters
where pos < length(col)
),
cte as (
select * from (
select col, group_concat(letter, '') over (partition by col order by letter) colvalue
from cteletters
)
where length(colvalue) = length(col)
)
select c.col, c.colvalue = p.paramvalue result
from cte c cross join param p
See the demo.
Results:
| col | result |
| ---- | ------ |
| ab | 0 |
| abcd | 0 |
| acb | 1 |
| bca | 1 |
If the letters of the parameter are already sorted (like 'abc') then this code can be simplified to use only the last 2 CTEs.

Cache SQL query to create 1 row from multiple records

I have the below record and would like to create 1 row record.
I tried STUFF, FOR XML PATH and did not work
+-----------+-------+---------+
| CLIENT_ID | Event | DX_Code |
+-----------+-------+---------+
| 54 | 5 | F45.72 |
| 54 | 5 | X45.34 |
| 54 | 5 | M98.32 |
+-----------+-------+---------+
Output = 54, 5, F45.72 X45.34 M98.32
You can do that by using STUFF() with FOR XML PATH('') as
CREATE TABLE T
([CLIENT_ID] int, [Event] int, [DX_Code] varchar(6))
;
INSERT INTO T
([CLIENT_ID], [Event], [DX_Code])
VALUES
(54, 5, 'F45.72'),
(54, 5, 'X45.34'),
(54, 5, 'M98.32')
;
SELECT DISTINCT T1.[CLIENT_ID],
T1.[Event],
STUFF(
(
SELECT ',' + T2.[DX_Code]
FROM T T2
WHERE T2.[CLIENT_ID] = T1.[CLIENT_ID]
AND T2.[Event] = T1.[Event]
FOR XML PATH ('')
) , 1, 1, ''
) Result
FROM T T1;
This should give you the expected result
SELECT CAST(t1.CLIENT_ID AS VARCHAR) + ','+ CAST(t1.Event AS VARCHAR)+ ','+
STUFF(( SELECT ' ' + t2.DX_Code AS [text()]
FROM #temp t2
WHERE
t2.CLIENT_ID = t1.CLIENT_ID
and t2.Event = t1.Event
FOR XML PATH('')
), 1, 1, '' )
AS OutputText
FROM #temp t1
GROUP BY t1.CLIENT_ID,t1.Event
Output:
54,5,F45.72 X45.34 M98.32

Transpose rows to columns in SQL Server

is there a way to transpose this table:
A a
A b
A c
A d
B e
B f
C g
C h
C i
to this?
A B C
a e g
b f h
c i
d
Many thanks!
You can do this using ROW_NUMBER and conditional aggregation:
WITH Cte AS(
SELECT *,
rn = ROW_NUMBER() OVER(PARTITION BY col1 ORDER BY col2)
FROM #tbl
)
SELECT
A = MAX(CASE WHEN col1 = 'A' THEN col2 END),
B = MAX(CASE WHEN col1 = 'B' THEN col2 END),
C = MAX(CASE WHEN col1 = 'C' THEN col2 END)
FROM Cte
GROUP BY rn
ONLINE DEMO
Dynamic sql version for Felix Pamittan's answer
Query
DECLARE #sql AS VARCHAR(MAX);
SELECT #sql = 'WITH Cte AS(
SELECT *, rn = ROW_NUMBER() OVER(
PARTITION BY col1 ORDER BY col2
)
FROM your_table_name
) SELECT '
+ STUFF((
SELECT DISTINCT
',COALESCE(MAX(CASE col1 WHEN ''' + col1 + ''' THEN col2 END), '''') AS [' + col1 + ']'
FROM your_table_name
FOR XML PATH('')), 1, 1, '');
SELECT #sql += ' FROM cte GROUP BY rn;';
EXEC(#sql);
Result
+---+---+---+
| A | B | C |
+---+---+---+
| a | e | g |
| b | f | h |
| c | | i |
| d | | |
+---+---+---+
Find demo here

SQL Server: rows to columns

sample datas
date | num | name | value
01012012 | 1 | A | 20
01012012 | 1 | B | 30
01012012 | 2 | C | 40
wish to like this
date | num | A | B | C
01012012 | 1 | 20 | 30 | --
01012012 | 2 | -- | -- | 40
name is not fixed can allow many names
How about something like
DECLARE #Table TABLE(
date DATETIME,
num INT,
name VARCHAR(20),
value FLOAT
)
INSERT INTO #Table SELECT '20121201',1,'A',20
INSERT INTO #Table SELECT '20121201',1,'B',30
INSERT INTO #Table SELECT '20121201',2,'C',40
SELECT *
FROM (
SELECT date,
num,
name,
value
FROM #Table
) t
PIVOT (
SUM(Value) FOR name IN ([A],[B],[C])
) p
SQL Fiddle DEMO
For dynamic columns you need to use dynamic SQL
SQL FIDDLE EXAMPLE
declare
#cols nvarchar(max),
#stmt nvarchar(max)
select #cols = isnull(#cols + ', ', '') + '[' + Name + ']' from table1
select #stmt = '
select *
from table1 as T
pivot
(
max(T.value)
for name in (' + #cols + ')
) as P'
exec sp_executesql #stmt = #stmt
If you don't need a dynamic number of columns, you can use plain SQL like
select *
from table1 as T
pivot
(
max(T.value)
for name in ([A],[B],[C])
) as P