MySQL table "pivot" without creating tables/views: unique column values as header - mysql

I have this 'occupations' two-column table, holding the name and occupation of multiple persons. The occupations are known and can only be 'Developer', 'Engineer','Doctor','Musician'.
Name | Occupation
Dan | Developer
Martin | Doctor
Sam | Engineer
Andre | Musician
Tom | Engineer
The aim is to obtain something like the following:
Doctor | Engineer | Developer | Musician
Martin | Sam | Dan | Andre
NULL | Tom | NULL | NULL
All columns should be alphabetically ordered.
Do you guys have any suggestions regarding how I can this be achieved (without creating tables, views) using MySQL?
Thanks a lot!

This is a pain, but you can do it using variables and aggregation:
select max(doctor) as doctor,
max(engineer) as engineer,
max(developer) as developer,
max(musician) as musician
from ((select name as doctor, null as engineer, null as developer, null as musician,
(#rnd := #rnd + 1) as rn
from t cross join
(select #rnd := 0) as params
where occupation = 'doctor'
) union all
(select null as doctor, name as engineer, null as developer, null as musician,
(#rne := #rne + 1) as rn
from t cross join
(select #rne := 0) as params
where occupation = 'engineer'
) union all
(select null as doctor, null as engineer, name as developer, null as musician,
(#rnv := #rnv + 1) as rn
from t cross join
(select #rnv := 0) as params
where occupation = 'developer'
) union all
(select null as doctor, null as engineer, null as developer, name as musician,
(#rnm := #rnm + 1) as rn
from t cross join
(select #rnm := 0) as params
where occupation = 'musician'
) union all
) o
group by rn;

This will work in MySql 8.0:
with occup as (
select
case when o.occupation = 'Doctor' then o.Name end as Doctor,
case when o.occupation = 'Engineer' then o.Name end as Engineer,
case when o.occupation = 'Developer' then o.Name end as Developer,
case when o.occupation = 'Musician' then o.Name end as Musician
from occupations o
),
doctors as (
select ROW_NUMBER() OVER (
ORDER BY case when occup.Doctor is null then 1 else 0 end
) as rn, occup.Doctor from occup
),
engineers as (
select ROW_NUMBER() OVER (
ORDER BY case when occup.Engineer is null then 1 else 0 end
) as rn, occup.Engineer from occup
),
developers as (
select ROW_NUMBER() OVER (
ORDER BY case when occup.Developer is null then 1 else 0 end
) as rn, occup.Developer from occup
),
musicians as (
select ROW_NUMBER() OVER (
ORDER BY case when occup.Musician is null then 1 else 0 end
) as rn, occup.Musician from occup
)
select doctors.Doctor, engineers.Engineer, developers.Developer, musicians.Musician
from doctors
inner join engineers on doctors.rn = engineers.rn
inner join developers on engineers.rn = developers.rn
inner join musicians on musicians.rn = developers.rn
WHERE coalesce(doctors.Doctor, engineers.Engineer, developers.Developer, musicians.Musician) IS NOT NULL;
See the demo

Related

Is there a way to stop writing same subqueries again and again when joining subqueries in MySQL

I need to full outer join these subqueries
SET #n1 = 0;
SET #n2 = 0;
SELECT * FROM
(SELECT (#n1:=#n1 + 1) AS n, name FROM occupations WHERE occupation="Doctor") AS t1
LEFT OUTER JOIN
(SELECT (#n2:=#n2 + 1) AS n, name FROM occupations WHERE occupation="Professor") AS t2
ON t1.n=t2.n
UNION
SELECT * FROM
(SELECT (#n1:=#n1 + 1) AS n, name FROM occupations WHERE occupation="Doctor") AS t1
RIGHT OUTER JOIN
(SELECT (#n2:=#n2 + 1) AS n, name FROM occupations WHERE occupation="Professor") AS t2
ON t1.n=t2.n
;
Here I had to write the same subqueries again and again.
If there is any method like below it will be very easy
SET #n1 = 0;
SET #n2 = 0;
t1 = (SELECT (#n1:=#n1 + 1) AS n, name FROM occupations WHERE occupation="Doctor");
t2 = (SELECT (#n2:=#n2 + 1) AS n, name FROM occupations WHERE occupation="Professor");
SELECT * FROM t1 LEFT OUTER JOIN t2 ON t1.n=t2.n
UNION
SELECT * FROM t1 RIGHT OUTER JOIN AS t2 ON t1.n=t2.n;
But I don't know any method to do something like that. Also I don't want to create any views or temporary tables to do this. Please help. Thank you.
Pivot questions are usually tackled using conditional aggregation in mysql for example
drop table if exists t;
create table t
(name varchar(10), job varchar(10));
insert into t values
('aaa','dr'),
('bbb','plumber'),
('ccc','lab'),
('ddd','dr'),
('eee','plumber'),
('ggg','lab'),
('fff','plumber')
;
select max(case when job = 'dr' then name else null end) dr,
max(case when job = 'plumber' then name else null end) plumber,
max(case when job = 'lab' then name else null end) lab
from
(
select name,job,
if(job <> #p,#rn:=1,#rn:=#rn+1) rn,
#p:=job p
from t
cross join (select #rn:=0,#p:='') r
order by job, name
) s
group by rn;
+------+---------+------+
| dr | plumber | lab |
+------+---------+------+
| aaa | bbb | ccc |
| ddd | eee | ggg |
| NULL | fff | NULL |
+------+---------+------+
3 rows in set (0.02 sec)
Note the generation of a row number to enable the group (in version 8.0 you could use row_number function) and the ordering by name.
I were able to do what i wanted to do. Here is my code. Thank you everyone.
SET #n1 = 0;
SET #n2 = 0;
SET #n3 = 0;
SET #n4 = 0;
select t1.name, t2.name, t3.name, t4.name from
(select
(#n1:=#n1 + 1) AS n,
case when occupation = 'Doctor' then name else null end as name
from occupations
order by if(occupation = 'Doctor',0,1), name) as t1,
(select
(#n2:=#n2 + 1) AS n,
case when occupation = 'Professor' then name else null end as name
from occupations
order by if(occupation = 'Professor',0,1), name) as t2,
(select
(#n3:=#n3 + 1) AS n,
case when occupation = 'Singer' then name else null end as name
from occupations
order by if(occupation = 'Singer',0,1), name) as t3,
(select
(#n4:=#n4 + 1) AS n,
case when occupation = 'Actor' then name else null end as name
from occupations
order by if(occupation = 'Actor',0,1), name) as t4
where t1.n=t2.n and t2.n=t3.n and t3.n=t4.n and not(t1.name is null and t2.name is null and t3.name is null and t4.name is null);

How to use IN in sql query when table has key , values pairs

I am working on a query which is having key value pairs
student
StdId StuName phnNum
1 John 87678
student_meta_data
S.NO field_name field_value StdId
1 college St.Anns 1
2 Address Arizona 1
3 IdNum 321 1
4 Subject Maths 1
5 Marks 90 1
6 Subject Physics 1
7 Marks 80 1
I would like to fetch data from student_meta_data table, for this I had written query like the below,
select
case when student_meta_data.field_name = 'Subject' Then field_value end as subject
case when student_meta_data.field_name ='Marks' Then field_value end as marks
case when student_meta_data.field_name = 'IdNum' Then field_value end as IdNum
from student_meta_data
where student_meta_data.StdId=1
&& student_meta_data.field_name in ('Subject')
for the above query I am fetching records like the below,
subject marks IdNum
null null null
I am expecting to fetch records like below,
subject marks IdNum
Maths 90 321
Physics 80 321
can you one suggest in this. Thanks in advance.
SQL DEMO
SELECT rn,
MAX(subject) as subject,
MAX(case when field_name = 'Marks' Then field_value end) as marks,
MAX(idNum) as idNum
FROM ( SELECT m.*,
#idNum := if(`field_name` = 'IdNum', `field_value`, #idNum) as idNum,
#subject := if(`field_name` = 'Subject', `field_value`, #subject) as subject,
#rn := if (#s = #subject,
#rn,
if(#s := #subject, #rn+1, #rn+1)
) as rn
FROM student_meta_data m
CROSS JOIN (SELECT #idNum := 0, #rn := 0, #subject := '', #s := '' ) as var
ORDER BY `SNO` ) as T
WHERE rn > 0
GROUP BY rn;
OUTPUT
Using variable to track idNum and creating the groups for each subject. First query is just the inner subquery for debug propose, the final is your desire result
Your data model leaves a lot to be desired. Nevertheless, here's something to think about...
SELECT a.field_value subject
, b.field_value marks
FROM
( SELECT x.*
, MIN(y.id) y_id
FROM student_meta_data x
JOIN student_meta_data y
ON y.id > x.id
AND y.field_name = 'marks'
WHERE x.field_name = 'subject'
GROUP
BY x.id
) a
JOIN student_meta_data b
ON b.id = a.y_id;
+---------+-------+
| subject | marks |
+---------+-------+
| Maths | 90 |
| Physics | 80 |
+---------+-------+
I can't stress this enough, but your design needs a change. Normalize it.
Derived from #Juan's answer.
You can do this using user variables and simple filtering on that.
select idnum, subject, marks
from (
select
#idnum := if(field_name='IdNum', field_value, #idnum) as idnum,
#subject := if(field_name='Subject', field_value, #subject) as subject,
#marks := if(field_name='Marks', field_value, #marks) as marks,
m.field_name
from student_meta_data
cross join (
select #idnum := 0,
#subject := null,
#marks := 0
) x
order by sno
) t
where field_name = 'Marks';
I'm filtering based on just Marks field_name because it comes last in the given order and we'll have all the other required values set by then.
Demo

Multiple Alias, one Column

I am trying to give multiple Aliases to the same column, basically, i want these two queries to be one:
SELECT name AS singlePeople FROM People
JOIN ID FROM Numbers
ON People.ID=Numbers.ID
WHERE People.isMarried=f;
SELECT name AS marriedPeople FROM People
JOIN ID FROM Numbers
ON People.ID=Numbers.ID
WHERE People.isMarried=t;
I want my results to look like:
singlePeople marriedPeople
------------- --------------
Bob Kelly John SMith
John Adams
Is this sufficient?
SELECT (CASE WHEN p.isMarried THEN 'Married' ELSE 'Single' END) as which,
name
FROM People p JOIN
Numbers n
ON p.ID = n.ID;
If not, you can do this with variables:
select max(case when not ismarried then name end) as single,
max(case when ismarried then name end) as married
from (select name, p.ismarried,
(#rn := if(#i = ismarried, #rn + 1,
if(#i := ismarried, 1, 1)
)
) as seqnum
from people p join
numbers n
on p.id = n.id cross join
(select #i := NULL, #rn := 0) params
order by ismarried
) pn
group by rn;

What's the SQL idiom for zipping — in the functional sense — two queries?

For example, if I have a set of classes and a set of classrooms, and I want to pair the two up with some arbitrary pairing:
> SELECT class_name FROM classes ORDER BY class_name
Calculus
English
History
> SELECT room_name FROM classrooms ORDER BY room_name
Room 101
Room 102
Room 201
I'd like to "zip" them like this:
> SELECT class_name FROM classes ORDER … ZIP SELECT room_name FROM classrooms ORDER …
Calculus | Room 101
English | Room 102
History | Room 201
Currently I'm dealing with MySQL… but possibly — optimistically? — there is a reasonably standards compliant way to do this?
One way to do it in MySql
SELECT c.class_name, r.room_name
FROM
(
SELECT class_name, #n := #n + 1 rnum
FROM classes CROSS JOIN (SELECT #n := 0) i
ORDER BY class_name
) c JOIN
(
SELECT room_name, #m := #m + 1 rnum
FROM classrooms CROSS JOIN (SELECT #m := 0) i
ORDER BY room_name
) r
ON c.rnum = r.rnum
Output:
| CLASS_NAME | ROOM_NAME |
-------------|-----------|
| Calculus | Room 101 |
| English | Room 102 |
| History | Room 201 |
Here is SQLFIddle demo
Same thing in Postgres will look like
SELECT c.class_name, r.room_name
FROM
(
SELECT class_name,
ROW_NUMBER() OVER (ORDER BY class_name) rnum
FROM classes
) c JOIN
(
SELECT room_name,
ROW_NUMBER() OVER (ORDER BY room_name) rnum
FROM classrooms
) r
ON c.rnum = r.rnum
Here is SQLFiddle demo
And in SQLite
SELECT c.class_name, r.room_name
FROM
(
SELECT class_name,
(SELECT COUNT(*)
FROM classes
WHERE c.class_name >= class_name) rnum
FROM classes c
) c JOIN
(
SELECT room_name,
(SELECT COUNT(*)
FROM classrooms
WHERE r.room_name >= room_name) rnum
FROM classrooms r
) r
ON c.rnum = r.rnum
Here is SQLFiddle demo
This is a form of join, but you need to create the join key. Alas, though, this requires a full outer join, because you do not know which list is longer.
So, you can do this by using variables to enumerate the rows and then using union all and group by to get the values:
select max(case when which = 'class' then name end) as class_name,
max(case when which = 'room' then name end) as room_name
from ((SELECT class_name as name, #rnc := #rnc + 1 as rn, 'class' as which
FROM classes cross join
(select #rnc := 0) const
ORDER BY class_name
) union all
(select room_name, #rnr := #rnr + 1 as rn, 'room'
from classrooms cross join
(select #rnr := 0) const
ORDER BY room_name
)
) t
group by rn;

optimize mysql query: medal standings

I have 2 tables:
olympic_medalists with columns gold_country, silver_country, bronze_country
flags with country column
I want to list the olympic medal table accordingly. I have this query, it works, but it seems to kill mysql. Hope someone can help me with an optimized query.
SELECT DISTINCT country AS sc,
IFNULL(
(SELECT COUNT(silver_country)
FROM olympic_medalists
WHERE silver_country = sc AND silver_country != ''
GROUP BY silver_country),0) AS silver_medals,
IFNULL(
(SELECT COUNT(gold_country)
FROM olympic_medalists
WHERE gold_country = sc AND gold_country != ''
GROUP BY gold_country),0) AS gold_medals,
IFNULL(
(SELECT COUNT(bronze_country)
FROM olympic_medalists
WHERE bronze_country = sc AND bronze_country != ''
GROUP BY bronze_country),0) AS bronze_medals
FROM olympic_medalists, flags
GROUP BY country, gold_medals, silver_country, bronze_medals HAVING (
silver_medals >= 1 || gold_medals >= 1 || bronze_medals >= 1)
ORDER BY gold_medals DESC, silver_medals DESC, bronze_medals DESC,
SUM(gold_medals+silver_medals+bronze_medals)
result will be like:
country | g | s | b | tot
---------------------------------
country1 | 9 | 5 | 2 | 16
country2 | 5 | 5 | 5 | 15
and so on
Thanks!
olympic medalists:
`id` int(8) NOT NULL auto_increment,
`gold_country` varchar(64) collate utf8_unicode_ci default NULL,
`silver_country` varchar(64) collate utf8_unicode_ci default NULL,
`bronze_country` varchar(64) collate utf8_unicode_ci default NULL, PRIMARY KEY (`id`)
flags
`id` int(11) NOT NULL auto_increment,
`country` varchar(128) default NULL,
PRIMARY KEY (`id`)
This will be much more efficient than your current solution of executing three different SELECT subqueries for each row in a cross-joined relation (and you wonder why it stalls out!):
SELECT a.country,
COALESCE(b.cnt,0) AS g,
COALESCE(c.cnt,0) AS s,
COALESCE(d.cnt,0) AS b,
COALESCE(b.cnt,0) +
COALESCE(c.cnt,0) +
COALESCE(d.cnt,0) AS tot
FROM flags a
LEFT JOIN (
SELECT gold_country, COUNT(*) AS cnt
FROM olympic_medalists
GROUP BY gold_country
) b ON a.country = b.gold_country
LEFT JOIN (
SELECT silver_country, COUNT(*) AS cnt
FROM olympic_medalists
GROUP BY silver_country
) c ON a.country = c.silver_country
LEFT JOIN (
SELECT bronze_country, COUNT(*) AS cnt
FROM olympic_medalists
GROUP BY bronze_country
) d ON a.country = d.bronze_country
What would be even faster is instead of storing the actual textual country name in each of the gold, silver, and bronze columns, just store the integer-based country id. Comparisons on integers are always going to be faster than comparisons on strings.
Moreover, once you replace each country name in the olympic_medalists table with the corresponding id's, you'll want to create an index on each column (gold, silver, and bronze).
Updating the textual names to be their corresponding id's instead is a simple task and could be done with a single UPDATE statement in conjunction with some ALTER TABLE commands.
try this:
SELECT F.COUNTRY,IFNULL(B.G,0) AS G,IFNULL(B.S,0) AS S,
IFNULL(B.B,0) AS B,IFNULL(B.G+B.S+B.B,0) AS TOTAL
FROM FLAGS F LEFT OUTER JOIN
(SELECT A.COUNTRY,
SUM(CASE WHEN MEDAL ='G' THEN 1 ELSE 0 END) AS G,
SUM(CASE WHEN MEDAL ='S' THEN 1 ELSE 0 END) AS S,
SUM(CASE WHEN MEDAL ='B' THEN 1 ELSE 0 END) AS B
FROM
(SELECT GOLD_COUNTRY AS COUNTRY,'G' AS MEDAL
FROM OLYMPIC_MEDALISTS WHERE GOLD_COUNTRY IS NOT NULL
UNION ALL
SELECT SILVER_COUNTRY AS COUNTRY,'S' AS MEDAL
FROM OLYMPIC_MEDALISTS WHERE SILVER_COUNTRY IS NOT NULL
UNION ALL
SELECT BRONZE_COUNTRY AS COUNTRY,'B' AS MEDAL
FROM OLYMPIC_MEDALISTS WHERE BRONZE_COUNTRY IS NOT NULL)A
GROUP BY A.COUNTRY)B
ON F.COUNTRY=B.COUNTRY
ORDER BY IFNULL(B.G,0) DESC,IFNULL(B.S,0) DESC,
IFNULL(B.B,0) DESC,IFNULL(B.G+B.S+B.B,0) DESC,F.COUNTRY