MySQL foreign key and xref table - mysql

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

Related

Grouping results after join and having clause

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

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.

MySQL - join multiple mapped tables and count records with different mapping conditions

It's the 3rd day I'm trying to write a MySQL query. Did lots of search, but it still doesn't work as expected. I'll try to simplify tables as much as possible
System has tkr_restaurants table:
restaurant_id | restaurant_name
1 | AA
2 | BB
3 | CC
Each restaurant has a division assigned (tkr_divisions table):
division_id | restaurant_id | division_name
1 | 1 | AA-1
2 | 1 | AA-2
3 | 2 | BB-1
Then there are meals in tkr_meals_to_restaurants_divisions table, where each meal can be assigned (mapped) to whole restaurant(s) and/or specific division(s). If meal is mapped to restaurant, all restaurant's divisions should see it. If meal is mapped to division(s), only specific division(s) should see it.
meal_id | mapped_restaurant_id | mapped_division_id
1 | 1 | NULL
2 | NULL | 1
3 | NULL | 2
I need to display a list of restaurants and number of meals mapped to it depending on user permissions.
Example 1: if user has permissions to access whole restaurant_id 1 and restaurant_3 (and no specific divisions), then list should be:
AA | 3
CC | 0
(because user can access meals mapped to restaurant 1 + all its division, and restaurant 3 + all its divisions (even if restaurant 3 has no divisions/meals mapped))
Example 2: if user has permissions to access only division_id 1, then list should be:
AA | 1
(because user can only access meals mapped to division 1).
The closest query I could get is:
Example 1:
SELECT *,
(SELECT COUNT(DISTINCT meal_id)
FROM
tkr_meals_to_restaurants_divisions
WHERE
tkr_meals_to_restaurants_divisions.mapped_restaurant_id=tkr_restaurants.restaurant_id
OR tkr_meals_to_restaurants_divisions.mapped_division_id=tkr_divisions.division_id)AS total_meals
FROM
tkr_restaurants
LEFT JOIN
tkr_divisions
ON tkr_restaurants.restaurant_id=tkr_divisions.restaurant_id
WHERE
tkr_restaurants.restaurant_id IN (1, 3)
OR tkr_restaurants.restaurant_id IN (
SELECT restaurant_id
FROM tkr_divisions
WHERE division_id IN (NULL)
)
GROUP BY
tkr_restaurants.restaurant_id
ORDER BY
tkr_restaurants.restaurant_name
However, result was:
AA | 2
CC | 0
I believe I'm greatly over-complicating this query, but all the simpler queries I wrote produced even more inaccurate results.
What about this query:
SELECT
FROM tkr_restaurants AS a
JOIN tkr_divisions AS b
ON a.restaurant_id = b.restaurant_id
LEFT OUTER JOIN tkr_meals_to_restaurants_divisions AS c
ON (c.mapped_restaurant_id = a.restaurant_id OR c.mapped_division_id = b.division_id)
As a Base four your further work. It combine all information into one table. If you add e.g. this:
WHERE a.restaurant_id IN (1, 3)
the result will be
| restaurant_id | restaurant_name | division_id | restaurant_id | division_name | meal_id | mapped_restaurant_id | mapped_division_id |
|---------------|-----------------|-------------|---------------|---------------|---------|----------------------|--------------------|
| 1 | AA | 1 | 1 | AA-1 | 1 | 1 | (null) |
| 1 | AA | 2 | 1 | AA-2 | 1 | 1 | (null) |
| 1 | AA | 1 | 1 | AA-1 | 2 | (null) | 1 |
| 1 | AA | 2 | 1 | AA-2 | 3 | (null) | 2 |
just count the distinct meal ids with COUNT(DISTINCT c.meal_id) and take the restaurant name to get AA: 3 for your example 2
I used a sqlfiddle: http://sqlfiddle.com/#!9/fa2b78/18/0
[EDIT]
Change JOIN tkr_divisions AS b to LEFT OUTER JOIN tkr_divisions AS b
Change SELECT * to SELECT a.restaurant_name, COUNT(DISTINCT c.meal_id)
Add a GROUP BY a.restaurant_name at the end.
Update the SQL Fiddle (new link)

MySQL/MariaDB GROUP BY, ORDER BY returns same result twice

Assume I have the following table
+----+--------+--------+
| id | result | person |
+----+--------+--------+
| 1 | 1 | 1 |
| 2 | 2 | 2 |
| 3 | 2 | 2 |
| 4 | 4 | 3 |
| 5 | 4 | 1 |
| 6 | 1 | 2 |
+----+--------+--------+
Now I want to get the best result by each person ordered high to low, where best result means highest value of the result-column, so basically I want to GROUP BY person and ORDER BY result. Also if a person has the same result more than one time, I only want to return want one of those results. So the return I want is this:
+----+--------+--------+
| id | result | person |
+----+--------+--------+
| 4 | 4 | 3 |
| 5 | 4 | 1 |
| 2 | 2 | 2 |
+----+--------+--------+
The following query almost gets me there:
SELECT id, groupbytest.result, groupbytest.person
FROM groupbytest
JOIN (
SELECT MAX(result) as res, person
FROM groupbytest
GROUP BY person
) AS tmp
ON groupbytest.result = tmp.res
AND groupbytest.person = tmp.person
ORDER BY groupbytest.result DESC;
but returns two rows for the same person, if this person has made the same best result twice, so what I get back is
+----+--------+--------+
| id | result | person |
+----+--------+--------+
| 4 | 4 | 3 |
| 5 | 4 | 1 |
| 2 | 2 | 2 |
| 3 | 2 | 2 |
+----+--------+--------+
If two results for the same person are similar, only the one with lowest id should be returned, so instead of returning rows with ids 2 and 3, only row with id 2 should be returned.
Any ideas how to implement this?
Try this:
SELECT ttable.* from ttable
inner join
(
SELECT max(ttable.id) as maxid FROM `ttable`
inner join (SELECT max(`result`) as res, `person` FROM `ttable` group by person) t
on
ttable.result = t.res
and
ttable.person = t.person
group by ttable.person ) tt
on
ttable.id = tt.maxid
Check if tmp results in the correct resulting table. I think tmp should group correctly. The join adds new rows, because you have different values of "id".
Hence the rows with different id's will be treatet as different rows, no matter if the other columns are equal. You do not have duplicate results as long as there is no duplicate id. Try to remove the id from the SELECT. Then you should have the result you wanted, but without the id.
Example: Imagine Rooms with your id's from above. Let result be the amount of tables in the room and person the amount of people. Just because you have randomly the same amount of tables and people in room 2 and 3, it doesn't mean, that this are the same rooms.

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