Selecting rows whose foreign rows ONLY match a single value - mysql

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.

Related

Group and join two tables based on id?

I have a mysql table that looks something like this:
id | name
---+-------
1 | cola
2 | pepsi
3 | sprite
and another table:
customer | buy1 | buy2
---------+------+-----
Jhon | 2 | 3
Alice | 1 | 3
Tony | 3 | 2
I want to join the two tables and generate
customer | buy1 | buy2
---------+-------+--------
Jhon | Pepsi | Sprite
Alice | Cola | Sprite
Tony | Sprite| Pepsi
SELECT C.customer, REF.NAME, REF2.NAME
FROM OTHER_TABLE AS C
JOIN TABLE_SOMETHING_LIKE_THIS AS REF ON C.BUY1 = REF.ID
JOIN TABLE_SOMETHING_LIKE_THIS AS REF2 ON C.BUY2 = REF2.ID
You can write subqueries in a select list like this:
select
customer,
(select name from tbl1 where id = buy1) buy1,
(select name from tbl1 where id = buy2) buy2
from
tbl2;

How to query many-to-many relation with features table (AND condition)

I guess this is a common setting, but as I don't do that much SQL work, I can't get my head around this one... So, I've got a bunch of songs that have certain features (style of music, mood etc.) and I would like to select songs that are attributed some of these features (e. g. songs that are happy and euphoric).
SONG
+----+----------+
| id | title |
+----+----------+
| 1 | song #1 |
+----+----------+
| 2 | song #2 |
+----+----------+
FEATURE
+----+-------+----------+
| id | name | value |
+----+-------+----------+
| 1 | mood | sad |
+----+-------+----------+
| 2 | mood | happy |
+----+-------+----------+
| 3 | mood | euphoric |
+----+-------+----------+
| 4 | style | rock |
+----+-------+----------+
| 5 | style | jazz |
+----+-------+----------+
SONG_FEATURE
+---------+------------+
| song_id | feature_id |
+---------+------------+
| 1 | 1 |
+---------+------------+
| 2 | 1 |
+---------+------------+
| 2 | 2 |
+---------+------------+
I would like to select all the songs that have certain features with an AND condition. I would use this query for the OR-case.
SELECT
s.*,
f.*
FROM
song_feature sf
LEFT JOIN song s ON s.id = sf.song_id
LEFT JOIN feature f ON f.id = sf.feature_id
WHERE
(
f.name = 'style'
AND f.value = 'pop'
)
OR /* <-- this works, but I would like an AND condition */
(
f.name = 'style'
AND f.value = 'pop'
)
GROUP BY sf.song_id;
But this obviously does not work for the AND condition, so I guess I'm on the wrong track here... Any hints will be greatly appreciated.
You can do it with aggregation, if you filter the resultset of the joins and set the condition in the HAVING clause:
SELECT s.id, s.title
FROM SONG s
INNER JOIN SONG_FEATURE sf ON sf.song_id = s.id
INNER JOIN FEATURE f ON f.id = sf.feature_id
WHERE (f.name, f.value) IN (('mood', 'sad'), ('mood', 'happy'))
GROUP BY s.id, s.title
HAVING COUNT(DISTINCT f.name, f.value) = 2
See the demo.
Results:
> id | title
> -: | :------
> 2 | song #2

mySQL JOIN of several tables

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.

Selecting 2 columns from a MySQL db with join

I have the following tables:
Teams
+------+--------------+--------------+
| id | team_name | team_code |
+------+--------------+--------------+
| 1 | Wales | WAL |
| 2 | England | ENG |
| 3 | New Zealand | NZL |
+------+--------------+--------------+
Matches
+------+-----------+-----------+
| id | team_a | team_b |
+------+-----------+-----------+
| 1 | WAL | ENG |
| 2 | ENG | NZL |
| 3 | WAL | NZL |
+------+-----------+-----------+
I know how a join works, but I can't get my head around how I can query to the database by Matched.id to get the team name for BOTH teams from the Teams database. Let me explain better
For each match I need to determine the team names
I will query by Matches.id
So for Match: 1 I'll need to select 'Wales' and 'England'
Hope I've explained my problem clearly enough, but If I haven't please feel free to ask more questions
The database structure seems to be "off" to me.
You should have both team_a and team_b as foreign keys to the Teams table primary key.
You can then join on the IDs (twice) to get the full names.
Matches:
+------+-----------+-----------+
| id | team_a | team_b |
+------+-----------+-----------+
| 1 | 1 | 2 |
| 2 | 2 | 3 |
| 3 | 1 | 3 |
+------+-----------+-----------+
SELECT T1.team_name, T2.team_name
FROM Matches M
INNER JOIN Teams T1
ON M.team_a = T1.id
INNER JOIN Teams T2
ON M.team_b = T2.id
WHERE M.id = 1
Select
matches.*,
teamA.team_name as TeamA_Name,
teamB.team_name as TeamB_Name
from matches
inner join teams teamA on matches.team_a = TeamA.team_code
inner join teams teamB on matches.team_b = TeamB.team_code
where
matches.id = 1
select
m.id,
t1.team_name,
t2.team_name
from
Matches m
inner join Teams t1 on m.team_a = t1.team_code
inner join Teams t2 on m.team_b = t2.team_code

many-to-many and many-to-many intersections

Say I have a database that has people, grocery stores, and items you can buy in the store, like so:
Stores People Foods
----------------- ------------------ ------------------
| id | name | | id | name | | id | name |
----------------- ------------------ ------------------
| 1 | Giant | | 1 | Jon Skeet | | 1 | Tomatoes |
| 2 | Vons | | 2 | KLee1 | | 2 | Apples |
| 3 | Safeway | ------------------ | 3 | Potatoes |
----------------- ------------------
I have an additional table which keep track of which stores sell what:
Inventory
--------------------
| store_id| food_id|
--------------------
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
| 3 | 1 |
| 3 | 2 |
| 3 | 3 |
--------------------
And I have another table that has shopping lists on it
Lists
---------------------
| person_id| food_id|
---------------------
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
| 2 | 3 |
---------------------
My question is, given a person, or their id, what is the best way to figure out what stores they can go to so they will get everything on their list. Is there a pattern for these types of computations in MySQL?
My attempt (very ugly and messy) is something like:
-- Given that _pid is the person_id we want to get the list of stores for.
SELECT stores.name, store_id, num, COUNT(*) AS counter
FROM lists
INNER JOIN inventory
ON (lists.food_id=inventory.food_id)
INNER JOIN (SELECT COUNT(*) AS num
FROM lists WHERE person_id=_pid
GROUP BY person_id) AS T
INNER JOIN stores ON (stores.id=store_id)
WHERE person_id=_pid
GROUP BY store_id
HAVING counter >= num;
Thanks for your time!
Edit SQL Fiddle with Data
If I were to solved the problem, I'll join the four tables with their linking column (specifically the foreign keys) then a subquery on the HAVING clause to count the number of items on the list for each person. Give this a try,
SET #personID := 1;
SELECT c.name
FROM Inventory a
INNER JOIN Foods b
ON a.food_id = b.id
INNER JOIN Stores c
ON a.store_id = c.id
INNER JOIN Lists d
ON d.food_id = b.id
WHERE d.person_id = #personID
GROUP BY c.name
HAVING COUNT(DISTINCT d.food_id) =
(
SELECT COUNT(*)
FROM Lists
WHERE person_ID = #personID
)
SQLFiddle Demo
#JohnWoo: why DISTINCT?
Another one...
SET #pid=2;
SELECT store_id, name
FROM inventory
JOIN lists ON inventory.food_id=lists.food_id
JOIN stores ON store_id=stores.id
WHERE person_id=#pid
GROUP BY store_id
HAVING COUNT(*)=(
SELECT COUNT(*)
FROM lists
WHERE person_id=#pid
);