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"
)
Related
I have 2 tables: one for the restaurants containing general information, and the other - for it's description translations. The second table has such columns:
restaurant_id, description, language
The client application makes request for the list of restaurants, and passes the language it wants the result for. So now I need to JOIN those 2 tables. Everything would be easy, if each restaurant had description in each of available languages, but it does not. Some restaurants are missing descriptions in some languages.
So in case if the description is missing in selected language, I should return a description in default language (let it be eng for example).
So the question is how can I JOIN the tables the way that if the description in selected language is available - join with it, otherwise - join with eng?
What you can do is left join onto your translations table with the chosen language and also inner join onto translations again with the default language.
Then you can use COALESCE to chose the first, non-null value. For example
SELECT r.name, COALESCE(t.description, t_default.description) as description
FROM restaurant r
LEFT JOIN translation t ON r.id = t.restaurant_id
AND t.lang = 'fr' -- the chosen language
INNER JOIN translation t_default ON r.id = t_default.restaurant_id
AND t_default.lang = 'en' -- the default language
Demo ~ http://sqlfiddle.com/#!9/aa68c0/1
I've used an INNER JOIN for the default language to ensure there's a value to display. If you're fine with a possible NULL result, change it to a LEFT JOIN.
One more solution is to use temporary table to select IDs of the restaurants which have translation, and then using UNION to combine the restaurants with translation + all default restaurants excluding restaurant IDs having translations:
Create temporary table with IDs:
CREATE TEMPORARY TABLE IF NOT EXISTS existing_ids AS (SELECT r.id FROM restaurant r JOIN translation t ON r.id = t.restaurant_id WHERE t.lang = 'fr');
Select rows having translation:
SELECT r.name, t.description FROM restaurant r JOIN translation t ON r.id = t.restaurant_id WHERE t.lang = 'fr'
Union with rows with default translation excluding IDs from the temporary table:
UNION
SELECT r.name, t.description FROM restaurant r JOIN translation t ON r.id = t.restaurant_id WHERE t.lang = 'en' AND id not in (SELECT id from existing_ids)
Demo:
http://sqlfiddle.com/#!9/d3e66dc/1
How to select resource with where condition enforcing having two relations in joined table.
For example lets say I have to tables resource and item where one resource can have many items and item can be assigned to many resources.
Now I need to select a resource which have two specific items ? how to do it in the simplest way possible ?
SELECT
r.name,
GROUP_CONCAT(DISTINCT CONCAT(i.name)) AS itemNames
FROM
resources r
LEFT JOIN resources_items ri ON ri.resourceId = r.id
LEFT JOIN items i ON i.id = ri.itemId
WHERE i.id= '1' AND i.id = '2'
GROUP BY r.id
Is this a good direction ?
Later on I'd like to select many resources based on many items. For example all resources that have id in given array or have an item with id 1 or 2
Another approach would be using IN() and COUNT()
SELECT
r.name,
GROUP_CONCAT(DISTINCT CONCAT(i.name)) AS itemNames
FROM
resources r
LEFT JOIN resources_items ri ON ri.resourceId = r.id
LEFT JOIN items i ON i.id = ri.itemId
WHERE i.id IN(1,2)
GROUP BY r.id
HAVING COUNT(DISTINCT i.id) = 2
For more element you just need to update your IN and the count need to be equal to the length of your given array
This approach is mysql specific which will use sum() to make the each filter is true like
SELECT
r.name,
GROUP_CONCAT(DISTINCT CONCAT(i.name)) AS itemNames
FROM
resources r
LEFT JOIN resources_items ri ON ri.resourceId = r.id
LEFT JOIN items i ON i.id = ri.itemId
WHERE i.id IN(1,2)
GROUP BY r.id
HAVING SUM(i.id = 1) > 1
AND SUM(i.id = 2) > 1
And repeat this SUM(i.id = #itemInput) clause according to your input items so as compare to first approach this is also a complex one like your which will involve no. of conditions or no. of joins. while in first approach you just need to count your inputs and match in having clause
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 newbie here.
So we have 3 tables:
categories(cat_id,name);
products(prod_id,name);
relationships(prod_id,cat_id);
It is a one-to-many relationship.
So, given a category name say "Books". How do I find all the products that come under books?
As an example,
categories(1,Books);
categories(2,Phones);
products(302,Sherlock Holmes);
relationships(302,1);
You need to JOIN the three tables.
SELECT p.*
FROM relationships r
INNER JOIN products p
ON p.prod_id = r.prod_id
INNER JOIN categories c
ON c.cat_d = r.cat_id
WHERE c.name = 'Books'
You have to join tables on related columns and specify WHERE clause to select all records where category name = 'Books'
SELECT p.*
FROM categories c
JOIN relationships r ON c.cat_id = r.cat_id
JOIN products p ON r.prod_id = p.prod_id
WHERE c.name = 'Books' -- or specify parameter like #Books
In SQL you often join related tables and beginners tend to join, whatever the situation. I would not recommend this. In your case you want to select products. If you only want to show products data, select from products only. You want to select products that are in the category 'Books' (or for which exists an entry in category 'Books'). Hence use an IN or EXISTS clause in order to find them:
select * from products
where prod_id in
(
select prod_id
from relationships
where cat_id = (select cat_id from categories where name = 'Books')
);
Thus you get a well structured query that tells the reader easily how the tables are related and what data you are actually interested in. Later, with different tables and data to select, this may keep you from duplicate result rows that you must get rid of by using DISTINCT or from getting wrong aggregates (sums, counts, etc.), because of mistakenly considering records multifold.
try this:
select p.Prod_id,p.name
from products p inner join relationships r on
p.prod_id = r.prod_id
where r.cat_id = (select cat_id from categories where name = 'books')
or
select p.Prod_id,p.name
from products p inner join relationships r on
p.prod_id = r.prod_id inner join categories c on c.cat_id = r.cat_id
where c.name = 'books'
So basically I'm joining 3 tables together. The main table is recipe, then it goes to ingredients list then ingredient.
So I need to have a query which has only recipes which contain NO chicken. The problem I am having is that because recipes have many ingredients when I use where != that just removes the ingredients with that meat but leaves the others.....how can i account for the multiple ingredients.
select Recipe.name as "No chicken" from Recipe inner join IngredientList on Recipe.recipeId=IngredientList.recipeId inner join Ingredients on IngredientList.IngredientId=Ingredients.ingredientId where type!="chcicken" group by Recipe.name;
Your original statement has a GROUP BY with no aggregate function. That doesn't make sense. It should be an ORDER BY if you're trying to sort.
Try something like this:
SELECT `Recipe`.`name` AS "No chicken"
FROM `Recipe`
WHERE `Recipe`.`RecipeId` NOT IN (
SELECT DISTINCT `IngredientList`.`RecipeId` AS `RecipeID`
FROM `IngredientList`
INNER JOIN `Ingredients` ON `IngredientList`.`IngredientId` = `Ingredients`.`IngredientId`
WHERE `Ingredients`.`Type` = 'chicken'
)
ORDER BY `Recipe`.`name`
Depending on your schema, you may need to use SELECT DISTINCT in the main select statement if you're getting duplicate recipe names.
The above have some typos, but Amirshk has a logically correct answer.
However, I recommend one avoid the IN() and NOT IN() clauses in MySQL as they are very, very slow on a set of tables as big as a large recipe database would get. IN and NOT IN can be re-written as joins to cut the runtime to 1/100th the time in MySQL 5.0. Even with MySQL 5.5's great improvements, the equivalent JOIN query benchmarks 1/5th the time on large tables.
Here is the revised query:
SELECT
Recipe.name AS "No Chicken"
FROM Recipe LEFT JOIN
(
SELECT IngredientList.recipeId, Ingredients.ingredientId
FROM IngredientList JOIN Ingredients USING (IngredientId)
WHERE Ingredients.type = 'chicken'
) WithChicken
ON Recipe.recipeId = WithChicken.recipeId
WHERE WithChicken.recipeId IS NULL;
This is pretty obtuse, so here is simplified SQL that provides the key concept of the NOT IN(...) equivalent exclusion join:
SELECT whatever FROM x
WHERE x.id NOT IN (
SELECT id FROM y
};
becomes
SELECT whatever FROM x
LEFT JOIN y ON x.id = y.id
WHERE y.id IS NULL;
Use an inner query to filter recipes with chicken, then select all the recipes without them.
As so:
select
Recipe.name as "No chicken"
from Recipe
inner join IngredientList on Recipe.recipeId=IngredientList.recipeId
inner join Ingredients on IngredientList.IngredientId=Ingredients.ingredientId
where Recipe.recipeId NOT IN (
select
Recipe.recipeId
from Recipe
inner join IngredientList on Recipe.recipeId=IngredientList.recipeId
inner join Ingredients on IngredientList.IngredientId=Ingredients.ingredientId
type ="chcicken" group by Recipe.recipeId)