Group results from different tables by rank - mysql

I'm sure I'm not the first to need to do this, but I couldn't find a similar question that accounted for the nuance.
I have 3 tables (fav_food, fav_color, and fav_place) that all follow a similar pattern:
userid | label | rank |
=================================
1 | red | 1
1 | green | 2
1 | orange | 3
2 | blue | 1
2 | red | 2
...
Each table will have at most 3 items per userid, but some users might have fewer than 3, and other users might have none for a given table. i.e., it's possible user 10 has 2 favorite colors, 1 favorite food, and 0 favorite places.
I'm looking for a query that can output my data like so:
userid | fav_food | fav_place | fav_color | rank
===========================================================
1 | pizza | New York | red | 1
1 | burgers | NULL | green | 2
1 | NULL | NULL | orange | 3
2 | tacos | Chicago | blue | 1
2 | burgers | Orlando | red | 2
...
Basically, all ranked 1 items together, ranked 2 items together, and ranked 3 items together (NULLs were no item of that rank exists).
I was able to get it working using 3 separate queries (one for each table) + post processing at the application layer, but for the sake of my personal knowledge base, I was wondering if anyone knew how to do it in a single query.
Many thanks!

#Isick,
You can do this with a LEFT OUTER JOIN on each table to a table containing just the userid and rank. DEMO
select user_rank.userid, user_rank.rank, f.food, p.place, c.color from
(
select userid, rank from fav_food
union
select userid, rank from fav_place
union
select userid, rank from fav_color
) user_rank
left outer join
( select userid, rank, label as food from fav_food) f
on user_rank.userid = f.userid and user_rank.rank = f.rank
left outer join
( select userid, rank, label as place from fav_place) p
on user_rank.userid = p.userid and user_rank.rank = p.rank
left outer join
( select userid, rank, label as color from fav_color) c
on user_rank.userid = c.userid and user_rank.rank = c.rank
order by userid, rank

Related

SQL insert query with some condition

I am using MySQL.
There are three tables presented: Patient, Occupies, Room and Privte_Patient. I need to identify the first available room and allocate the room to a newly admitted patient with PIN '314' (the patient is already added to a database). Note that the room should be either single or multiple occupancy depending whether the patient is private or not.
As a result, I want get the SQL query which will allocate the patient with PIN '314' to the room number 1. Because this patient is not a private patient and room number 1 is the first room with empty bed (size is 2 beds).
Any idea of how to identify this room? Can I do it using Conditional INSERT?
Table Patient
+-------+---------+
| PIN | name |
+-------+---------+
|314 | Lana |
|778899 | Michael |
|345566 | Jone |
+-------+---------+
Table Occupies
+--------+--------+
|patient | room |
+--------+--------+
|778899 | 1 |
|345566 | 4 |
+-------+---------+
Table Room
+--------+--------+
|number | size |
+--------+--------+
| 1 | 2 |
| 2 | 12 |
| 3 | 1 |
| 4 | 1 |
+-------+---------+
Private_Patient
+--------+--------+
|patient |consultant|
+--------+--------+
|345566 | 345566 |
+-------+---------+
Consider:
insert into occupies(patient, room)
select 314, r.number
from room r
left join (select room, count(*) size from occupies group by room) o
on o.room = r.number
where coalesce(o.size, 0) < r.size
order by r.number
limit 1
For each room, the query brings the number of occupants and uses that information to filter out rooms that are full already. The first room where at least in bed is available is selected.
It might be simpler to understand with a correlated subquery:
insert into occupies(patient, room)
select 314, r.number
from room r
where r.size > (
select count(*)
from occupies o
where o.room = r.number
)
order by r.number
limit 1
INSERT INTO Occupies(room, patient)
SELECT number, '314'
FROM Room r
INNER JOIN
(SELECT room, COUNT(patient) amountOfPatientsInRoom from Occupies GROUP BY
room) ON
o.room = r.number
WHERE r.size < o.amountOfPatientsInRoom
LIMIT 1

Select from related tables

So in my data base i have 2 tables related:
The fields are id, name, price and a int so i know if they a all sold or not
fruits
|IDfruit| name | price | sold |
| 1 |orange | 5 | 0
| 2 |apple | 10 | 0
| 3 |grape | 15 | 1
| 4 |lemon | 7 | 1
primary key is IDfruit
images
|IDimage| url | idfruit_image
| 1 | image1.png | 1
| 2 | image2.png | 1
| 3 | image3.png | 2
| 4 | image4.png | 3
| 5 | image5.png | 4
| 6 | image6.png | 4
| 7 | image7.png | 4
IDimage is primary key and idfruit_image is a foreign key that references IDfruit
The result i want is all fruits and the FIRST image of each fruit.
So what i've done is
select fruits.*, url , idfruit_image
from fruits,images
where IDfruit = idfruit_image;
This return all fruits and all images of each fruit, but i would like just one image of each fruit, how can i achieve this?
And what if i want everything from all sold fruits and just the first image of each one
Use GROUP BY to get one row per fruit, and an aggregation function to select one of the images.
SELECT f.*, MAX(url) AS url
FROM fruits AS f
LEFT JOIN images AS i ON f.idfruit = i.idfruit_image
GROUP BY f.idfruit
I understand that you want the first image per fruit, first being defined as: the image with the smallest idimage.
If you just want the url of the image, a correlated subquery should be an acceptable solution:
select
f.*,
(
select i.url
from images i
where i.idfruit_image = f.idfruit
order by i.idimage
limit 1
) url
from fruits f
If you want the whole image record, one option is to join, then filter with a subquery:
select f.*, i.*
from fruits f
inner join images i on i.idimage = (
select min(i1.idimage) from images i1 where i1.idfruit_image = f.idfruit
)
Finally: in MySQL 8.0, you can use row_number() for this:
select *
from (
select
f.*,
i.*,
row_number() over(partition by i.idfruit_image order by i.idimage) as rn
from fruits f
inner join images i on i.idfruit_image = f.idfruit
) t
where rn = 1

MySQL add avg of count by id to existing select with id

Im not even sure what the title of this question should be but lets start out with my data.
I have a table of users who have taken a few lessons while belonging to a particular training center.
lesson table
id | lesson_id | user_id | has_completed
----------------------------------------
1 | asdf3314 | 2 | 1
2 | d13saf12 | 2 | 1
3 | a33adff5 | 2 | 0
4 | a33adff5 | 1 | 1
5 | d13saf12 | 1 | 0
user table
id | center_id | ...
----------------------------------------
1 | 20 | ...
2 | 30 | ...
training center table
id | center_name | ...
----------------------------------------
20 | learn.co | ...
30 | teach.co | ...
I've written a small chunk but am now stuck as I don't know how to proceed. This statement gets the counted total of completed lessons per user. it then figures the average completed value from a center id. if two users belong to a center and have completed 3 lessons and 2 lessons it finds the average of 3 and 2 then returns that.
SELECT
FLOOR(AVG(a.total)) AS avg_completion,
FROM
(SELECT
user_id,
user.center_id,
count(user_id) AS total
FROM lesson
LEFT JOIN user ON user.id = user_id
WHERE is_completed = 1 AND center_id = 2
GROUP BY user_id) AS a;
The question I have is how do I loop through the training centers table and also append average data from similar select statement as above to each center that is queried. I cant seem to pass the center id down to the subquery so there must be a fundamentally different way to achieve the same query but also loop through training centers.
An example of desired result:
center.id | avg_completion | ...training center table
-----------------------------------------------------
20 | 2 | ...
Your main query needs to select a.center_id and then use GROUP BY center_id. You can then join it with the training_center table.
SELECT c.*, x.avg_completion
FROM training_center AS c
JOIN (
SELECT
a.center_id,
FLOOR(AVG(a.total)) AS avg_completion
FROM (
SELECT
user_id
user.center_id,
count(*) AS total
FROM lesson
JOIN user ON user.id = user_id
WHERE is_completed = 1 AND center_id = 2
GROUP BY user_id) AS a
GROUP BY a.center_id) AS x
ON x.center_id = c.id
If I understand correctly:
select u.center_id, count(*) as num_users,
sum(l.has_completed) as num_completed,
avg(l.has_completed) as completed_ratio
from lesson l join
user u
on l.user_id = u.id
group by u.center_id

MySQL. Group matches in columns by a common id

I am codding a search page with multiple filters and I am wondering if this is the best approach to get the results.
Each result of the search has several attributes, here I am using two attributes to simplify the example.
The main 'items' table:
id_items
1
2
The 'languages' table:
id_languages | language_code
1 es
2 en
The 'attributes_one' table:
id_attributes_one
1
2
The 'attributes_one_translations' table:
id_attributes_one_translations | id_attributes_one | id_language_code | translation
1 | 1 | 1 | Oro
2 | 1 | 2 | Gold
3 | 2 | 1 | Plata
4 | 2 | 2 | Silver
The 'attributes_one_match' table:
id_attributes_one_match | id_attributes_one | id_items
1 | 1 | 1
2 | 2 | 1
3 | 1 | 2
The 'attributes_two' table:
id_attributes_two
1
The 'attributes_two_translations' table:
id_attributes_two_translations | id_attributes_two | id_language_code | translation
1 | 1 | 2 | 99% gold
The 'attributes_two_match' table:
id_attributes_two_match | id_attributes_two | id_items
1 | 1 | 1
The concept is one item can have 0 or more match of each attribute table, and that match can have 0 or more translations.
Here is the query I am using when the user selects the filters to get all the items that have the attribute_one 'Gold' or 'Silver' order by this attribute ascendant:
SELECT
i.id_items AS id,
GROUP_CONCAT(DISTINCT aot.translation ORDER BY aot.translation DESC SEPARATOR '!¡') AS attribute_one,
GROUP_CONCAT(DISTINCT att.translation ORDER BY att.translation DESC SEPARATOR '!¡') AS attribute_two
FROM
items i
LEFT JOIN
languages AS l ON l.language_code = 'en'
LEFT JOIN
attributes_one_match AS aom ON aom.id_items = i.id_items
LEFT JOIN
attributes_one_translations AS aot ON aot.id_attributes_one = aom.id_attributes_one
AND l.id_languages = aot.id_language_code
AND (MATCH (aot.translation) AGAINST ('"Gold"' IN BOOLEAN MODE)
OR MATCH (aot.translation) AGAINST ('"Silver"' IN BOOLEAN MODE))
LEFT JOIN
attributes_one AS ao ON ao.id_attributes_one = aom.id_attributes_one
LEFT JOIN
attributes_two_match AS atm ON atm.id_items = i.id_items
LEFT JOIN
attributes_two_translations AS att ON att.id_attributes_two = atm.id_attributes_two
AND l.id_languages = att.id_language_code
LEFT JOIN
attributes_two AS at ON at.id_attributes_two = atm.id_attributes_two
GROUP BY id
ORDER BY 2 ASC
The result I get is:
id | attribute_one | attribute_two
2 | Gold | null
1 | Silver!¡Gold | 99% gold
That result is what I was expecting. Now:
* The table items will have around 300k entries once the data base is filled.
* There are 28 attributes table to match with the item.
Each attribute table will have around 20k entries, and each translation table will have 2
times the entries of the table that represents.
* Each item will have from 0 to 20 match to each item table, so I think
I wont have problems using the function GROUP_CONCAT
I am concern about the performance because the search filter page I am doing updates itself by ajax each time the user change one of the filters (it updates the filters and the results). The max results per page will be 1000 items, I didn't put the LIMIT in the query of the example.
I am not an sql expert so I don't really know if what I am doing is the best approach. I would appreciate some feedback.
Thanks a lot!

Order table by presences in a third table

I have a movie database with a table for actors and another one for movies, I created a third table to add an actor partecipation in a movie. I added a field "star" to distinque leading actors from not leading actors.
I wish create a list order by the actors importance and so by the the total number of "stars".
SELECT a.id, a.name, COUNT( p.star ) AS star
FROM actors a
JOIN playing p, movies m
WHERE p.id_actor = a.id
AND p.id_movie = m.id
AND p.star =1
GROUP BY p.star
ORDER BY p.star DESC;
ACTORS
+----+---------+
| id | name |
+----+---------+
| 1 | actor01 |
| 2 | actor02 |
| 3 | actor03 |
+----+---------+
MOVIES
+----+----------+
| id | title |
+----+----------+
| 1 | movie01 |
| 2 | movie02 |
| 3 | movie03 |
+----+----------+
PLAYING
+----------+----------+-------+------+
| id_movie | id_actor | char | star |
+----------+----------+-------+------+
| 1 | 1 | char1 | 0 |
| 1 | 2 | char2 | 1 |
| 2 | 3 | char3 | 1 |
+----------+----------+-------+------+
I Need output Like:
+----------+--------------+
| actor | protagonist |
+----------+--------------+
| actor01 | 2 times |
| actor02 | 3 times |
+----------+--------------+
You need to fix the group by clause to group by the actor not the star column. You need to fix the order by to group by the aggregated column, not the original column:
SELECT a.id, a.name, sum( p.star = 1) AS stars
FROM actors a join playing p
on p.id_actor = a.id join
movies m
on p.id_movie = m.id
GROUP BY a.id, a.name
ORDER BY stars DESC;
Along the way, I fixed the from so it uses proper join syntax (with an on clause). And changed the query so it returns all actors, even those who have never been the star.
1.If you want to count all stars for an actor, you should group by actor but not stars.(Unless you want to count how many times an actor gets 1 star in a movie, you may not want to group by star)
2.You may want to use ON with JOIN
3.You may want to ORDER BY star but not ORDER BY p.star since you want to order by the result.
4.You may want to use SUM instead of COUNT to get the star counts.(SUM calculates the value but COUNT calculates the number. With SUM, you can set star value to whatever you want without change your sql. You can have star=2 which shows the actor is important to the movie or have star=-1, which means the actor stinks.)
You may have a look at the sql below:
SELECT a.id, a.name, SUM( p.star ) AS sum
FROM actors a
LEFT JOIN playing p ON p.id_actor = a.id
LEFT JOIN movies m ON p.id_movie = m.id
GROUP BY a.id
ORDER BY sum DESC;