Mysql split column string into three rows - mysql

This query
SELECT user_Id,CAST(leave_dates_object AS CHAR(10000) CHARACTER SET utf8) AS LeaveDates
FROM `lms_leaves`
returns
Now I want three rows user_Id and Date and F_or_H from the formatted string return from leave date.
I modified and tried code from this link but can't able to get result.
Expected Output
user_Id Date LeaveType
85 2016-09-06 F
85 2016-09-07 F
85 2016-09-06 H
63 2016-03-25 F
63 2016-03-02 F
63 2016-03-03 H
Please Help me.

Suppose you have maximally 8 dates in one record:
create table users (userId int not null, leaveDates varchar(1000) not null);
INSERT users (userId,leaveDates) VALUES (85,'--- \r-2016-09-06:F\r-2016-09-07:F'),(85,'---\r-2016-09-06:H'),(63,'---\r-2016-03-25:F'),(63,'---\r-2016-03-02:F\r-2016-03-03:H');
SELECT s.userId AS userId,LEFT(s.leaveDate,CHAR_LENGTH(s.leaveDate)-2) AS ldate, RIGHT(s.leaveDate,1) AS lflag
FROM (
SELECT
u.userId,
REPLACE(SUBSTRING(SUBSTRING_INDEX(u.leaveDates, '\r-', n.number),
CHAR_LENGTH(SUBSTRING_INDEX(u.leaveDates, '\r-', n.number -1)) + 1),
'\r-', '') as leaveDate
FROM (SELECT u0.userId,REPLACE(u0.leaveDates,'---','') AS leaveDates FROM users u0) u
INNER JOIN (SELECT 1 AS number UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8) n
ON (char_length(u.leaveDates) - char_length(replace(u.leaveDates, '\r-', '')) >= n.number-1)) s
WHERE s.leaveDate<>''
ORDER BY userId,ldate,lflag;

Related

How to create multiple rows from conditions on columns in a single row in SQL

I am fairly new to SQl and trying to get a solution.
How do i get this data
Index1 From_date To_date
A 2001 2003
B 2005 2007
to
Index1 Year
A 2001
A 2002
A 2003
B 2005
B 2006
B 2007
Creating a test:
CREATE TABLE letter (letter VARCHAR(1) PRIMARY KEY, from_date INT, to_date INT);
INSERT INTO letter VALUES ("A", 2001, 2003), ("B", 2005, 2007);
Selecting what is needed:
SELECT letter.letter, t1.f1 + t2.f1 * 10 + 2000 as yr
FROM letter
CROSS JOIN (
SELECT 0 f1 UNION ALL SELECT 1 f1 UNION ALL SELECT 2 f1 UNION ALL SELECT 3 f1 UNION ALL SELECT 4 f1 UNION ALL SELECT 5 f1 UNION ALL SELECT 6 f1 UNION ALL SELECT 7 f1 UNION ALL SELECT 8 f1 UNION ALL SELECT 18 f1) t1
CROSS JOIN
(SELECT 0 f1 UNION ALL SELECT 1 f1 UNION ALL SELECT 2 f1 UNION ALL SELECT 3 f1 UNION ALL SELECT 4 f1 UNION ALL SELECT 5 f1 UNION ALL SELECT 6 f1 UNION ALL SELECT 7 f1 UNION ALL SELECT 8 f1 UNION ALL SELECT 18 f1) t2
WHERE t1.f1 + t2.f1 * 100 + 2000 between letter.from_date and letter.to_date;
Result
A 2001
A 2002
A 2003
B 2005
B 2006
B 2007
The years between 2000 and 2099 are covered. If you need more - add more t tables
You can use an union with two select statements:
(SELECT index1, from_date as date FROM table)
UNION
(SELECT index1, to_date as date FROM table);
Here, it's like you have:
first table | second table
|
Index1 date | Index1 date
A 2001 | A 2003
B 2005 | B 2007
And you can add an order:
(SELECT index1, from_date as date FROM table)
UNION
(SELECT index1, to_date as date FROM table)
ORDER BY index1 ASC;
https://www.mysqltutorial.org/sql-union-mysql.aspx
This is a very good use-case for recursive CTEs:
with recursive cte as (
select index1, from_date, to_date
from t
union all
select index1, from_date + 1, to_date
from cte
where from_date < to_date
)
select index1, from_date
from cte;
Here is a db<>fiddle.

How to get all missing numbers from id column

Users table has id field in mysql database, and table has below record in id field
|id|
2
3
5
6
9
10
15
16
18
21
25
I want to get missing records row like below:-
1
4
7
8
11
12
13
14
17
19
20
22
23
24
How cool is this ?
SELECT seq.`id` FROM (
SELECT #row := #row + 1 as `id`
FROM `users` t, (SELECT #row := 0) r
CROSS JOIN `users` t2
) as seq WHERE seq.`id` NOT IN (SELECT `id` FROM `users`)
AND seq.`id` <= (SELECT max(`id`) from `users`);
If you want the missing ids for analytical purpose, should love this one:
SELECT CONCAT(dt.missing, IF(dt.`found`-1 > dt.missing, CONCAT(' to ', dt.`found` - 1), '')) AS missing
FROM ( SELECT #rownum:=#rownum+1 AS `missing`,
IF (#rownum=id, 0, #rownum:=id) AS `found`
FROM ( SELECT #rownum:=0 ) AS r
JOIN users
ORDER BY id ) AS dt
WHERE dt.`found`!= 0;
Use a calendar table approach and anti-join approach:
CREATE TABLE nums (id INT);
INSERT INTO nums (id)
(
SELECT (t*10+u+1) x FROM
(SELECT 0 t UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION
SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) A,
(SELECT 0 u UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION
SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) B
);
SELECT
n.id
FROM nums n
LEFT JOIN users u
ON n.id = u.id
WHERE
u.id IS NULL
ORDER BY
n.id;
Demo
Note that in the demo and query I did not limit the height of the range of missing numbers which gets returned. I arbitrarily used a sequence of numbers 1..100, but you may restrict to any size you want.

SQL - select where elements of a string match elements of another string

I have a table field, let's call it pattern containing a list of comma separated values.
For example: '10,20,30,40,50'
I need to select * from said table, where at least one element from another similar string appears in that field too.
For example, say I have the string '10,50,70' A record whose pattern field is '10,20,30,50,70' should be selected, because 10 and 70 are present in '10,50,70'.`
Is there any way of doing this, except lots of OR where i check if pattern LIKE '%10%' OR pattern LIKE %50% OR pattern LIKE %70% ?
You can use FIND_IN_SET() as an alternative like below though as already suggested in comment, consider Normalizing your table.
SELECT * FROM table_name
WHERE FIND_IN_SET('10','10,20,30,50,70')
OR FIND_IN_SET('50','10,20,30,50,70')
OR FIND_IN_SET('70','10,20,30,50,70') ;
As mentioned by Patrick Hofman, normalising the database is the best solution.
If you really must stick with such a table layout then there are a couple of solutions.
You can split up the comma separated values and join the results based on that. Not that the cross joined tables are just generating a range of numbers (in this case 0 to 99), but this must equal or exceed the max number of delimited values. It will also be slow as no indexes can be used. Also large numbers of records are generated in memory to do this calculation.
Something like this:-
SELECT DISTINCT sub0.id, sub1.id
FROM
(
SELECT DISTINCT id, SUBSTRING_INDEX(SUBSTRING_INDEX(pattern, ',', units.i + tens.i * 10), ',' -1) AS pattern_id
FROM some_table
CROSS JOIN (SELECT 1 AS i UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) units
CROSS JOIN (SELECT 1 AS i UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) tens
) sub0
INNER JOIN
(
SELECT DISTINCT id, SUBSTRING_INDEX(SUBSTRING_INDEX(pattern, ',', units.i + tens.i * 10), ',' -1) AS pattern_id
FROM some_table
CROSS JOIN (SELECT 1 AS i UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) units
CROSS JOIN (SELECT 1 AS i UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) tens
) sub1
ON sub0.pattern_id = sub1.pattern_id
AND sub0.id != sub1.id
If the comma separated value is the key to another table then you could maybe do a couple of joins based on FIND_IN_SET.
SELECT DISTINCT a.*, b.*
FROM some_table a
INNER JOIN some_patterns b
ON FIND_IN_SET(b.id, a.patterns)
INNER JOIN some_table c
ON FIND_IN_SET(b.id, c.patterns)
In reality it would probably be best to normalise you database, possibly using SQL based on these solutions just to help you extract the data to your new normalised format
I've inherited this method of storing small lists of integers and wrote the included function to rotate CSV of integers into a column.
CREATE function [dbo].[udf_IntegerColumnFromCSV] ( #DelimStr nvarchar(max) )
returns #ListOfInts table ( Value int )
AS
BEGIN
set #DelimStr = #DelimStr + ',' -- add one more to the end
declare #i int -- the "start" of the next value to be read
declare #j int -- the location of the next comma
declare #le int -- line end
set #i = 1
set #j = 1
set #le = len(#DelimStr)
while ( #j < #le ) begin
set #j = charindex( ',', #DelimStr, #i ) -- find the end of the next row
insert into #ListOfInts (Value) values ( cast(substring( #DelimStr, #i, #j-#i ) as integer) )
set #i = #j + 1
end
return
end
In a situation where you have a list of values stored as CSV:
select ID, ValueList from TableX
ID : ValueList
20 : 38, 39, 40, 41, 42, 44, 61,62,63,64,65,66, 70,71,72
21 : 12
22 : 61,62,63,64,65,66
Use the function to pivot the values into a column:
select ID, vl.* from TableX cross apply dbo.udf_IntegerColumnFromCSV(ValueList) vl
20: 38
20: 39
20: 40
20: 41
20: 42
20: 44
20: 61
20: 62
20: 63
20: 64
20: 65
20: 66
20: 70
20: 71
20: 72
21: 12
22: 61
22: 62
22: 63
22: 64
22: 65
22: 66

SQL query of multiple values in one cell

There is a table(Course Interests) which has all the values in one cell. But those values are just ids and I want to join them with another table(Course) so I can know their names.
Course Interests:
MemberID MemberName CoursesInterested
-------------- --------------------- --------------
1 Al 1,4,5,6
2 A2 3,5,6
Course Table:
CourseId Course
-------------- ---------------------
1 MBA
2 Languages
3 English
4 French
5 Fashion
6 IT
Desired Output:
MemberID MemberName CoursesInterested
-------------- --------------------- --------------
1 Al MBA,French,Fashion,IT
2 A2 English,Fashion,IT
I would like to do a SQL query in MySql that can help me to extract the desired output. I know how to do it in the opposite way(join values to one cell), but I've struggling on seek a way to separate the ids and do a cross-join into another table.
I'll appreciate any help from the community. Thanks
Use FIND_IN_SET to search for something in a comma-delimited list.
SELECT i.MemberID, i.MemberName, GROUP_CONCAT(c.Course) AS CoursesInterested
FROM CourseInterests AS i
JOIN Course AS c ON FIND_IN_SET(c.CourseId, i.CoursesInterested)
However, it would be better to create a relation table instead of storing the courses in a single column. This type of join cannot be optimized using an index, so it will be expensive for a large table.
Try this Out:
SELECT MemberID,MemberName,Group_Concat(C.Course) from
(
SELECT MemberID,MemberName,SUBSTRING_INDEX(SUBSTRING_INDEX(t.CoursesInterested, ',', n.n), ',', -1) value
FROM Table1 t CROSS JOIN
(
SELECT a.N + b.N * 10 + 1 n
FROM
(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b
ORDER BY n
) n
WHERE n.n <= 1 + (LENGTH(t.CoursesInterested) - LENGTH(REPLACE(t.CoursesInterested, ',', '')))
ORDER BY MemberID,value
) T JOIN course C ON T.value = C.CourseId
Group By MemberID,MemberName
Fiddle Demo
Output:
MemberID MemberName CoursesInterested
-------------- --------------------- --------------
1 Al MBA,French,Fashion,IT
2 A2 English,Fashion,IT

Mysql - Return rows that have no associated records in another table plus max that do have records

A simplified version of my problem. I have 2 tables:
Scores:
id code_id score
1 11 100
2 12 20
3 13 40
4 14 70
5 15 90
6 16 10
7 17 30
8 18 50
Codes:
id code
11
12
13
14
15
16 BBB
17 BBB
18 BBB
I need to produce a Mysql SELECT query that would return all the rows from the Scores table that have no associated codes in the Codes table plus just the highest score from rows that do have identical codes in the Codes table.
The required output is show below. Score ID's 1-5 are present as they have no associated code but only ID 8 is selected because it is the highest value of all the scores with a BBB code.
id code_id score
1 11 100
2 12 20
3 13 40
4 14 70
5 15 90
8 18 50
I hope that makes sense. I thought this would be an easy one but It has me puzzled. I have checked out a load of the [greatest-n-per-group] tagged questions and most seem to only reference one table when performing GROUP BYs and subselects and have not helped.
Many thanks.
Maybe I am missing something with your requirements but it might be easier to use a UNION ALL between two queries.
To get the rows that have a null code in the codes table you can use:
select s.id, s.code_id, s.score
from scores s
where exists (select id
from codes c
where s.code_id = c.id
and c.code is null)
Then to get the max(score) for each code, you can use:
select s.id, s.code_id, s.score
from scores s
inner join codes c
on s.code_id = c.id
inner join
(
select c.code, max(s.score) score
from scores s
inner join codes c
on s.code_id = c.id
where c.code is not null
group by c.code
) m
on c.code = m.code
and s.score = m.score;
Finally you can use a UNION ALL to combined the two queries:
select s.id, s.code_id, s.score
from scores s
where exists (select id
from codes c
where s.code_id = c.id
and c.code is null)
union all
select s.id, s.code_id, s.score
from scores s
inner join codes c
on s.code_id = c.id
inner join
(
select c.code, max(s.score) score
from scores s
inner join codes c
on s.code_id = c.id
where c.code is not null
group by c.code
) m
on c.code = m.code
and s.score = m.score
group by c.code //To remove duplicates where the code and the score are equal
See SQL Fiddle with Demo.
Edit as a side note if the code value is an empty string and not null, then you can alter the code to use '' (see demo).
Try this:
select s.*
from scores s join
(select coalesce(code, cast(id as varchar(32))) as codeid, max(id) as id
from codes c
group by coalesce(code, cast(id as varchar(32)))
) c
on s.code_id = c.id;
The idea is to summarize the codes table to get the maximum id per code, as well as a single row for each id where the code is NULL.
The following shows the results (apologies, this is for SQL Server but the code would be quite similar there):
declare #scores table (id int identity(1, 1), code_id int, score int);
insert into #scores(code_id, score)
select 11, 100 union all
select 12, 20 union all
select 13, 40 union all
select 14, 70 union all
select 15, 90 union all
select 16, 30 union all
select 17, 30 union all
select 18, 50;
declare #codes table (id int, code varchar(3));
insert into #codes(id, code)
select 11, NULL union all
select 12, NULL union all
select 13, NULL union all
select 14, NULL union all
select 15, NULL union all
select 16, 'BBB' union all
select 17, 'BBB' union all
select 18, 'BBB';
select s.*
from #scores s join
(select coalesce(code, cast(id as varchar(32))) as codeid, max(id) as id
from #codes c
group by coalesce(code, cast(id as varchar(32)))
) c
on s.code_id = c.id