Grouping results after join and having clause - mysql

Let's say I have three tables (mysql):
recipes
+----+----------------+--------------+
| id | title | image |
+----+----------------+--------------+
| 2 | recipe title 1 | banana image |
| 3 | recipe title 2 | potato image |
+----+----------------+--------------+
ingredient
+----+-----------+---------+---------------+
| id | recipe_id | food_id | quantity_kg |
+----+-----------+---------+---------------+
| 1 | 2 | 36 | 2.5 |
| 2 | 3 | 37 | 1.5 |
+----+-----------+---------+---------------+
food
+----+---------+-------+-----------+----------+
| id | name | price | foodType | unitType |
+----+---------+-------+-----------+----------+
| 36 | carrot | 2 | vegetable | kg |
| 37 | chicken | 12 | meat | kg |
+----+---------+-------+-----------+----------+
Now, I want to get all the recipes that are vegetarian, i.e. that don't contain any foods where foodType is 'meat' (or other animal product).
How do I perform such query?
Here is what I've tried so far:
SELECT
recipe.id as recipeId,
recipe.title as title,
food.type as foodType
FROM recipe
INNER JOIN ingredient on ingredient.recipe_id = recipe.id
INNER JOIN food on food.id = ingredient.aliment_id
HAVING
food.type <> 'meat' AND
food.type <> 'seafood' AND
food.type <> 'fish'
ORDER BY recipeId
This works (I get only vegetarian recipes) BUT it duplicates all the recipes, as long as they have multiple ingredients. eg. :
+----------+--------+----------+
| recipeId | title | foodType |
+----------+--------+----------+
| 5 | titleA | type1 |
| 5 | titleA | type2 |
| 5 | titleA | type3 |
| 8 | titleB | type2 |
| 8 | titleB | type5 |
| 8 | titleB | type1 |
| 8 | titleB | type3 |
+----------+--------+----------+
What I want to obtain is:
+----------+--------+
| recipeId | title |
+----------+--------+
| 5 | titleA |
| 8 | titleB |
+----------+--------+
I already tried getting rid of 'foodType' in the SELECT clause, but if I do so, mysql tells me : "Unknown column 'food.type' in 'having clause'"
I already tried to GROUP BY 'recipeId' right before HAVING clause, but I get that error : "Expression #3 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'myDb.food.type' which is not functionally dependent on columns in GROUP BY clause" (I understand that error).
I guess it has to do with something like "Grouping results after join and having clause", but I might be wrong...
Thanks a lot

You don't have a GROUP BY clause, so you shouldn't have a HAVING clause. Use WHERE instead
Remove the unwanted columns from your SELECT
Because the joins are across 1:many relationships but you're only selecting on the "one" side, you probably also want SELECT DISTINT instead of just SELECT
Also, you have another issue: The logic of your query isn't actually correct, even though it's returning apparently correct results with such a small set of sample data.
When looking at composition, you probably want to use EXISTS and a subquery. Perhaps something like this (untested):
SELECT
recipe.id as recipeId,
recipe.title as title,
food.type as foodType
FROM recipe r
WHERE NOT EXISTS
(SELECT food.type
FROM ingredient INNER JOIN food on food.id = ingredient.aliment_id
WHERE ingredient.recipe_id = r.id AND
food.type IN ('meat', 'seafood','fish')
)
ORDER BY recipeId

just exclude all recipes which have at least one meat ingridient
(i did it 10 years ago even without sql)
SELECT recipe.id, recipe.title FROM recipe
WHERE recipe.id NOT IN (
SELECT
recipe.id,
FROM recipe
INNER JOIN ingredient
on ingredient.recipe_id = recipe.id
INNER JOIN food
on food.id = ingredient.aliment_id
AND food.type IN ('meat', 'seafood', 'fish')
)
ORDER BY recipeId

Related

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 foreign key and xref table

I am trying to understand MySQL xref table and foreign key structures.
I have three tables with the following structures;
table_food
| id | name |
| 1 | apple_pie |
| 2 | pumpkin_pie |
table_ingredient
| id | name |
| 1 | apple |
| 2 | pumpkin |
| 3 | milk |
| 4 | flour |
| 5 | soy_milk |
table_food_ingredient
| food_id | ingredient_id |
| 1 | 1 |
| 1 | 3 |
| 1 | 4 |
| 2 | 2 |
| 2 | 3 |
I know how to get the ingredients from the table_food_ingredient to get all ingredient to apple_pie, should be the code below
SELECT ingredient_id FROM table_food_ingredient WHERE food_id = 1;
But I can't figure out how I get food_id from all the matching ingredient_id.
for example i want to get food_id if i select ingredient_id of 1, 3, 4 which should only give me food_id of 1(apple_pie)
To begin, if I understood correctly we must recover the number of the food according to the ingredients.
What is the problem ?
We want to retrieve a single result but the table contains multiple lines for a single id.
So we can't doing this with simple SELECT .. WHERE.
The solution
A beginning of solution is in this link Select all rows with multiple conditions.
You can get the result by using a combination of a WHERE, GROUP BY.
SELECT f.id, f.name
FROM table_food f
INNER JOIN table_food_ingredient fi
ON f.id = fi.food_id
WHERE fi.ingredient_id in(1, 3, 4)
GROUP BY f.id, f.name
HAVING count(distinct fi.ingredient_id) = 3;
Try this
SELECT food_id FROM Table1 WHERE ingredient_id in(1,3,4)
GROUP BY food_id
HAVING count(distinct ingredient_id)=3;
ingredient_id in(1,3,4) - contains all the ingredients.
count(distinct ingredient_id)=3; - The number should be count of ingredients in
IN

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.

Need MySQL query using left join which displays null for duplicate data on left

(Simplified)
In a table PET_TYPES the columns are id, type
In a table PET_NAMES the columns are id, type_id, pet_name
The basic query is:
select pt.*, pn.pet_name from PET_TYPES pt left join PET_NAMES pn on pt.id=pn.type_id
I need for display purposes, however, to show NULL for all duplicate values on the left. Instead of:
id | type | pet_name
1 | aardvark | NULL
2 | dog | Charlie
3 | dog | Rover
4 | cat | Tabby
5 | cat | Sandy
6 | cat | MeowMeow
I need:
id | type | petname
1 | aardvark | NULL
2 | dog | Charlie
3 | NULL | Rover
4 | cat | Tabby
5 | NULL | MeowMeow
6 | NULL | Sandy
TIA
You can do this using a variable and an IF, as follows:
SELECT id,
IF(type=#prev,NULL,#prev:=type) AS type,
petname
FROM (
select pt.*, pn.pet_name
from PET_TYPES pt
left join PET_NAMES pn on pt.id=pn.type_id
) AS list,
(SELECT #prev:='') AS init
ORDER BY list.type, list.id
Notice how your query is wrapped up in an outer one.
The IF statement suppresses the consecutive duplicates.

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
);