mySQL JOIN of several tables - mysql

I do have the following three tables in a MySQL-DB (InnoDB)
UserTab
ID | Name | ---
------------------
1 | Tom |
2 | Dick |
3 | Harry |
EventTab
ID | Name | ---
------------------
1 | Easter |
2 | Holidays |
3 | ThxGiving |
4 | Christmas |
ParticipationTab
ID | UserID | EventID
---------------------
1 | 1 | 1
2 | 1 | 2
3 | 1 | 3
4 | 2 | 1
5 | 2 | 4
6 | 3 | 3
And I want to achieve the follwing result with my query:
QueryResultTab
UserTab.Name | EventTab.Name | NoPart | Names
-----------------------------------------------
Tom | Easter | 2 | Tom, Dick
Tom | Holidays | 1 | Tom
Tom | ThxGiving | 2 | Tom, Harry
Dick | Easter | 2 | Tom, Dick
Dick | Christmas | 1 | Dick
Harry | ThxGiving | 2 | Tom, Harry
I do know about Count() combined with GROUP to get the number of participants
I know about group-concat to get the "Names".
SELECT Event, GROUP_CONCAT(Name ORDER BY Name ASC SEPARATOR ', ') as Names
FROM
(SELECT ID as UserID, Name FROM X_Users WHERE ConditionA) AS UserTab
INNER JOIN
(SELECT EventID, UserID FROM X_Participation WHERE ConditionB) AS ParticipationTab
ON UserTab.UserID = ParticipationTab.UserID
INNER JOIN
(SELECT ID as EventID, Event FROM X_Events WHERE ConditionC) AS EventTab
ON ParticipationTab.EventID = EventTab.EventID
GROUP BY EventTab.EventID
This gives me:
ConcatTab
EventTab.Name | Names
---------------------------
Easter | Tom, Dick
Holidays | Tom
ThxGiving | Tom, Harry
Easter | Tom, Dick
Christmas | Dick
ThxGiving | Tom, Harry
I know about JOINs as you can see. Probably I could use LEFT or RIGHT JOINs as well for this.
For the other parts I use this query:
SELECT Name, Event, NoPart
FROM (SELECT ID as UserID, Name FROM X_Users WHERE ConditionA) AS UserTab
INNER JOIN (SELECT EventID, UserID FROM X_Participation WHERE ConditionB) AS PartTab
ON UserTab.UserID = PartTab.UserID
INNER JOIN (SELECT ID as EventID, Event FROM X_Events WHERE ConditionC) AS EvTab
ON PartTab.EventID = EvTab.EventID
INNER JOIN (SELECT EventID as CntID, COUNT(*) AS NoPart FROM X_Participation WHERE ConditionB) AS CntTab
ON EvTab.EventID = CntTab.CntID
ORDER BY UserTab.UserID
This gives me:
CountTab
UserTab.Name | EventTab.Name | NoPart
--------------------------------------
Tom | Easter | 2
Tom | Holidays | 1
Tom | ThxGiving | 2
Dick | Easter | 2
Dick | Christmas | 1
Harry | ThxGiving | 2
But how to combine/merge ConcatTab and CountTab into QueryResultTab? I want to retrieve the result table in PHP row by row with mysql_fetch_assco().
Please don't tell me about PDO, etc. I know about it.
The other option - what I try to avoid - is do it within a PHP-loop and use numerous tiny SQL-queries to achieve the result.

Based on your sample data, you want all the rows in the participation table, with information from the dimensions. Then you want a summary of that table.
Here is an approach that uses a subquery in the FROM clause:
SELECT u.name, e.name, p2.numPart, p2.Names
FROM X_Participation p INNER JOIN
X_Users u
ON u.UserID = p.UserID INNER JOIN
X_Events e
ON p.EventID = e.EventID INNER JOIN
(SELECT p2.EventId, COUNT(*) as numPart,
GROUP_CONCAT(u2.name SEPARATOR ', ') as names
FROM X_Participation p2 INNER JOIN
X_Users u2
ON u2.UserID = p2.UserID
GROUP BY p2.EventId
) p2
ON p2.EventId = p.EventId;
Notes:
If you are going to use table aliases (which you should), make them shorter, not longer than the table names.
Don't use subqueries unnecessarily. This is especially true in MySQL which materializes subqueries.
You can put additional conditions in a WHERE clause of the outer query.

Related

MySQL - Select from 2 tables even the other table is empty

I have 2 tables consisting of artists and tracks.
Artist
| id | name |
| -------- | -------------- |
| 1 | John Doe |
| 2 | Dave Wang |
Tracks
| id | artist_id | title |
| -------- | -------------- | -------------- |
| 1 | 1 | Song 1 |
| 2 | 1 | Song 2 |
I tried
SELECT a.name, b.title FROM Artist a, Tracks b WHERE a.id = b.artist_id
It returns all the songs of John Doe.
Is there a way to add Dave Wang on the result even it's just null on the title?
For example result
name
title
John Doe
Song 1
John Doe
Song 2
Dave Wang
null
Use an explicit left join:
SELECT a.name, b.title
FROM Artist a
LEFT JOIN Tracks b
ON a.id = b.artist_id;
As a side note, your current query is using the old school implicit join syntax. The version I gave above is the correct way of writing the join.
Please try this query
SELECT a.id, a.name,b.title FROM artist as a LEFT JOIN tracks as b on a.id = b.artist_id;

Mysql query to get max age by section and if two or more has same age return student with smallest id

I have a table of students with temporary test values like this:
Table students
+----+-------------+-------+-----------+
| id | section_id | age | name |
+----+-------------+-------+-----------+
| 1 | 1 | 18 | Justin |
+----+-------------+-------+-----------+
| 2 | 2 | 14 | Jillian |
+----+-------------+-------+-----------+
| 3 | 2 | 16 | Cherry |
+----+-------------+-------+-----------+
| 4 | 3 | 19 | Ronald |
+----+-------------+-------+-----------+
| 5 | 3 | 21 | Marie |
+----+-------------+-------+-----------+
| 6 | 3 | 21 | Arthur |
+----+-------------+-------+-----------+
I want to query the table such that I want to get all the maximum ages of each section. However, if two students have the same age, the table produced will return the student with smallest id.
Return:
+----+------------+-----+--------+
| id | section_id | age | name |
+----+------------+-----+--------+
| 1 | 1 | 18 | Justin |
+----+------------+-----+--------+
| 3 | 2 | 16 | Cherry |
+----+------------+-----+--------+
| 5 | 3 | 21 | Marie |
+----+------------+-----+--------+
I tried this query:
SELECT ANY_VALUE(id), ANY_VALUE(section_id), MAX(age), ANY_VALUE(name) FROM
(SELECT id, section_id, age, name FROM students ORDER BY id) as X
GROUP BY section_id
Unfortunately, there are instances that id does not match the age and name.
I have on my end:
sql_mode = only_full_group_by
and I don't have a privilege to edit that, hence the any_value function but I have no idea how to use it.
This will do what you want.
It starts by finding the maximum age per section (including duplicates).
Then it joins those results with the minimum id per section (to eliminate duplicates).
And finally, select all fields for the matching id and section combinations.
SELECT s3.*
FROM students s3
INNER JOIN (
SELECT MIN(s2.id) AS id, s2.section_id
FROM students s2
INNER JOIN (
SELECT s1.section_id, MAX(s1.age) AS age
FROM students s1
GROUP BY s1.section_id
) s1 USING (section_id, age)
GROUP BY s2.section_id
) s2 USING (id, section_id);
Working SQL fiddle: https://www.db-fiddle.com/f/aezgAYM6A5KnXykceB7At1/0
I would simply use a correlated subquery:
select s.*
from students s
where s.id = (select s2.id
from students s2
where s2.section_id = s.section_id
order by s2.age desc, s2.id asc
limit 1
);
This is pretty much the simplest way to express the logic. And with an index on students(section, age, id), it should be the most performant as well.

SQL join for multiple columns with ids

I have two SQL tables. The first table stores a list of athletes with their id and name. For example:
athlete_id | first_name | last_name
-----------|------------|----------
1 | Matthew | Reese
2 | Tiffanie | Renz
3 | Tom | Dow
etc...
The second table stores entries for a track (sprint) event, and the id of the athlete competing in each lane. For example:
event_id | lane1_athlete_id | lane2_athlete_id | lane3_athlete_id
---------|------------------|------------------|-----------------
1 | 1 | 15 | 24
2 | 18 | 2 | 4
3 | 78 | 50 | 3
etc...
I need to create an SQL query which will return that second table, but with the athlete ids resolved to the athlete names. For example:
event_id | lane1_athlete | lane2_athlete | lane3_athlete
---------|---------------|---------------|--------------
1 | Matthew Reese | Lesa Allain | Nicole Spiers
2 | Emmy Bartol | Tiffanie Renz | Louise Baier
3 | Zack Bui | Norah Flagg | Tom Dow
I imagine this involves a table join, but I can't get my head around the correct query. Any help would be appreciated.
Join the second table to the first one, three times:
SELECT
e.event_id,
CONCAT(a1.first_name, ' ', a1.last_name) AS lane1_athlete,
CONCAT(a2.first_name, ' ', a2.last_name) AS lane2_athlete,
CONCAT(a3.first_name, ' ', a3.last_name) AS lane3_athlete
FROM events e
LEFT JOIN athletes a1
ON e.lane1_athlete_id = a1.athlete_id
LEFT JOIN athletes a2
ON e.lane2_athlete_id = a2.athlete_id
LEFT JOIN athletes a3
ON e.lane3_athlete_id = a3.athlete_id;

Selecting rows whose foreign rows ONLY match a single value

Say I have two tables --people and pets-- where each person may have more than one pet:
people:
+-----------+-------+
| person_id | name |
+-----------+-------+
| 1 | Bob |
| 2 | John |
| 3 | Pete |
| 4 | Waldo |
+-----------+-------+
pets:
+--------+-----------+--------+
| pet_id | person_id | animal |
+--------+-----------+--------+
| 1 | 1 | dog |
| 2 | 1 | dog |
| 3 | 1 | cat |
| 4 | 2 | cat |
| 5 | 3 | dog |
| 6 | 3 | tiger |
| 7 | 3 | tiger |
| 8 | 4 | tiger |
| 9 | 4 | tiger |
| 10 | 4 | tiger |
+--------+-----------+--------+
I'm trying to select the people who ONLY have tigers as pets. Obviously the only one that fits this criteria is Waldo, since Pete has a dog as well... but I'm having some trouble writing the query for this.
The most obvious case is select people.person_id, people.name from people join pets on people.person_id = pets.person_id where pets.animal = "tiger", but this returns Pete and Waldo.
It would be helpful if there was a clause like pets.animal ONLY = "tiger", but as far as I know this doesn't exist.
How could the query be written?
select people.person_id, people.name
from people
join pets on people.person_id = pets.person_id
where pets.animal = "tiger"
AND people.person_id NOT IN (select person_id from pets where animal != 'tiger');
Use group by and having:
select p.person_id
from pets p
group by p.person_id
having max(animal) = 'tiger' and min(animal) = 'tiger';
select distinct person_id
from pets
where animal = "tiger"
intersect
select distinct person_id
from pets
where animal = "tiger"
and person_id not in
(select person_id from pets where animal <> "tiger")
You can use intersect to select a person who only has tiger as his pet.
SELECT *
FROM people pp
WHERE EXISTS (SELECT * FROM pets pt
WHERE pt.person_id = pp.person_id
AND pt.animal = 'tiger'
)
AND NOT EXISTS (SELECT * FROM pets pt
WHERE pt.person_id = pp.person_id
AND pt.animal <> 'tiger'
);
If every person was guaranteed to have at least one pet, then the query could be as simple as:
select name
from people
where not exists (select 1
from pets
where pets.person_id = people.id and
pets.animal != 'tiger')
Or: return the people for whom there is no record that is not a tiger.
NOT EXISTS is executed as a very efficient anti-join, in which each row from people would be rejected as soon as a single non-tiger pet was found.

3 tables and 2 independent JOINs with CONCAT expressions

I have the following 3 tables with IDs
Table: users
user | name
1 | Joe
2 | John
Table: user_id1
user | id1
1 | 2
1 | 3
2 | 5
Table: user_id2
user | id2
1 | 3
1 | 4
I would like to get the following result for each user
name | ids 1 | ids 2
Joe | 2,3 | 3,4
John| 5 | NULL
I use this query:
SELECT
user.name,
GROUP_CONCAT(user_id1.id1) AS "ids1",
GROUP_CONCAT(user_id2.id2) AS "ids2"
FROM users
LEFT JOIN user_id1
ON user_id1.user=users.user
LEFT JOIN user_id2
ON user_id2.user=users.user
But I get this result:
name | ids1 | ids2
Joe | 2,2,3,3 | 3,4,3,4
John | 5 | NULL
What is wrong?
Thanks for your help
Using DISTINCT should solve it
SELECT
user.name,
GROUP_CONCAT(DISTINCT user_id1.id1) AS "ids1",
GROUP_CONCAT(DISTINCT user_id2.id2) AS "ids2"
FROM users
LEFT JOIN user_id1
ON user_id1.user=users.user
LEFT JOIN user_id2
ON user_id2.user=users.user