Separate comma separated list into query columns - mysql

I have a table which has the column containing the comma separated list like
ID : List
1 : 1,2,44,5 --row# 1
2 : 4,3,5,2,56,66 --row# 2
and so on. I want to write a select query which would have at max 10 columns Item1, Item2, Item3 .... Item10 and each column has a number from the corresponding comma separated list.
For example: for ID = 1
Item1 = 1, Item2 = 2, Item3 = 44, Item4 = 55 and all other columns would be null or empty
How can I write this in SQL?

You can do it like this:
select
substring_index(substring_index(str,',',1),',',-1)AS c1
, CASE WHEN LENGTH(str)-LENGTH(REPLACE(str,',','')) >= 1 THEN substring_index(substring_index(str,',',2),',',-1) ELSE NULL END AS c2
, CASE WHEN LENGTH(str)-LENGTH(REPLACE(str,',','')) >= 2 THEN substring_index(substring_index(str,',',3),',',-1) ELSE NULL END AS c3
, CASE WHEN LENGTH(str)-LENGTH(REPLACE(str,',','')) >= 3 THEN substring_index(substring_index(str,',',4),',',-1) ELSE NULL END AS c4
, CASE WHEN LENGTH(str)-LENGTH(REPLACE(str,',','')) >= 4 THEN substring_index(substring_index(str,',',5),',',-1) ELSE NULL END AS c5
, CASE WHEN LENGTH(str)-LENGTH(REPLACE(str,',','')) >= 5 THEN substring_index(substring_index(str,',',6),',',-1) ELSE NULL END AS c6
, CASE WHEN LENGTH(str)-LENGTH(REPLACE(str,',','')) >= 6 THEN substring_index(substring_index(str,',',7),',',-1) ELSE NULL END AS c7
, CASE WHEN LENGTH(str)-LENGTH(REPLACE(str,',','')) >= 7 THEN substring_index(substring_index(str,',',8),',',-1) ELSE NULL END AS c8
, CASE WHEN LENGTH(str)-LENGTH(REPLACE(str,',','')) >= 8 THEN substring_index(substring_index(str,',',9),',',-1) ELSE NULL END AS c9
, CASE WHEN LENGTH(str)-LENGTH(REPLACE(str,',','')) >= 9 THEN substring_index(substring_index(str,',',10),',',-1) ELSE NULL END AS c10
from test
Demo.
The expressions have two common parts:
LENGTH(str)-LENGTH(REPLACE(str,',','')) >= K - this subexpression determines if the string has at least K delimiters
substring_index(substring_index(str,',',K),',',-1) - this subexpression cuts out the element after the K-th delimiter

This can be done by creating a user-defined function reference from MySQL Split String Function ,pass your column which contains the comma separated string as first parameter to this function,in second parameter pass the separator in your case comma is separator and in third parameter pass your desired position to get the string
SPLIT_STR(string, separator, position)
CREATE FUNCTION SPLIT_STR(
x VARCHAR(255),
delim VARCHAR(12),
pos INT
)
RETURNS VARCHAR(255)
RETURN REPLACE(SUBSTRING(SUBSTRING_INDEX(x, delim, pos),
LENGTH(SUBSTRING_INDEX(x, delim, pos -1)) + 1),
delim, '');
And then you can query your data a below
SELECT
SPLIT_STR(List, ',', 1) item1,
SPLIT_STR(List, ',', 2) item2,
SPLIT_STR(List, ',', 3) item3,
SPLIT_STR(List, ',', 4) item4,
SPLIT_STR(List, ',', 5) item5,
SPLIT_STR(List, ',', 6) item6,
SPLIT_STR(List, ',', 7) item7,
SPLIT_STR(List, ',', 8) item8,
SPLIT_STR(List, ',', 9) item9,
SPLIT_STR(List, ',', 10) item10
FROM t
Fiddle Demo

Related

Split comma separated values in a particular comma postion using SQL or SSRS report

I have a field in SSRS that is concatenated values like
1234,1456,3456,7890,3457,3245,4345
I need to break/split after 8th comma or in a particular position in next row like:
1234,1456,3456,
7890,3457,3245,
4345
Here values are dynamic but, we have to split/break at every 8th or particular comma
In your example text, all the values have four characters. If that is the case, a simple recursive CTE does what you want:
with cte as (
select convert(varchar(max), NULL) as val, convert(varchar(max), field) as rest, 0 as lev
from t
union all
select left(rest, 15) as val, stuff(rest, 1, 15, '') as rest, lev+1
from cte
where rest <> ''
)
select val
from cte
where lev > 0;
Here is a db<>fiddle.
Once you've grabbed a copy of DelimitedSplit8K_LEAD (as STRING_SPLIT has no concept of ordinal positions) you can split the string and then "re-aggregate" it.
Using SQL Server 2017+:
DECLARE #YourString varchar(8000) = '1234,1456,3456,7890,3457,3245,4345';
WITH Split AS(
SELECT DS.Item,
DS.ItemNumber,
(DS.ItemNumber - 1) / 3 AS Grp
FROM dbo.DelimitedSplit8K_LEAD(#YourString,',') DS)
SELECT STRING_AGG(S.Item,',') WITHIN GROUP (ORDER BY S.ItemNumber ASC) AS NewString
FROM Split S
GROUP BY S.Grp;
SQL Server 2016-:
DECLARE #YourString varchar(8000) = '1234,1456,3456,7890,3457,3245,4345';
WITH Split AS(
SELECT DS.Item,
DS.ItemNumber,
(DS.ItemNumber - 1) / 3 AS Grp
FROM dbo.DelimitedSplit8K_LEAD(#YourString,',') DS)
SELECT STUFF((SELECT ',' + sq.Item
FROM Split sq
WHERE sq.Grp = S.Grp
ORDER BY sq.ItemNumber
FOR XML PATH(''),TYPE).value('.','varchar(8000)'),1,1,'') AS MewString
FROM Split S
GROUP BY S.Grp;
If you use SQL Server 2016+, you may use an approach, based on JSON. Just transform the input text into a valid JSON array and parse this array with OPENJSON():
Example with text:
Statement:
DECLARE #json nvarchar(max) = N'1234,1456,3456,7890,3457,3245,4345'
SELECT CONCAT(
MAX(CASE WHEN CONVERT(int, [key]) % 3 = 0 THEN [value] END),
MAX(CASE WHEN CONVERT(int, [key]) % 3 = 1 THEN [value] END),
MAX(CASE WHEN CONVERT(int, [key]) % 3 = 2 THEN [value] END)
) AS OutputText
FROM OPENJSON(CONCAT(N'["', REPLACE(#json, N',', N',","'), N'"]'))
GROUP BY (CONVERT(int, [key]) / 3)
Result:
---------------
OutputText
---------------
1234,1456,3456,
7890,3457,3245,
4345
Example with table:
Table:
CREATE TABLE Data (TextData nvarchar(max))
INSERT INTO Data (TextData)
VALUES (N'1234,1456,3456,7890,3457,3245,4345')
Statement:
SELECT d.TextData, c.OutputData
FROM Data d
CROSS APPLY (
SELECT CONCAT(
MAX(CASE WHEN CONVERT(int, [key]) % 3 = 0 THEN [value] END),
MAX(CASE WHEN CONVERT(int, [key]) % 3 = 1 THEN [value] END),
MAX(CASE WHEN CONVERT(int, [key]) % 3 = 2 THEN [value] END)
) AS OutputData
FROM OPENJSON(CONCAT(N'["', REPLACE(d.TextData, N',', N',","'), N'"]'))
GROUP BY (CONVERT(int, [key]) / 3)
) c
Result:
---------------------------------------------------
TextData OutputData
---------------------------------------------------
1234,1456,3456,7890,3457,3245,4345 1234,1456,3456,
1234,1456,3456,7890,3457,3245,4345 7890,3457,3245,
1234,1456,3456,7890,3457,3245,4345 4345

Flattening a Table in prep for Json

Maybe it's that I'm tired but this is escaping me.
Let's say that I want to flatten this table:
a_id a_val b_id b_val c_id c_val d_id d_val
1 a 10 b 100 c 1000 f
1 a 20 d 200 g null null
2 e 30 h 300 i null null
2 j 40 k null null null null
3 l null null null null null null
Into this query result:
id mystring
1, (1:a,10:b,100:c,1000:f),(1:a,20:d,200:g)
2, (2:e,30:h,300:i),(2:j,40:k)
3, (3:l)
The table only renders four levels deep (a, b, c, d) so no dynamic sql issue.
Now I'd usually just use GROUP_CONCAT(CONCAT(...)) but that won't work with the Nulls present. And maybe using coalesce somehow will solve this but... I feel pretty stupid at the moment... and I can't figure it out.
Unfortunately I can't use mysql json services on this installation so I need to construct the data. thanks.
The solution here will probably just be a combination of clever concatenation and IFNULL calls. My shot in the dark:
SELECT a_id, GROUP_CONCAT(CONCAT('(',
a_id, ':', a_value, ',',
IFNULL(b_id, ''), IF(b_id IS NOT NULL, ':', ''), IFNULL(b_val, ''),
...repeat for c and d
')'
) SEPARATOR ',')
FROM table
GROUP BY a_id;
select a_id as id,
group_concat(concat(
case isnull(a_id) when true then '' else '(' end,
coalesce(a_id, ''),
case isnull(a_id) when true then '' else ':' end,
coalesce(a_val, ''),
case isnull(b_id) when true then '' else ',' end,
coalesce(b_id, ''),
case isnull(b_id) when true then '' else ':' end,
coalesce(b_val, ''),
case isnull(c_id) when true then '' else ',' end,
coalesce(c_id, ''),
case isnull(c_id) when true then '' else ':' end,
coalesce(c_val, ''),
case isnull(d_id) when true then '' else ',' end,
coalesce(d_id, ''),
case isnull(d_id) when true then '' else ':' end,
coalesce(d_val,''),
case isnull(a_id) when true then '' else ')' end
) separator ',')
from table
group by a_id;

mysql joining the same table

Given a table named RECORD in mysql with following structure:
rid(pk & AI) patientid(fk) recordTYPE(varchar) recordValue(varchar) recordTimestamp(timestamp)
1 1 temperature(℃) 37.2 2015-08-11 18:10:04
2 1 weight(kg) 65.0 2015-08-11 18:20:08
3 1 heartbeat(bpm) 66 2015-08-11 18:30:08
4 1 temperature(℃) 36.8 2015-08-11 18:32:08
You can see that for the same date, there can be multiple records for one particular type of record. e.g. temperature in the sample data :
rid patientid recordTYPE value recordtimestamp
1 1 temperature(℃) 37.2 2015-08-11 18:10:04
4 1 temperature(℃) 36.8 2015-08-11 18:32:08
In this case, we should choose the latest record. i.e. the record with rid = 4 and value = 36.8 .
Now given an input date e.g. '2015-8-11', I want to do a query to obtain something like:
date patientid temperature(℃) weight(kg) heartbeat(bpm)
2015-08-11 1 36.8 65.0 66
2015-08-11 2 36.5 80.3 70
2015-08-11 3 35.5 90.5 80
..........................................................
..........................................................
2015-08-11 4 35.5 null null
Fig. 2
In addition, you can see that for a particular date, there may not be any records of some types. In this case, the value in that column is null.
I tried the following query:
SELECT max(recordTimestamp), patientid, recordTYPE, recordValue
FROM RECORD
WHERE date(recordTimestamp) = '2015-08-11'
GROUP BY patientid, recordTYPE
The result is something like:
date patientid recordTYPE recordValue
2015-08-11 1 temperature(℃) 36.8
2015-08-11 1 weight(kg) 65.0
2015-08-11 1 heartbeat(bpm) 66
2015-08-11 2 temperature(℃) 36.5
2015-08-11 2 weight(kg) 80.3
2015-08-11 2 heartbeat(bpm) 70
2015-08-11 4 temperature(℃) 35.5
Fig. 4
The questions are:
Given this table RECORD, what is the proper mysql statement (in terms
of performance such as retrieval speed) to produce the desired result set (i.e. Fig.2)?
Will it be better (in terms of facilitating query and scalability such as adding new types of record) if the db design is changed?
e.g. Create one table for each type of record instead of putting all types of record in one table.
Any suggestion is appreciated as I'm a db novice...... Thank you.
You can try this:-
SELECT MAX(rid), patientid, recordTYPE, MAX(recordValue), recordTimestamp
FROM YOUR_TABLE
WHERE recordTimestamp = '2015/08/11'
GROUP BY patientid, recordTYPE, recordTimestamp;
Here's one way to do it. SQL Fiddle Demo
Sadly MySQL doesn't support the row_number() over (partition by ...) syntax which would have simplified this a lot.
Instead I've made excessive use of a trick discussed here: https://stackoverflow.com/a/3470355/361842
select `date`
, `patientId`
, max(case when `tRank`=1 then `temperature(℃)` else null end) `temperature(℃)`
, max(case when `wRank`=1 then `weight(kg)` else null end) `weight(kg)`
, max(case when `hRank`=1 then `heartbeat(bpm)` else null end) `heartbeat(bpm)`
from
(
select case when #p = `patientId` and #d = cast(`recordTimestamp` as date) then #x := 1 else #x := 0 end
, case when #x = 0 then #t := 0 end
, case when #x = 0 then #w := 0 end
, case when #x = 0 then #h := 0 end
, case `recordType` when 'temperature(℃)' then case #x when 1 then #t := #t + 1 else #t := 1 end else null end as `tRank`
, case `recordType` when 'weight(kg)' then case #x when 1 then #w := #w + 1 else #t := 1 end else null end as `wRank`
, case `recordType` when 'heartbeat(bpm)' then case #x when 1 then #h := #h + 1 else #t := 1 end else null end as `hRank`
, case `recordType` when 'temperature(℃)' then `recordValue` else null end as `temperature(℃)`
, case `recordType` when 'weight(kg)' then `recordValue` else null end as `weight(kg)`
, case `recordType` when 'heartbeat(bpm)' then `recordValue` else null end as `heartbeat(bpm)`
, #d := cast(`recordTimestamp` as date) as `date`
, #p := `patientId` as `patientId`
from `Record`
cross join
(
SELECT #t := 0
, #w := 0
, #h := 0
, #p := 0
, #x := 0
, #d := cast(null as date)
) x
order by `patientId`, `recordTimestamp` desc
) y
group by `date`, `patientId`
order by `date`, `patientId`
Breakdown
This says that if this is the last temperature of the day for the current grouping's partientId/date combo then return it; otherwise return null. It then takes the max of the matching values (which given all but 1 are null, gives us the one we're after).
, max(case when `tRank`=1 then `temperature(℃)` else null end)
How tRank = 1 means the last temperature of the day for a patientId/date combo is explained later.
This line says that if this record has the same patientId and date as the previous record then set x to 1; if it's a new combo set it to 0.
select case when #p = `patientId` and #d = cast(`recordTimestamp` as date) then #x := 1 else #x := 0 end
The next lines say that if we have a new patiendIt/date combo, reset the t, w and h markers to say "the next value you receive will be the one we're after".
, case when #x = 0 then #t := 0 end
The next lines split the data by recordType; returning null if this record isn't their record type, or returning a number saying what how many of this type of record we've now seen for the patientId/date combo.
, case `recordType` when 'temperature(℃)' then case #x when 1 then #t := #t + 1 else #t := 1 end else null end as `tRank`
This is similar to the above; except instead of returning a combo-counter it returns the value of the current record (or null if this is a different record type).
, case `recordType` when 'temperature(℃)' then `recordValue` else null end as `temperature(℃)`
We then record the current record's date and patientId values, so we can compare them with the next record on the next iteration.
, #d := cast(`recordTimestamp` as date) as `date`
, #p := `patientId` as `patientId`
The cross join and following subquery is just used to initialise our variables.
The (first) order by is used to ensure that comparing current and previous records is enough to tell if we're looking ata different combo (i.e. if all combos are grouped then any change is easy to spot; if the combos keep alternating we'd need to keep track of every combo we'd seen before).
recordTimestamp is sorted descending so that the first record we see on a new combo will be the last record that day; the one we're after.
The group by is used to ensure we get 1 result per combo; and the last order by just to make our output ordered.

MySql transpose multiple rows into column - the most optimized way for large data?

i want to transpose multiple rows to column.
This my table with data (about 20 mil.rows)
PHONE SERVICE
0000000 service1
0000000 service2
0000000 service3
1111111 service1
1111111 service4
2222222 service5
and I would like to get the following output:
PHONE SC1 SC2 SC3 SC4 SC5
0000000 service1 service2 service3 NULL NULL
1111111 service1 service4 NULL NULL NULL
2222222 service5 NULL NULL NULL NULL
etc..
Anybody know the fastest to do this (for about 20mil records)? Thanks very much!
This should work well, make sure you have an index on phone.
SELECT phone,
SUBSTRING_INDEX(services, ',', 1) SC1,
if(service_count >= 2, SUBSTRING_INDEX(SUBSTRING_INDEX(services, ',', 2), ',', -1), NULL) SC2,
if(service_count >= 3, SUBSTRING_INDEX(SUBSTRING_INDEX(services, ',', 3), ',', -1), NULL) SC3,
if(service_count >= 4, SUBSTRING_INDEX(SUBSTRING_INDEX(services, ',', 4), ',', -1), NULL) SC4,
if(service_count >= 5, SUBSTRING_INDEX(SUBSTRING_INDEX(services, ',', 5), ',', -1), NULL) SC5
FROM (SELECT phone, GROUP_CONCAT(service) AS services, COUNT(*) as service_count
FROM phones
GROUP BY phone) AS x
DEMO
You can use variables to number the services within each group and use conditional aggregation to pivot your rows into columns.
select phone,
max(case when rn = 1 then service end) sc1,
max(case when rn = 2 then service end) sc2,
max(case when rn = 3 then service end) sc3,
max(case when rn = 4 then service end) sc4,
max(case when rn = 5 then service end) sc5
from (
select phone,service,
#rowNum := if(#prevPhone = phone,#rowNum+1,1) rn,
#prevPhone := phone
from mytable
cross join (select #prevPhone := null, #rowNum := 1) c
order by phone, service
) t1 group by phone

How to replace NULL with empty string in SQL?

I am using below query to fetch column value by comma separated.
(SELECT STUFF ((SELECT ',' + CAST(Proj_ID AS VARCHAR) FROM PROJECT
left join dbo.PROJ_STA on
Project.PROJ_STA_ID = Project.PROJ_STA_ID
WHERE ENTER_DT < DATEADD(Year, -7, GETDATE()) AND PROJ_LFCYC_STA_CD = 'A' AND
PROJ_STA.PROJ_STA_DS = 'Cancelled' FOR XML PATH('')), 1, 1, '')
AS Enter_Date)
Can anyone guide me to replace null value by empty string here.
Updated:
(SELECT STUFF ((SELECT ',' + coalesce( CAST(Proj_ID AS VARCHAR), '' ) FROM PROJECT
left join dbo.PROJ_STA on
Project.PROJ_STA_ID = Project.PROJ_STA_ID
WHERE ENTER_DT < DATEADD(Year, -7, GETDATE()) AND PROJ_LFCYC_STA_CD = 'A' AND
PROJ_STA.PROJ_STA_DS = 'Cancelled' FOR XML PATH('')), 1, 1, '')
AS Enter_Date)
Try IsNull
select ISNULL(Column,'') as ColumnName
OR COALESCE
select COALESCE(NULLIF(ColumnName,''), 'Column')
An example from the AdventureWorks database
select e.ModifiedDate, ISNULL(p.FirstName,'') as FirstName
from Person.BusinessEntity as e
left join Person.Person as p on e.BusinessEntityID = p.BusinessEntityID
By using this, if there are no matching Person records, the FirstName will be displayed as an empty string instead of NULL
You can white out null values with the coalesce function
select coalesce(MyColumn, '')
Coalesce takes any number of columns or constants and returns the first one which isn't null.
Your query would be:
(SELECT STUFF ((SELECT ',' + convert(varchar, coalesce( Proj_ID, '' )) FROM PROJECT
left join dbo.PROJ_STA on
Project.PROJ_STA_ID = Project.PROJ_STA_ID
WHERE ENTER_DT < DATEADD(Year, -7, GETDATE()) AND PROJ_LFCYC_STA_CD = 'A' AND
PROJ_STA.PROJ_STA_DS = 'Cancelled' FOR XML PATH('')), 1, 1, '')
AS Enter_Date)