select query to push null values to last column in address lines - sql

I need to write a query to select the address lines so that the null values should be pushed to last columns. For example if the table has below data
ID, line1, line2, line3, line4, line5, line6
1 null test2 test3 test4 test5 test6
2 test1 test2 test3 test4 test5 test6
3 null null null null test5 test6
4 test1 test2 test3 null test5 test6
Output of the query should be as follow
ID, line1, line2, line3, line4, line5, line6
1 test2 test3 test4 test5 test6 null
2 test1 test2 test3 test4 test5 test6
3 test5 test6 null null null null
4 test1 test2 test3 test5 test6 null
I tried to concatenate the values with a delimiter by eliminating the nulls and then split the values again, but I am having difficulty in splitting the values back as the actual data contains characters which are not xml supported. Is there a easy way to do this with out creating functions to split? Below works when there are no characters in values which cause cast to xml fail
SELECT
ISNULL(Line1 + '|', '')
+ ISNULL(Line2 + '|', '')
+ ISNULL(Line3 + '|', '')
+ ISNULL(Line4 + '|', '')
+ ISNULL(Line5 + '|', '')
+ ISNULL(Line6 , '') as tmpaddr
FROM #addr1
SELECT DISTINCT id,
S.a.value('(/H/r)[1]', 'VARCHAR(100)') AS line1,
S.a.value('(/H/r)[2]', 'VARCHAR(100)') AS line2,
S.a.value('(/H/r)[3]', 'VARCHAR(100)') AS line3,
S.a.value('(/H/r)[4]', 'VARCHAR(100)') AS line4,
S.a.value('(/H/r)[5]', 'VARCHAR(100)') AS line5,
S.a.value('(/H/r)[6]', 'VARCHAR(100)') AS line6
FROM
(
SELECT *,CAST (N'<H><r>' + REPLACE(tmpaddr , '|', '</r><r>') + '</r></H>' AS XML) AS [vals]
FROM #addr ) d
CROSS APPLY d.[vals].nodes('/H/r') S(a)

Let me assume you are using SQL Server, based on the syntax conventions.
You can do what you want with cross apply and aggregation:
SELECT a.id, t.*
FROM #addr1 a CROSS APPLY
(SELECT MAX(CASE WHEN seqnum = 1 THEN line END) as line1,
MAX(CASE WHEN seqnum = 2 THEN line END) as line2,
MAX(CASE WHEN seqnum = 3 THEN line END) as line3,
MAX(CASE WHEN seqnum = 4 THEN line END) as line4,
MAX(CASE WHEN seqnum = 5 THEN line END) as line5,
MAX(CASE WHEN seqnum = 6 THEN line END) as line6
FROM (SELECT num, line, ROW_NUMBER() OVER (ORDER BY num) as seqnum
FROM (VALUES (1, a.line1),
(2, a.line2),
(3, a.line3),
(4, a.line4),
(5, a.line5),
(6, a.line6)
) v(num, line)
WHERE line IS NOT NULL
) t
) t;

Related

Split comma separated values of a multiple column in row in SQL query

I have data Like this
Code address phno
123 test1,test2,test3 123,456,789
And I want output
Code address phno
123 test1 123
123 test2 456
123 test3 789
My code is this
declare #address VARCHAR(500) = 'test1,test2,test3', #phoneno VARCHAR(500) = '123,456,789'
select *
from STRING_SPLIT(#address, ','), STRING_SPLIT(#phoneno, ',')
but it returns multiple values
You can use recursive CTEs for this:
with cte as (
select code, cast(NULL as varchar(max)) as address, cast(NULL as varchar(max)) as phno,
cast(address + ',' as varchar(max)) as rest_address, cast(phno + ',' as varchar(max)) as rest_phno, 0 as lev
from t
union all
select code, left(rest_address, charindex(',', rest_address) - 1), left(rest_phno, charindex(',', rest_phno) - 1),
stuff(rest_address, 1, charindex(',', rest_address), ''), stuff(rest_phno, 1, charindex(',', rest_phno), ''), lev + 1
from cte
where rest_address <> ''
)
select code, address, phno
from cte
where lev > 0;
Here is a db<>fiddle.
If you can have more than 100 elements in the lists, you'll need option (maxrecursion 0).
Please try the following solution.
SQL
DECLARE #tbl TABLE (Code CHAR(3), address VARCHAR(100), phno VARCHAR(100));
INSERT INTO #tbl (Code, address, phno) VALUES
('123', 'test1,test2,test3', '123,456,789');
;WITH cte1 AS
(
SELECT Code, value AS address
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS seq
FROM #tbl
CROSS APPLY STRING_SPLIT(address, ',')
), cte2 AS
(
SELECT Code, value AS phno
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS seq
FROM #tbl
CROSS APPLY STRING_SPLIT(phno, ',')
)
SELECT cte1.code, cte1.address, cte2.phno
FROM cte1 INNER JOIN cte2
ON cte2.seq = cte1.seq;
SQL #2, JSON based
;WITH rs AS
(
SELECT *
, ar1 = '["' + Replace(address, ',', '","') + '"]'
, ar2 = '["' + Replace(phno, ',', '","') + '"]'
FROM #tbl
)
SELECT Code, address.[value] AS [address], phno.[value] AS phno
FROM rs
CROSS APPLY OPENJSON (ar1, N'$') AS address
CROSS APPLY OPENJSON (ar2, N'$') AS phno
WHERE address.[key] = phno.[key];
Output
+------+---------+------+
| code | address | phno |
+------+---------+------+
| 123 | test1 | 123 |
| 123 | test2 | 456 |
| 123 | test3 | 789 |
+------+---------+------+

Migrate a Column into Multiple Columns

I have a table with the following columns 'ID, LIST_OF_VALUES'.
Example data is:
ID| LIST_OF_VALUES
--+----------------------------
1 | firstval-secondval-thirdval
2 | val1-val2
3 | val10-val20-val30
4 | singleval
I would like to select the data like this:
ID| VAL1 | VAL2 | VAL3
--+----------+-----------+-------
1 | firstval | secondval | thirdval
2 | val1 | val2 | NULL
3 | val10 | val20 | val30
4 | singlval | NULL | NULL
I am aware of the STRING_SPLIT function. I have tried using it in various ways with Cross Apply, but I can't seem to get the result I want.
I know I can do this using a mess of SUBSTR/INDEX, but I am just curious if STRING_SPLIT offers a more elegant solution.
Just another option
Example of XML Option
Select A.ID
,Val1 = tmpXML.value('/x[1]','varchar(100)')
,Val2 = tmpXML.value('/x[2]','varchar(100)')
,Val3 = tmpXML.value('/x[3]','varchar(100)')
from YourTable A
Cross Apply ( values ( Cast('<x>' + replace([LIST_OF_VALUES],'-','</x><x>')+'</x>' as xml) ) ) B(tmpXML)
Returns
ID Val1 Val2 Val3
1 firstval secondval thirdval
2 val1 val2 NULL
3 val10 val20 val30
4 singleval NULL NULL
Example of JSON Option - as suggested by #PanagiotisKanavos if 2016+
Select A.ID
,Val1 = JSON_VALUE(S,'$[0]')
,Val2 = JSON_VALUE(S,'$[1]')
,Val3 = JSON_VALUE(S,'$[2]')
from #YourTable A
Cross Apply ( values ( '["'+replace(replace([LIST_OF_VALUES],'"','\"'),'-','","')+'"]' ) ) B(S)
Assuming you don't have duplicates, you can use it . . . but it is not trivial:
select t.*, s.*
from t cross apply
(select max(case when seqnum = 1 then value end) as val1,
max(case when seqnum = 2 then value end) as val2,
max(case when seqnum = 3 then value end) as val3
from (select s.value,
row_number() over (order by charindex('-' + value + '-', '-' + t.list_of_values + '-') as seqnum
from string_split(t.list_of_values, '-') s
) s
) s;
Unfortunately, string_split() doesn't provide the ordering. This recreates it using charindex().
One simple and very efficient and scalable way would be to use an ordinal splitter such as dbo.DelimitedSplit8K (or dbo.DelimitedSplitN4K (for nchar/nvarchar). Then the query would be something like this
dbo.DelimitedSplit8K tvf
CREATE FUNCTION dbo.DelimitedSplit8K
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000)
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
), --10E+1 or 10 rows
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 (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l;
Query
select t.id,
max(case when ds.ItemNumber=1 then ds.Item end) as val1,
max(case when ds.ItemNumber=2 then ds.Item end) as val2,
max(case when ds.ItemNumber=3 then ds.Item end) as val3
from tTable t
cross apply
dbo.DelimitedSplit8K(t.LIST_OF_VALUES, '-') ds
group by t.id
order by t.id;
[EDIT] Here is an alternate method which produces the same output but does not use the DelimitedSplit8K function. This is the same approach as Gordon's but with an outer GROUP BY clause.
;with charindex_split_cte(id, Item, ItemNumber) as (
select t.id, sp.value,
row_number() over (order by charindex('-' + sp.value + '-', '-' + t.list_of_values + '-'))
from tTable t
cross apply string_split(t.list_of_values, '-') sp)
select id,
max(case when ItemNumber=1 then Item end) as val1,
max(case when ItemNumber=2 then Item end) as val2,
max(case when ItemNumber=3 then Item end) as val3
from charindex_split_cte
group by id
order by id;

Splitting Single Column value in to three Column values for a report

For Example
I have below Scenario Can any one suggest on this
Current Scenario
Col1
-----
Test1
Test2
test3
Test4
Test5
Test6
test7
Test8
Test9
Test10
test11
Test12
Expected scenario
Col1 Col2 Col3
-------------------
Test1 Test2 Test3
Test4 Test5 Test6
test7 Test8 Test9
Test10 Test11 test12
...
Goes on
enter image description here
You seem to want something like this:
select max(case when seqnum % 3 = 0 then col end) as col1,
max(case when seqnum % 3 = 1 then col end) as col2,
max(case when seqnum % 3 = 2 then col end) as col3
from (select t.*,
row_number() over (order by col) - 1 as seqnum
from t
) t
group by floor(seqnum / 3)
order by min(seqnum);
This adds a sequential number and then uses arithmetic and group by to reduce the number of rows.

Move table data from one row to another row if source row is null

I have the table, with some columns contains null values, i tasked to move all the NULL values into left side, so the numeric values are moved from right to left in same order.
(for ex)
Column1 Column2 Column3 Column4
--------------------------------------------
NULL 1 NULL 3
1 NULL 3 2
1 2 3 NULL
Output should be
Column1 Column2 Column3 Column4
--------------------------------------------
1 3 NULL NULL
1 3 2 NULL
1 2 3 NULL
This is a pain, but you can do it using outer apply:
select t.id, v.*
from t outer apply
(select max(case when i = 1 then colval end) as col1,
max(case when i = 2 then colval end) as col2,
max(case when i = 3 then colval end) as col3,
max(case when i = 4 then colval end) as col4
from (select row_number() over (order by colnum) as i, v.*
from (values (1, t.col1), (2, t.col2), (3, t.col3), (4, t.col4)
) v(colnum, colval)
where colval is not null
) v
) v;
I should note that the need to do this type of transformation suggests that you have a poor data model. The values in the separate columns should probably be in another table, with one row per id and per column.

SQL Select stuff

I have project table.
This is my query which is fetching following results.
select top 5 proj_ID, Proj_NM
from project
Output:
proj_ID Proj_NM
-------------------
20 test1
21 test2
22 test3
24 test4
25 test5
I want to get this output instead. Can any one pls help.
proj_ID Proj_NM All_Proj_NM
---------------------------------
20 test1 test1,test2,test3,test4,test5
21 test2 test1,test2,test3,test4,test5
22 test3 test1,test2,test3,test4,test5
24 test4 test1,test2,test3,test4,test5
25 test5 test1,test2,test3,test4,test5
You can use FOR XML PATH for that
select top 5 proj_ID, Proj_NM,
(select STUFF( (select top 5 ',' + Proj_NM
from project
order by proj_id
FOR XML PATH('')
), 1, 1, '')) AS All_Proj_NM
from project
order by proj_ID
Try this
Select Distinct ST2.proj_ID,ST2.Proj_NM,
substring((Select ',' + ST1.Proj_NM AS [text()]
From project ST1
WHERE ST1.Proj_NM IN (SELECT Top 5 Proj_NM From Projects )
ORDER BY ST1.proj_ID
For XML PATH ('')),2, 1000) [Pr_Name]
From dbo.project ST2