SQL server how to change pipe separated column string to rows - sql

I have 3 columns in a table, in which 2 columns have string separated by a '|' pipe. Both these columns values depend on each other.
For an example: I have data in the table like this :
ID product quantity
1 A|B|C 1|2|3
2 X|Y|Z 7|8|9
I would like to change it to something like this :
ID product quantity
1 A 1
1 B 2
1 C 3
2 X 7
2 Y 8
2 Z 9
As i am working with SSMS, i don't have any other choice except SQL. I try to use cross apply but i am not getting right result. For 1 row i receive 9 rows instead of getting 3.
could anyone suggest me which method should i use?
Thank you in advance!!
JACK

This is rather tricky, because you need for the values to match up. The following takes a recursive CTE approach:
with cte as (
select id,
left(product, charindex('|', product + '|') - 1) as product,
left(quantity, charindex('|', quantity + '|') - 1) as quantity,
substring(product, charindex('|', product + '|') + 1, 1000) as products,
substring(quantity, charindex('|', quantity + '|') + 1, 1000) as quantities
from t
union all
select id,
left(products, charindex('|', products + '|') - 1) as product,
left(quantities, charindex('|', quantities + '|') - 1) as quantity,
substring(products, charindex('|', products + '|') + 1, 1000) as products,
substring(quantities, charindex('|', quantities + '|') + 1, 1000) as quantities
from cte
where products <> '' and quantities <> ''
)
select id, product, quantity
from cte;
Here is a little Rextester.

Test Data
CREATE TABLE #t (ID INT, product VARCHAR(100) , quantity VARCHAR(100) )
INSERT INTO #t VALUES
(1 ,'A|B|C' , '1|2|3'),
(2 ,'X|Y|Z' , '7|8|9');
Query
WITH Products AS (
SELECT ID
, Product_Split.a.value('.', 'VARCHAR(100)') Products
, ROW_NUMBER() OVER (PARTITION BY ID ORDER BY (SELECT NULL)) rn
FROM (
SELECT ID
,Cast ('<X>'
+ Replace(product, '|', '</X><X>')
+ '</X>' AS XML) AS Product_Data
FROM #t
) AS t
CROSS APPLY Product_Data.nodes ('/X') AS Product_Split(a)
),
Quantities AS (
SELECT ID
, Quantity_Split.a.value('.', 'VARCHAR(100)') Quantity
, ROW_NUMBER() OVER (PARTITION BY ID ORDER BY (SELECT NULL)) rn
FROM (
SELECT ID
,Cast ('<X>'
+ Replace(quantity, '|', '</X><X>')
+ '</X>' AS XML) AS Quantity_Data
FROM #t
) AS t
CROSS APPLY Quantity_Data.nodes ('/X') AS Quantity_Split(a)
)
SELECT t.ID
, P.Products
, Q.Quantity
FROM #t t
LEFT JOIN Products P ON t.ID = p.ID
LEFT JOIN Quantities Q ON Q.ID = t.ID
AND Q.rn = p.rn
Result Set
╔════╦══════════╦══════════╗
║ ID ║ Products ║ Quantity ║
╠════╬══════════╬══════════╣
║ 1 ║ A ║ 1 ║
║ 1 ║ B ║ 2 ║
║ 1 ║ C ║ 3 ║
║ 2 ║ X ║ 7 ║
║ 2 ║ Y ║ 8 ║
║ 2 ║ Z ║ 9 ║
╚════╩══════════╩══════════╝

splitting strings is easy, there are tons of examples. The tricky part here is to connect the fragments via their position. My suggestion uses XMLs abilities to target an element by its position:
DECLARE #tbl TABLE(ID INT, product VARCHAR(100) , quantity VARCHAR(100) )
INSERT INTO #tbl VALUES
(1 ,'A|B|C' , '1|2|3')
,(2 ,'X|Y|Z' , '7|8|9');
--This is the query
WITH CastedToXML AS
(
SELECT *
,CAST('<x>' + REPLACE(product,'|','</x><x>') + '</x>' AS XML) AS ProductXml
,CAST('<x>' + REPLACE(quantity,'|','</x><x>') + '</x>' AS XML) AS QuantityXml
FROM #tbl
)
SELECT *
,ProductXml.value('/x[sql:column("Nmbr")][1]','nvarchar(10)') AS ProductAtPosition
,QuantityXml.value('/x[sql:column("Nmbr")][1]','int') AS QuantityAtPosition
FROM CastedToXML
--Create a set of running numbers (spt_values is just a pre-filled table with many rows)
CROSS APPLY (SELECT TOP(CastedToXML.ProductXml.value('count(/x)','int'))
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values) AS Tally(Nmbr);
the result
+----+------+-------------------+--------------------+
| ID | Nmbr | ProductAtPosition | QuantityAtPosition |
+----+------+-------------------+--------------------+
| 1 | 1 | A | 1 |
+----+------+-------------------+--------------------+
| 1 | 2 | B | 2 |
+----+------+-------------------+--------------------+
| 1 | 3 | C | 3 |
+----+------+-------------------+--------------------+
| 2 | 1 | X | 7 |
+----+------+-------------------+--------------------+
| 2 | 2 | Y | 8 |
+----+------+-------------------+--------------------+
| 2 | 3 | Z | 9 |
+----+------+-------------------+--------------------+
Some explanation:
the cast to xml transfers your A|B|C to
<x>A</x>
<x>B</x>
<x>C</x>
This list is joined with a number set created on the fly using the count of <x> as the TOP limit.
Now it is easy to pick the <x> out of your XML by the position.
Try it out!
UPDATE: Non-unique IDs
DECLARE #tbl TABLE(ID INT, product VARCHAR(100) , quantity VARCHAR(100) )
INSERT INTO #tbl VALUES
(1 ,'A|B|C' , '1|2|3')
,(2 ,'X|Y|Z' , '7|8|9')
,(3 ,'a|b|c' , '7|8|9')
,(2 ,'D|e|f' , '7|8|9')
;
--This is the query
WITH CastedToXML AS
(
SELECT *
,ROW_NUMBER() OVER(PARTITION BY ID ORDER BY ID) AS RowIndex
,CAST('<x>' + REPLACE(product,'|','</x><x>') + '</x>' AS XML) AS ProductXml
,CAST('<x>' + REPLACE(quantity,'|','</x><x>') + '</x>' AS XML) AS QuantityXml
FROM #tbl
)
SELECT *
,ProductXml.value('/x[sql:column("Nmbr")][1]','nvarchar(10)') AS ProductAtPosition
,QuantityXml.value('/x[sql:column("Nmbr")][1]','int') AS QuantityAtPosition
FROM CastedToXML
--Create a set of running numbers (spt_values is just a pre-filled table with many rows)
CROSS APPLY (SELECT TOP(CastedToXML.ProductXml.value('count(/x)','int'))
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM master..spt_values) AS Tally(Nmbr);

Related

String split on multiple fields to one result

I have a table that looks like the below set as a field for one value:
|---------------------|------------------|------------------|
| Colour | Amount | Size |
|---------------------|------------------|------------------|
| Black,Blue,Green | 1,2,2 | 100,100,100 |
|---------------------|------------------|------------------|
I need to do a string split on each of them and return it in one go.
I've currently got this and works for colour:
SELECT value as colour
FROM [table_name]
CROSS APPLY STRING_SPLIT(colour, ',')
I can't figure out how to do multiple string splits in one go. It should return it as this then:
|---------------------|------------------|------------------|
| Colour | Amount | Size |
|---------------------|------------------|------------------|
| Black | 1 | 100 |
|---------------------|------------------|------------------|
| Blue | 2 | 100 |
|---------------------|------------------|------------------|
| Green | 2 | 100 |
|---------------------|------------------|------------------|
Any help would be great!
Unfortunately, string_split() doesn't provide the option to preserve the order of the substrings it produces. Hence, that is very, very tricky to use in this case.
I prefer a recursive CTE (until the function gets fixed):
with cte as (
select convert(varchar(max), null) as color,
convert(varchar(max), null) as amount,
convert(varchar(max), null) as size,
convert(varchar(max), colors + ',') as rest_colors,
convert(varchar(max), amounts + ',') as rest_amounts ,
convert(varchar(max), sizes + ',') as rest_sizes,
0 as lev
from t
union all
select left(rest_colors, charindex(',', rest_colors) - 1),
left(rest_amounts, charindex(',', rest_amounts) - 1),
left(rest_sizes, charindex(',', rest_sizes) - 1),
stuff(rest_colors, 1, charindex(',', rest_colors), ''),
stuff(rest_amounts, 1, charindex(',', rest_amounts), ''),
stuff(rest_sizes, 1, charindex(',', rest_sizes), ''),
lev + 1
from cte
where rest_colors <> ''
)
select color, amount, size
from cte
where lev > 0;
Here is a db<>fiddle.
As Gordon mentioned, string_split() does not GTD sequence. That said, and if you are open to a Table-Value Function, consider the following where we UNPIVOT your data and then apply a final PIVOT. Note: The RN = ... could be replaced with your own ID (if you have one)
I adjusted the values to illustrate there is a proper sequencing.
Example
;with cte as (
Select RN = row_number() over (order by (select null))
,[Colour]
,[Amount]
,[Size]
From YourTable
)
Select *
From (
Select RN,Item='Colour',B.* From cte Cross Apply [dbo].[tvf-Str-Parse](Colour,',') B
Union All
Select RN,Item='Amount',B.* From cte Cross Apply [dbo].[tvf-Str-Parse](Amount,',') B
Union All
Select RN,Item='Size' ,B.* From cte Cross Apply [dbo].[tvf-Str-Parse](Size ,',') B
) src
Pivot ( max(RetVal) for Item in ([Colour],[Amount],[Size] ) ) pvt
Returns
RN RetSeq Colour Amount Size
1 1 Black 1 100
1 2 Blue 2 200
1 3 Green 3 300
The Function if Interested
CREATE FUNCTION [dbo].[tvf-Str-Parse] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select RetSeq = row_number() over (order by 1/0)
,RetVal = ltrim(rtrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(#String,#Delimiter,'§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);

Split string into multiple rows with multiple columns in paired

I have a table as below:
Order No | Customers | Amount
---------+----------------------+-------------
1 | Briant~~Luck | 23~~2122
2 | Mike~~Lee~~David | 10~~200~37
3 | Stak | 100
With format, each customer has one value in Amount.
I'm trying to figure out how to expand the ~~ delimited values to populate a new customers table, which should look like this:
Order No | Customer | Amount
---------+----------------------+---------
1 | Briant | 23
1 | Luck | 2122
2 | Mike | 10
2 | Lee | 200
2 | David | 37
3 | Stak | 100
How can I do?
Any solution in SQL query, function or cursor is appreciated.
Thanks
I think you could store data as your expected result structure. It is much better.
Btw you could use a split function to get your output
DECLARE #SampleData AS TABLE
(
OrderNo int,
Customers varchar(200),
Amount varchar(200)
)
INSERT INTO #SampleData
(
OrderNo,
Customers,
Amount
)
VALUES
( 1, 'Briant~~Luck','23~~2122'),
( 2, 'Mike~~Lee~~David','10~~200~~37'),
( 3, 'Stak','100')
SELECT sd.OrderNo, c.[Value] AS Customer, a.[Value] AS Amount
FROM #SampleData sd
CROSS APPLY
(
SELECT Pos, Value
FROM [dbo].[SplitString](sd.Customers,'~~')
) c
CROSS APPLY
(
SELECT Pos, Value
FROM [dbo].[SplitString](sd.Amount,'~~')
) a
WHERE c.Pos = a.Pos
ORDER BY sd.OrderNo
Split function
CREATE FUNCTION [dbo].[SplitString] (#Text varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select Pos = Row_Number() over (Order By (Select null))
,Value = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>'+ Replace(#Text,#Delimiter,'</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);
Demo link: http://rextester.com/XRX32958
This solution uses XML, CROSS APPLY & ROW_NUMBER to deconstruct the '~~' seperated fields.
It doesn't require a UDF or the STRING_SPLIT function from SQL Server 2016.
-- Using a table variable for the test
declare #Orders table ([Order No] int, Customers varchar(30), Amount varchar(30));
insert into #Orders ([Order No], Customers, Amount) values
(1,'Briant~~Luck','23~~2122'),
(2,'Mike~~Lee~~David','10~~200~~37'),
(3,'Stak','100');
SELECT C.[Order No], C.Customer, A.Amount
FROM
(
SELECT
[Order No],
row_number() over (partition by [Order No] order by (select 1)) as rn,
Customers.Name.value('.', 'VARCHAR(100)') AS Customer
FROM (
SELECT [Order No], CAST ('<x>' + REPLACE(Customers, '~~', '</x><x>') + '</x>' AS XML) AS XCustomers
FROM #Orders
) AS Q1
CROSS APPLY Q1.XCustomers.nodes ('/x') AS Customers(Name)
) C
JOIN (
SELECT
[Order No],
row_number() over (partition by [Order No] order by (select 1)) as rn,
Amounts.Value.value('.', 'VARCHAR(100)') AS Amount
FROM (
SELECT [Order No], CAST ('<x>' + REPLACE(Amount, '~~', '</x><x>') + '</x>' AS XML) AS XAmounts
FROM #Orders
) AS Q1
CROSS APPLY Q1.XAmounts.nodes ('/x') AS Amounts(Value)
) A
ON (C.[Order No] = A.[Order No] AND C.RN = A.RN);
Or if you know there will be maximum 3 values in those strings.
Then the trick with PARSENAME could be used:
SELECT [Order No],
PARSENAME(REPLACE(Customers, '~~', '.'), v.n) as Customer,
PARSENAME(REPLACE(Amount, '~~', '.'), v.n) as Amount
FROM #Orders t
JOIN (VALUES (1),(2),(3)) AS v(n)
ON v.n <= (len(Customers) - len(replace(Customers, '~~', ','))+1);
If you are on SQL2016+ then try this:
drop table if exists dbo.Orders;
create table dbo.Orders (
ID int
, Customers varchar(100)
, Amount varchar(100)
);
insert into dbo.Orders (ID, Customers, Amount)
values (1,'Briant~~Luck', '23~~2122')
, (2, 'Mike~~Lee~~David', '10~~200~~37')
, (3, 'Stak', '100');
select
o.ID
, t01.value as Customer
, t02.value as Amount
from dbo.Orders o
outer apply (
select
ROW_NUMBER () over (order by o.Customers ASC) as Rbr
, t.value
from string_split (replace(o.Customers, '~~', '~'), '~') t
) t01
outer apply (
select
ROW_NUMBER () over (order by o.Amount ASC) as Rbr
, t.value
from string_split (replace(o.Amount, '~~', '~'), '~') t
) t02
where t01.Rbr = t02.Rbr
If you are on a version of SQL Server prior to 2016, you will need to create your own String Splitting function and reference that in your script. The version I use is as follows:
create function [dbo].[StringSplit]
(
#str nvarchar(4000) = ' ' -- String to split.
,#delimiter as nvarchar(1) = ',' -- Delimiting value to split on.
,#num as int = null -- Which value to return.
)
returns table
as
return
( -- Start tally table with 10 rows.
with n(n) as (select n from (values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n(n))
-- Select the same number of rows as characters in #str as incremental row numbers.
-- Cross joins increase exponentially to a max possible 10,000 rows to cover largest #str length.
,t(t) as (select top (select len(#str) a) row_number() over (order by (select null)) from n n1,n n2,n n3,n n4)
-- Return the position of every value that follows the specified delimiter.
,s(s) as (select 1 union all select t+1 from t where substring(#str,t,1) = #delimiter)
-- Return the start and length of every value, to use in the SUBSTRING function.
-- ISNULL/NULLIF combo handles the last value where there is no delimiter at the end of the string.
,l(s,l) as (select s,isnull(nullif(charindex(#delimiter,#str,s),0)-s,4000) from s)
select rn as ItemNumber
,Item
from(select row_number() over(order by s) as rn
,substring(#str,s,l) as item
from l
) a
where rn = #num -- Return a specific value where specified,
or #num is null -- Or the everything where not.
)
And is used as follows. Note that I have split the outer apply into separate queries to avoid row duplication:
declare #t table(OrderNo int,Customers nvarchar(500),Amount nvarchar(500));
insert into #t values
(1,'Briant~~Luck','23~~2122')
,(2,'Mike~~Lee~~David','10~~200~~37')
,(3,'Stak','100');
with c as
(
select t.OrderNo
,c.ItemNumber
,c.Item as Customers
from #t t
outer apply dbo.StringSplit(replace(t.Customers,'~~','|'),'|',null) c
),a as
(
select t.OrderNo
,a.ItemNumber
,a.Item as Amount
from #t t
outer apply dbo.StringSplit(replace(t.Amount,'~~','|'),'|',null) a
)
select c.OrderNo
,c.Customers
,a.Amount
from c
join a
on(c.OrderNo = a.OrderNo
and c.ItemNumber = a.ItemNumber
)
order by a.OrderNo
,c.Customers;
Output:
+---------+-----------+--------+
| OrderNo | Customers | Amount |
+---------+-----------+--------+
| 1 | Briant | 23 |
| 1 | Luck | 2122 |
| 2 | David | 37 |
| 2 | Lee | 200 |
| 2 | Mike | 10 |
| 3 | Stak | 100 |
+---------+-----------+--------+
Here solution as plsql
declare
type t_array_text is table of varchar2(30);
type t_array_number is table of number;
array_text t_array_text := t_array_text();
array_number t_array_number := t_array_number();
i number := 1;
cursor c1 is
select * from deneme;
begin
FOR c in c1
LOOP
i := 1;
array_text := t_array_text();
array_number := t_array_number();
for rec in (
SELECT regexp_substr(c.customer, '[[:alpha:]]+', 1, level) a frOM dual
CONNECT BY level<=regexp_count(c.customer,'~~')+1)
loop
array_text.extend();
array_text (i) := rec.a;
i := i + 1;
end loop;
i := 1;
for rec in (
SELECT regexp_substr(c.amount, '[0-9]+', 1, level) a frOM dual
CONNECT BY level<=regexp_count(c.amount,'~~')+1)
loop
array_number.extend();
array_number (i) := rec.a;
i := i + 1;
end loop;
for y in 1..array_text.count loop
dbms_output.put_line (c.order_no || ' ' || array_text(y) || ' ' || array_number(y));
end loop;
END LOOP;
end;
result as follows:
1 Briant 23
1 Luck 2122
2 Mike 10
2 Lee 200
2 David 37
3 Stak 10

Multiple CSV Column to Row SQL Query

I have following data
I want to Convert Column to Row
I have use following Query ::
Select Distinct [ApplicantID],
Split.a.value('.', 'VARCHAR(100)') AS Subjects,
hoursperweek,[Days],[Time]
From ( Select
Cast ('<M>' + REPLACE([Subjects], ',', '</M><M>') + '</M>' AS XML)
AS Subjects , ApplicantID, hoursperweek,[Days],[Time]
From [dbo].[Progrm])
AS A CROSS APPLY Subjects.nodes ('/M') AS Split(a)
It give me following Result
but I want to seprate Subjects, Hoursperweek, days, time. How can I do that ?
ApplicantID Subjects HoursPerWeek Days Time
1 a 12 Sun 12:45:12
1 b 25 Mon 14:45:12
Here is a function to split the Comma separated values into Rows
CREATE FUNCTION dbo.Splitstrings (#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255))
RETURNS #split_tab TABLE (
RN INT IDENTITY(1, 1),
SPLIT_VAL VARCHAR(100))
WITH SCHEMABINDING
AS
BEGIN ;
;WITH E1(N)
AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
E2(N) AS (SELECT 1 FROM E1 a,E1 b),
E4(N) AS (SELECT 1 FROM E2 a, E2 b),
E42(N) AS (SELECT 1 FROM E4 a, E2 b),
cteTally(N)
AS (SELECT 0
UNION ALL
SELECT TOP (Datalength(Isnull(#List, 1))) Row_number()OVER (
ORDER BY (SELECT NULL))
FROM E42),
cteStart(N1)
AS (SELECT t.N + 1
FROM cteTally t
WHERE ( Substring(#List, t.N, 1) = #Delimiter
OR t.N = 0 ))
INSERT INTO #split_tab
SELECT SPLIT_VAL= Substring(#LIST, S.N1, Isnull(NULLIF(Charindex(#DELIMITER, #LIST, S.N1), 0) - S.N1, 8000))
FROM CTESTART S;
RETURN
END
Now the ugly code to split the data
;WITH data
AS (SELECT 1 ApplicantID,
'a,b' Subjects,
'12,25' HoursPerWeek,
'sun,mon' Days,
'12:45:12,14:45:12' Time),
sub
AS (SELECT RN,
ApplicantID,
Subjects=SPLIT_VAL
FROM data
CROSS apply Splitstrings (Subjects, ',')),
HPW
AS (SELECT RN,
ApplicantID,
HoursPerWeek=SPLIT_VAL
FROM data
CROSS apply Splitstrings (HoursPerWeek, ',')),
Days
AS (SELECT RN,
ApplicantID,
Days=SPLIT_VAL
FROM data
CROSS apply Splitstrings (Days, ',')),
Time
AS (SELECT RN,
ApplicantID,
Time=SPLIT_VAL
FROM data
CROSS apply Splitstrings (Time, ','))
SELECT D.APPLICANTID,
S.SUBJECTS,
H.HOURSPERWEEK,
DA.DAYS,
T.TIME
FROM DATA D
FULL OUTER JOIN SUB S
ON D.APPLICANTID = S.APPLICANTID
FULL OUTER JOIN HPW H
ON D.APPLICANTID = H.APPLICANTID
AND H.RN = S.RN
FULL OUTER JOIN DAYS DA
ON D.APPLICANTID = DA.APPLICANTID
AND DA.RN = COALESCE(S.RN, H.RN)
FULL OUTER JOIN TIME T
ON D.APPLICANTID = T.APPLICANTID
AND T.RN = COALESCE(S.RN, H.RN, DA.RN)
Result:
╔═════════════╦══════════╦══════════════╦══════╦══════════╗
║ APPLICANTID ║ SUBJECTS ║ HOURSPERWEEK ║ DAYS ║ TIME ║
╠═════════════╬══════════╬══════════════╬══════╬══════════╣
║ 1 ║ a ║ 12 ║ sun ║ 12:45:12 ║
║ 1 ║ b ║ 25 ║ mon ║ 14:45:12 ║
╚═════════════╩══════════╩══════════════╩══════╩══════════╝
This is the reason we should never store comma separated values into single column. When you want to parse the data things get difficult and we need to such ugly codes to parse the data

SQL Server - Complex Dynamic Pivot multiple columns

FYI, this question is already answered but I have some new requirements which is very complex to implement so, I am posting it as a new question instead of editing the old question: (Previous Question)
I have two tables "Controls" and "ControlChilds" (in the ControlChilds table we have added a new column called ControlChildComments which we need to show in PIVOT output)
Parent Table Structure:
Create table Controls(
ProjectID Varchar(20) NOT NULL,
ControlID INT NOT NULL,
ControlCode Varchar(2) NOT NULL,
ControlPoint Decimal NULL,
ControlScore Decimal NULL,
ControlValue Varchar(50)
)
Sample Data
ProjectID | ControlID | ControlCode | ControlPoint | ControlScore | ControlValue
P001 1 A 30.44 65 Invalid
P001 2 C 45.30 85 Valid
Child Table Structure:
Create table ControlChilds(
ControlID INT NOT NULL,
ControlChildID INT NOT NULL,
ControlChildValue Varchar(200) NULL,
ControlChildComments Varchar(200) NULL
)
Sample Data
ControlID | ControlChildID | ControlChildValue | ControlChildComments
1 100 Yes Something
1 101 No NULL
1 102 NA Others
1 103 Others NULL
2 104 Yes New one
2 105 SomeValue NULL
Based on my previous question (Previous Question) I got this output (You can refer to the PIVOT queries which produces this output in the answer given by #bluefeet. Thanks again #bluefeet.)
But now my requirement is changed and I need ControlChildComments after each Child values. For example, A_Child1, A_Child1Comments, A_Child2, A_Child2Comments etc...
Another tricky thing is I need to show the comments only when they are not null otherwise I shouldn't show the column. For example, in this case, it should be like this:
A_Child1, A_Child1Comments, A_Child2, A_Child3, A_Child3Comments, A_Child4, C_Child1, C_Child1Comments, C_Child2
Is this possible? I tried lot of things but the results are not accurate.
Since you now have multiple columns in your ControlChilds table that you need to PIVOT, you will need to use the similar method of unpivoting them first that you applied with the Controls table.
You will need to unpivot both the ChildControlValue and ChildControlComments using code similar to:
select
projectId,
col = ControlCode+'_'+subCol+cast(seq as varchar(10)),
value
from
(
select c.ProjectId,
c.ControlCode,
cc.ControlChildValue,
cc.ControlChildComments,
row_number() over(partition by c.ProjectId, c.ControlCode
order by cc.ControlChildId) seq
from controls c
inner join controlchilds cc
on c.controlid = cc.controlid
) d
cross apply
(
select 'ChildValue', ControlChildValue union all
select 'ChildComments', ControlChildComments
) c (subCol, value);
See SQL Fiddle with Demo. This gets your data in the format:
| PROJECTID | COL | VALUE |
|-----------|------------------|-----------|
| P001 | A_ChildValue1 | Yes |
| P001 | A_ChildComments1 | Something |
| P001 | A_ChildValue2 | No |
| P001 | A_ChildComments2 | (null) |
| P001 | A_ChildValue3 | NA |
You then use this code in your existing query:
select ProjectId,
A_ControlPoint, A_ControlScore, A_ControlValue,
A_ChildValue1, A_ChildComments1, A_ChildValue2,
A_ChildComments2, A_ChildValue3, A_ChildComments3,
A_ChildValue4, A_ChildComments4,
C_ControlPoint, C_ControlScore, C_ControlValue,
C_Child1, C_Child2
from
(
select
ProjectId,
col = ControlCode +'_'+col,
val
from
(
select
c.ProjectId,
c.ControlCode,
c.ControlPoint,
c.ControlScore,
c.ControlValue
from controls c
) d
cross apply
(
select 'ControlPoint', cast(controlpoint as varchar(10)) union all
select 'ControlScore', cast(ControlScore as varchar(10)) union all
select 'ControlValue', ControlValue
) c (col, val)
union all
select
projectId,
col = ControlCode+'_'+subCol+cast(seq as varchar(10)),
value
from
(
select c.ProjectId,
c.ControlCode,
cc.ControlChildValue,
cc.ControlChildComments,
row_number() over(partition by c.ProjectId, c.ControlCode
order by cc.ControlChildId) seq
from controls c
inner join controlchilds cc
on c.controlid = cc.controlid
) d
cross apply
(
select 'ChildValue', ControlChildValue union all
select 'ChildComments', ControlChildComments
) c (subCol, value)
) src
pivot
(
max(val)
for col in (A_ControlPoint, A_ControlScore, A_ControlValue,
A_ChildValue1, A_ChildComments1, A_ChildValue2,
A_ChildComments2, A_ChildValue3, A_ChildComments3,
A_ChildValue4, A_ChildComments4,
C_ControlPoint, C_ControlScore, C_ControlValue,
C_Child1, C_Child2)
) piv;
See SQL Fiddle with Demo. Finally, you'll implement this in your dynamic SQL script:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(col)
from
(
select ControlCode,
col = ControlCode +'_'+col,
seq,
so
from controls
cross apply
(
select 'ControlPoint', 0, 0 union all
select 'ControlScore', 0, 1 union all
select 'ControlValue', 0, 2
) c (col, seq, so)
union all
select ControlCode,
col = ControlCode+'_'+subcol+cast(rn as varchar(10)),
rn,
so
from
(
select ControlCode,
row_number() over(partition by c.ProjectId, c.ControlCode
order by cc.ControlChildId) seq
from controls c
inner join controlchilds cc
on c.controlid = cc.controlid
) d
cross apply
(
select 'ChildValue', seq, 3 union all
select 'ChildComments', seq, 4
) c (subcol, rn, so)
) src
group by ControlCode, seq, col, so
order by ControlCode, seq, so
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT ProjectId, ' + #cols + '
from
(
select ProjectId,
col = ControlCode +''_''+col,
val
from
(
select
c.ProjectId,
c.ControlCode,
c.ControlPoint,
c.ControlScore,
c.ControlValue
from controls c
) d
cross apply
(
select ''ControlPoint'', cast(controlpoint as varchar(10)) union all
select ''ControlScore'', cast(ControlScore as varchar(10)) union all
select ''ControlValue'', ControlValue
) c (col, val)
union all
select
projectId,
col = ControlCode+''_''+subCol+cast(seq as varchar(10)),
value
from
(
select c.ProjectId,
c.ControlCode,
cc.ControlChildValue,
cc.ControlChildComments,
row_number() over(partition by c.ProjectId, c.ControlCode
order by cc.ControlChildId) seq
from controls c
inner join controlchilds cc
on c.controlid = cc.controlid
) d
cross apply
(
select ''ChildValue'', ControlChildValue union all
select ''ChildComments'', ControlChildComments
) c (subCol, value)
) x
pivot
(
max(val)
for col in (' + #cols + ')
) p '
exec sp_executesql #query;
See SQL Fiddle with Demo. Both of these gives a result:
| PROJECTID | A_CONTROLPOINT | A_CONTROLSCORE | A_CONTROLVALUE | A_CHILDVALUE1 | A_CHILDCOMMENTS1 | A_CHILDVALUE2 | A_CHILDCOMMENTS2 | A_CHILDVALUE3 | A_CHILDCOMMENTS3 | A_CHILDVALUE4 | A_CHILDCOMMENTS4 | C_CONTROLPOINT | C_CONTROLSCORE | C_CONTROLVALUE | C_CHILDVALUE1 | C_CHILDCOMMENTS1 | C_CHILDVALUE2 | C_CHILDCOMMENTS2 |
|-----------|----------------|----------------|----------------|---------------|------------------|---------------|------------------|---------------|------------------|---------------|------------------|----------------|----------------|----------------|---------------|------------------|---------------|------------------|
| P001 | 30.44 | 65.00 | Invalid | Yes | Something | No | (null) | NA | Others | Others | (null) | 45.30 | 85.00 | Valid | Yes | New one | SomeValue | (null) |
Here is an example of a dynamic crosstab. Since you have multiple columns you would need to adjust the dynamic portion of this to suit.
if OBJECT_ID('Something') is not null
drop table Something
create table Something
(
ID int,
Subject1 varchar(50)
)
insert Something
select 10868952, 'NUR/3110/D507' union all
select 10868952, 'NUR/3110/D512' union all
select 10868952, 'NUR/4010/D523' union all
select 10868952, 'NUR/4010/HD20' union all
select 12345, 'asdfasdf'
declare #MaxCols int
declare #StaticPortion nvarchar(2000) =
'with OrderedResults as
(
select *, ROW_NUMBER() over(partition by ID order by Subject1) as RowNum
from Something
)
select ID';
declare #DynamicPortion nvarchar(max) = '';
declare #FinalStaticPortion nvarchar(2000) = ' from OrderedResults Group by ID order by ID';
with E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select #DynamicPortion = #DynamicPortion +
', MAX(Case when RowNum = ' + CAST(N as varchar(6)) + ' then Subject1 end) as Subject' + CAST(N as varchar(6)) + CHAR(10)
from cteTally t
where t.N <=
(
select top 1 Count(*)
from Something
group by ID
order by COUNT(*) desc
)
select #StaticPortion + #DynamicPortion + #FinalStaticPortion
--declare #SqlToExecute nvarchar(max) = #StaticPortion + #DynamicPortion + #FinalStaticPortion;
--exec sp_executesql #SqlToExecute

How to merger the splited data into same table?

I have a table having data like this
id | Name | Age
---------------
1 | a,b | 10
I want to split that data and merge with the same table like this
id | Name | Age
---------------
1 | a | 10
1 | b | 10
I have a split function.
Test Data
DECLARE #TABLE TABLE(id INT, Name VARCHAR(10), Age INT)
INSERT INTO #TABLE VALUES
(1 ,'a',10), (1 ,'b',10),
(2 ,'d',20), (2 ,'f',20)
Query
SELECT t.ID
,STUFF(( SELECT ',' + Name
FROM #TABLE
WHERE ID = t.ID AND Age = t.Age
FOR XML PATH(''),TYPE)
.value('.','NVARCHAR(MAX)'),1,1,'') AS Name
,t.Age
FROM #TABLE t
GROUP BY t.id, t.Age
Result
╔════╦══════╦═════╗
║ ID ║ Name ║ Age ║
╠════╬══════╬═════╣
║ 1 ║ a,b ║ 10 ║
║ 2 ║ d,f ║ 20 ║
╚════╩══════╩═════╝
It seems to me that OP has the data concatenated and needs to split it. Here's one way to do it (fiddle: http://sqlfiddle.com/#!3/6b2a1/15)
WITH cteSplitCol
AS
(
SELECT id,
Name,
Age,
SUBSTRING(Name, 1, 1) AS Character,
1 AS CharPos,
MAX(LEN(Name)) OVER () AS MaxLen
FROM Table1
UNION ALL
SELECT id,
Name,
Age,
SUBSTRING(Name, CharPos + 1, 1) AS Character,
CharPos + 1,
MaxLen
FROM cteSplitCol
WHERE CharPos + 1 <= MaxLen
)
SELECT id,
Character AS Name,
Age
FROM cteSplitCol
WHERE Character <> ',';