select most matched rows at first in mysql - 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`

Related

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.

SQL -- many-to-many query

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

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

MySQL Group BY, Having, Order By Query

Ok, so assume I have the following tables:
recipes
id (pk)
name
added
modified
recipe_versions
id (pk)
recipe_id (fk to recipes.id)
version
content
added
What I want is a query that grabs the latest recipe_versions.added data and then joins with the base recipe data. Then sorts all results by recipes.added ASC I have the following, but the group by, is not selecting the latest recipe_versions row, seems to be selecting the first.
SELECT r.`id`,
r.name,
rv.version,
rv.content,
r.added,
r.modified,
FROM recipes r,
recipe_versions rv
WHERE r.`id` = rv.recipe
GROUP BY rv.recipe
HAVING max(rv.added)
ORDER BY r.added ASC
Use this solution:
SELECT
c.*, b.*
FROM
(
SELECT recipe_id, MAX(added) AS mostrecent
FROM recipe_versions
GROUP BY recipe_id
) a
INNER JOIN
recipe_versions b ON
a.recipe_id = b.recipe_id AND
a.mostrecent = b.added
INNER JOIN
recipes c ON a.recipe_id = c.id
ORDER BY
c.added

MySQL Join Query

I need to query the database by joining two tables. Here is what I have:
Table Town:
id
name
region
Table Supplier:
id
name
town_id
I currently have the following query which outputs all the Towns that belong to a given region:
SELECT id, name FROM Town WHERE region = 'North West';
Now I need to extend this query and create two further queries as follows:
Output the number of Suppliers for each Town
Output only the Towns that have 1 or more Supplier
I am using PHP for my scripts if that helps. I know I may be able to to get this data using PHP but in terms of performance it will probably be better if it is done in MySQL.
EDIT (27/07/10):
I now needs to extend this one last time - there is another table called Supplier_vehicles:
id
supplier_id
vehicle_id
A Supplier can have many Supplier_vehicles. The count (NumSupplier in this case) needs to now contain the total number of suppliers in a given town that have any of the given vehicle_id (IN condition):
SELECT * FROM Supplier s, Supplier_vehicles v WHERE s.id = v.supplier_id AND v.vehicle_id IN (1, 4, 6)
Need to integrate the above query into the existing JOIN query.
Count the number of suppliers.
SELECT t.id, t.name, count(s.id) as NumSupplier
FROM Town t
LEFT OUTER JOIN Suppliers s ON s.town_id = t.id
GROUP BY t.id, t.name
Only towns that have at least one supplier
SELECT DISTINCT t.id, t.name
FROM Town t
INNER JOIN Suppliers s ON s.town_id = t.id
And you are 100% correct, the best place for this is an SQL query.
SELECT t.id, t.name, count(s.id) as NumSupplier
FROM Town t
LEFT JOIN Suppliers s
[WHERE NumSupplier > 1]
GROUP BY t.id