SQL -- many-to-many query - mysql

I'm trying to select all recipes which contain a given number of ingredients. I'm able to find all recipes based on one given recipe_id
with:
SELECT name
FROM recipe
INNER JOIN recipe_ingredient
ON recipe.recipe_id = recipe_ingredient.recipe_id
WHERE recipe_ingredient.recipe_id = ?
But I'm having trouble figuring out what the query should look like when I'm looking for recipes which contain more than contain more than one specific ingredient. For Example Ingredient A and Ingredient B.
My tables look like this:
ingredient
-ingredient_id
-name
recipe_ingredient
-recipe_ingredient
-ingredient_id
-recipe_id
recipe
-recipe_id
-name
I would be very happy about any ideas on how to solve that problem!
Thanks.

Your query will look something like this
Your query will look something like this
SELECT name, count(*)
FROM recipe
INNER JOIN recipe_ingredient
ON recipe.recipe_id = recipe_ingredient.recipe_id
GROUP BY name
HAVING count(*) > 1

IF looking for specific Ingredients, you could do a pre-query doing a union of all the ingredients you are interested in. Join that to the ingredients table per recipe and make sure that all ingredients are accounted for. This is handled by the group by and having count = the number of ingredients you are looking for.
I did this example based on the name of the ingredient. If you have/know the actual ingredient ID (which would be more accurate such as web-based and you have the IDs chosen by a user), just change the join condition to the ingredient ID instead of just the description of the ingredient.
SELECT
r.name,
r.recipe_id
from
( SELECT 'Milk' as Ingredient
UNION select 'Eggs'
UNION select 'Butter' ) as Wanted
JOIN recipe_ingredient ri
ON Wanted.Ingredient = ri.recipe_ingredient
JOIN Recipe r
ON ri.Recipe_id = r.id
group by
ri.recipe_id
having
COUNT(*) = 3
In this case, Milk, Butter, Eggs and a final count = 3.

In order to match all elements of an IN clause, you need to make sure you select only Recipes that have a count which matches the total number of Ingredients in your list:
SELECT name
FROM recipe
INNER JOIN recipe_ingredient
ON recipe.recipe_id = recipe_ingredient.recipe_id
WHERE recipe_ingredient.ingredient_id IN (ID1, ID2, ID3) --list of ingredient IDs
Group By Name
Having Count(*) = 3 --# of Ingredients you have chosen
Good luck finding which recipe will work with the ingredients you have available
Here is a functional example

Just use OR for your where.
Like this
$query = "SELECT name
FROM recipe
INNER JOIN recipe_ingredient
ON recipe.recipe_id = recipe_ingredient.recipe_id
WHERE recipe_ingredient.recipe_id = ? OR recipe_ingredient.recipe_id = ?";

Use group by and having clause
SELECT name
FROM recipe
INNER JOIN recipe_ingredient
ON recipe.recipe_id = recipe_ingredient.recipe_id
GROUP BY name
HAVING count(1) > 1

Related

Is there a more elegant way to query many to many tables on multiple ORs and ANDs?

I have tables dishes, ingredients and dishes_ingredients. I would like to show all dishes that have mango and chicken. I can achieve this by first getting all dishes containing mango and then selecting all dishes that (also) contain chicken within these results. Like this:
SELECT e_dishes.id, e_dishes.name FROM
(
SELECT DISTINCT dishes.id, dishes.name FROM dishes
JOIN dishes_ingredients ON dishes.id = dishes_ingredients.dishes_id
JOIN ingredients ON dishes_ingredients.ingredients_id = ingredients.id
WHERE ingredients.name = 'mango'
) AS e_dishes
JOIN dishes_ingredients ON e_dishes.id = dishes_ingredients.dishes_id
JOIN ingredients ON dishes_ingredients.ingredients_id = ingredients.id
WHERE ingredients.name = 'chicken';
But it does not seem elegant enough. I think that there must be a way to get all dishes containing mango and chicken in just one query.
You can use aggregation:
SELECT d.id, d.name
FROM dishes d JOIN
dishes_ingredients di
ON d.id = di.dishes_id JOIn
ingredients i
ON di.ingredients_id = i.id
WHERE i.name IN ('mango', 'chicken')
GROUP BY d.id, d.name
HAVING COUNT(*) = 2; -- number of ingredients
This assumes that the same ingredient is not listed twice. If so, use having count(distinct i.name) = 2.
Note the use of table aliases so the query is easier to write and to read.

select most matched rows at first in mysql

my project is running on laravel 5.4, mysql 5.7
I have 4 tables
recipes (id, name)
ingredient_recipe (id, recipe_id, ingredient_id, amount)
ingredients (id, name, cat_id)
ingredient_category (id, name)
recipes and ingredients have many to many relations through ingredient_recipe table. each recipe can have many ingredients. each ingredient has its category cat_id which references to id in ingredient_category table.
I need to select all recipes which ingredients' categories ids equals the requested values, and put the most matched values at top.
for example requested ingredient categories ids are [23,56,76,102,11].
lets say recipe foo have ingredients which categories matched 23,56,76, bar matched 23,56 and baz matched 23. they should be ordered - foo, bar, baz. how can I order them in this way?
here's my sql code
--select recipes
SELECT * from recipes where exists
--select recipe's ingredients
(select `ingredients`.`id`
from `ingredients`
inner join
`ingredient_recipe` on `ingredients`.`id` =
`ingredient_recipe`.`ingredient_id` where `recipes`.`id` =
`ingredient_recipe`.`recipe_id`
and exists
--select ingredient categories, where id ..
(select `ingredient_category`.`id`
from `ingredient_category`
where `ingredients`.`cat_id` = `ingredient_category`.`id`
and `id` IN (23,56,76,102,11)))
but this code doesn't 'put' mostly matched recipes at top. I know I can do select like in this example and then filter them, but is there way to do in in sql?
Join the recipe table to the ingredient table via the junction table ingredient_recipe, and then aggregate by recipe. For each recipe, we can count the number of ingredients which map your list, and we can order the result set with the higher matches appearing first.
SELECT
r.id,
r.name,
COUNT(CASE WHEN i.cat_id IN (23, 56, 76, 102, 11) THEN 1 END) AS match_cnt
FROM recipes r
INNER JOIN ingredient_recipe ir
ON r.id = ir.recipe_id
INNER JOIN ingredients i
ON ir.ingredient_id = i.id
GROUP BY r.id
ORDER BY match_cnt DESC;
We could also add a HAVING clause, e.g. to filter off recipes which do not meet a minimum number of matching ingredients. We could also a LIMIT clause to limit the total number of matches.
You can use GROUP BY and a left join to the categories table to get a count of the number of categories, then sort on that count.
SELECT
a.`id`,
a.`name`,
c.`id`.
c.`name`,
count(d.`id`) as `numcategories`,
GROUP_CONCAT(d.`name`)
FROM `recipes` a
JOIN `ingredient_recipe` b
ON a.`id` = b.`recipe_id`
JOIN `ingredients` c
ON b.`ingredient_id` = c.`id`
LEFT JOIN `ingredient_category` d
ON c.`cat_id` = d.`id`
GROUP BY a.`name`,c.`name`
ORDER BY count(d.`id`) DESC, a.`name`,c.`name`

MySQL SELECT query with a many to many relationship

I'm having trouble making a SELECT/WHERE query using a many to many relationship type. A user inputs ingredients, and I want to find which recipes use all the ingredients provided among the other ingredients (if any). (Think: use up the last ingredients I have in my fridge)
My DB is currently designed like this:
recipes_ingredients looks like this
For example, if I give,id_ingredient IN (22, 23) i want the recipe #16497 only, not #16631 (since it only has 22 and not 23).
I've come up with something that does the opposite of what I described
SELECT DISTINCT recipes.*
FROM recipes_ingredients
JOIN recipes ON recipes_ingredients.id_recipe = recipes.id
WHERE id_ingredient IN ( 96, 13196 )
If you want to get recipes which should have these both ingredients(not single ingredient) then you can use aggregation with some filter
SELECT r.*
FROM recipes_ingredients i
JOIN recipes r ON i.id_recipe = r.id
WHERE i.id_ingredient IN ( 96, 13196 )
GROUP BY r.id
HAVING COUNT(DISTINCT i.id_ingredient ) = 2
OR
SELECT r.*
FROM recipes_ingredients i
JOIN recipes r ON i.id_recipe = r.id
GROUP BY r.id
HAVING SUM(i.id_ingredient = 96)
AND SUM(i.id_ingredient = 13196)
Assuming that you need recipes that contains all ingredients you have on the input then you may use JOIN
SELECT recipes.* FROM recipes
JOIN recipes_ingredients r1 ON recipes.id = r1.id_recipe AND r1.id_ingredient = 96
JOIN recipes_ingredients r2 ON recipes.id = r2.id_recipe AND r2.id_ingredient = 13192
Unfortunately there is no intersect operator in mysql which would be more simple.
SELECT count(*) matches, id_recipe FROM `recipes_ingredients`
WHERE `id_ingredient` in ('23',...)
Group By `id_recipe`
WHERE matches = (
SELECT count(*) FROM ingredients where id in ('23',...)
);
This provides the count of matching ingredients per recipe, then compares counts to the exact number of parameters passed in. Or, since you are using phpmyadmin (and as such: PHP), you can pass in a count of the parameters (using PHP's count() if they start in an array, for example), and skip the subquery.
You can then join this list outwards to get any further information.

Select if another select returns rows

Im trying to make a select to make cooking recipe select within the items you have.
i have a table named ingredientsOwn with the following structure:
idType (int) amount (int)
Another table named recipes with this structure:
idRecipe (int) name (varchar)
And another table named recipeIngredients
idRecipe (int) idType (int) amount (int)
I would like to show the recipes you can do with the elements you have, how could i perform this?
Im trying to implement it in only one query cause i really dont know how to go throw and array on node js.
Thanks
The way I would go around this is, try to compute for each recipe, the number of ingredients you need, and join that with the number of ingredients own, and if the two numbers match, you have a candidate recipe.
So, to get the number of ingredients a recipe needs you'll have to do something like (this is more like a sql server syntax, so please try to focus in the concepts, and not the syntax):
select idRecipe, count(*) as neededIngredientsCount
from recipeIngredients
group by idRecipe
To get the number of available ingredients for each receipe, you have to join your ingredientsOwn with recipeIngredients, to be able to tell how many matching ingredients you have for each recipe.
select ingredientsOwn.idRecipe, count(*) as matchingIngredientsCount
from ingredientsOwn inner join recipeIngredients
on ingredientsOwn.idType = recipeIngredients.idType
where ingredientsOwn.amount >= recipeIngredients.amount
group by ingredientsOwn.idRecipe
Now you join the previous 2 queries to get the idRecieps that you have enough ingredients for, and join them with the recipes table to get the recipe name.
select r.idRecipe, r.name from
((select idRecipe, count(*) as neededIngredientsCount
from recipeIngredients
group by idRecipe) as in
inner join
(select ingredientsOwn.idRecipe, count(*) as matchingIngredientsCount
from ingredientsOwn inner join recipeIngredients
on ingredientsOwn.idType = recipeIngredients.idType
where ingredientsOwn.amount >= recipeIngredients.amount
group by ingredientsOwn.idRecipe) as io
on in.idRecipe = io.idRecipe
and in.neededIngredientsCount = io.matchingIngredientsCount
inner join
(select * from recipes) as r
on r.idRecipe = in.idRecipe)
Hope this helps, and sorry for not being able to provide valid mysql syntax.
SELECT * FROM recipes INNER JOIN (
select idRecipe from recipeIngredients
WHERE recipeIngredients.idType IN (
SELECT ingredientsOwn.idType from ingredientsOwn
)
) as a ON a.idRecipe = recipes.idRecipe

Incomprehensible query behaviour

I have multiple tables, related by multiple foreign keys as in the following example:
Recipes(id_recipe,name,calories,category) - id_recipe as PK.
Ingredients(id_ingredient,name,type) - id_ingredient as PK.
Contains(id_ingredient,id_recipe,quantity,unit) - (id_ingredient,id_recipe) as PK, and as Foreign Keys for Recipes(id_recipe) and Ingredients(id_ingredient).
You can see this relations represented in this image.
So basically Contains is a bridge between Recipes and Ingredients.
The query I try to write it's supposed to give as result the names of the recipes whose ingredients type are "bovine" but not "lactic".
My attempt:
SELECT DISTINCT Recipes.name
FROM Ingredients JOIN Contains USING(id_ingredient) JOIN Recipes USING (id_recipe)
WHERE Ingredients.type = "bovin"
AND Ingredients.type <> "lactic";
The problem is it still shows me recipes that have at least one lactic ingredient.
I would appreciate any help!
This is the general form of the kind of query you need:
SELECT *
FROM tableA
WHERE tableA.ID NOT IN (
SELECT table_ID
FROM ...
)
;
-- EXAMPLE BELOW --
The subquery gives the id values of all recipes that the "lactic" ingredient is used in, the outer query says "give me all the recipes not in that list".
SELECT DISTINCT Recipes.name
FROM Recipes
WHERE id_recipe IN (
SELECT DISTINCT id_recipe
FROM `Ingredients` AS `i`
INNER JOIN `Contains` AS `c` USING (id_ingredient)
WHERE `i`.`type` = "lactic"
)
;
Alternatively, using your original query:
You could've changed the second join to a LEFT JOIN, changed it's USING to an ON & included AND type = "lactic" there instead, and ended the query with HAVING Ingredients.type IS NULL (or WHERE, I just prefer HAVING for "final result" filtering). This would tell you which items could not be joined to the "lactic" ingredient.
A common solution of this type of question (checking conditions over a set of rows) utilizes aggregate + CASE.
SELECT R.Name
FROM Recipes R
INNER JOIN Contains C
on R.ID_Recipe = C.ID_Recipe
INNER JOIN Ingredients I
on C.ID_Ingredient = I.ID_Ingredient
GROUP BY R.name
having -- at least one 'lactic' ingredient
sum(case when type = 'lactic' then 1 else 0 end) = 0
and -- no 'bovin' ingredient
sum(case when type = 'bovin' then 1 else 0 end) > 0
It's easy to extend to any number of ingredients and any kind of question.
Hijacked the fiddle of xQbert
SELECT R.NAME
FROM CONTAINS C
INNER JOIN INGREDIENTS I
ON I.ID_INGREDIENTS = C.ID_INGREDIENTS AND I.TYPE = 'bovine' AND I.TYPE <> "lactic"
INNER JOIN RECIPES R
ON R.ID_RECIPE = C.ID_RECIPE
GROUP BY R.NAME
That should work, maybe you need to escape 'contains'. It could be recognized as a SQL function.
SQL Fiddle
In my example burgers and pasta have 'Bovin' and thus show up. So do cookies but cookies also have 'lactic' which is why they get excluded.
SELECT R.Name
FROM Recipes R
INNER JOIN Contains C
on R.ID_Recipe = C.ID_Recipe
INNER JOIN Ingredients I
on C.ID_Ingredient = I.ID_Ingredient
LEFT JOIN (SELECT R2.ID_Recipe
FROM Ingredients I2
INNER JOIN Contains C2
on C2.ID_Ingredient = I2.ID_Ingredient
INNER JOIN Recipes R2
on R2.ID_Recipe = C2.ID_Recipe
WHERE Type = 'lactic'
GROUP BY R2.ID_Recipe) T3
on T3.ID_Recipe = R.ID_Recipe
WHERE T3.ID_Recipe is null
and I.Type = 'Bovin'
GROUP BY R.name
There likely is a more elegant way of doing this. I really wanted to CTE this and join it to itself.. but no CTE in mySQL. Likely a way to do this using exists too.... I'm not a big fan of using IN clauses as the performance generally suffers. Exists fastest, Joins 2nd fastest, in slowest (generally speaking)
The inline view (sub query) returns the ID_recipe of those you don't want to include.
The outer query returns the Name of the recipes with ingredients you want.
By joining these two together using an outer join we return all recipes and only those with the undesired ingredient. We then limit the results to only those where the recipe ID doesn't exist for the undesired ingredient. (undesired ingredient not found) you'll get only those recipes having all desired ingredients.
You can use NOT EXISTS for this.
Try this:
SELECT DISTINCT Recipes.`name`
FROM Recipes JOIN Contains AS C1 USING (id_recipe) JOIN Ingredients USING(id_ingredient)
WHERE Ingredients.type = "bovin"
AND NOT EXISTS (
SELECT 1
FROM Contains AS C2 JOIN Ingredients USING(id_ingredient)
WHERE C1.id_recipe = C2.id_recipe
AND Ingredients.type = "lactic"
)