mysql add weights on query parameters - php

Ok, for this question I need idea how to make an algorithm for sorting results.
Let me explain a problem:
i have different dish recipes (more than 2000) consists of different ingredients.
having 3 tables:
dish
id | name
ingredient
id | name
dish_ingredient
id | id_dish | id_ingredient
Application i made let users select different ingredients they have and app shows them proper dish recipe sort by count of ingredients they have. Now I would like to have algorithm where users would select ingredients and add them some kind of "weight".
Example which works for now: if user select ingredients beef, carrots, onion, salt, pepper algorithm looks which recipes has these ingredients (not necessary all of them) and sort them by ingredients that user has. So first recipe from this sorting could have just salt and then maybe flour and eggs (recipe for pancakes) at the end of list there could be recipes with beef which are more convenient for user.
So my idea is that user could add some weights on his ingredients so search algorithm would give him more proper recipes. If you do not understand what i want, you could see application on www.mizicapogrnise.si (transalate it from Slovenian language to your language) to see how app works now.
Thanks for all your help.

The basic query to count the ingredients is:
select di.id_dish, count(*) as NumIngredients
from dish_ingredient di join
ingredient i
on i.id = di.id_ingredient
group by di.id_dish;
You can modify this for ingredients by introducing a case clause into the query:
select di.id_dish, count(*) as NumIngredients,
sum(case when i.name = 'beef' then 2.0
when i.name = 'salt' then 0.5
. . .
end) as SumWeights
from dish_ingredient di join
ingredient i
on i.id = di.id_ingredient
group by di.id_dish
order by SumWeights desc;
The . . . is not part of the SQL syntax. You have to fill in with similar lines where this occurs.
An alternative formulation is to put the information in a subquery. The subquery contains the weight for each ingredient. This is then joined to the dish_ingredient table to get the weight for each ingredient in each dish. The outer query aggregates the results by dish:
select di.id_dish, count(*) as NumIngredients,
sum(w.weight) as SumWeights
from dish_ingredient di join
ingredient i
on i.id = di.id_ingredient left outer join
(select 'beef' as name, 2.0 as weight union all
select 'salt', 0.5 union all
. . .
) w
on i.name = w.name
group by di.id_dish
order by SumWeights desc;
Both these method require modifying the query to get the necessary weight information into the database.

Related

How to filter MySQL query to omit specific matches

I do apologize for any description issues as this is my first post. I am attempting to write a query that will "return all salespersons who did NOT sell to company Red" for a specific set of tables. I will list the tables below (as well as my currently written query) but there is one salesperson who meets both criteria (sold AND not sold). I want to see how to omit them from the query results but am not finding a way.
Tables
My query so far:
SELECT Salesperson.ID, Salesperson.Name
FROM Salesperson, Customer, Order
WHERE Salesperson.ID = Order.Sales_ID
AND Order.Cust_ID = Customer.ID
AND Order.Cust_ID <> (
SELECT Customer.ID
FROM Customer
WHERE Name = 'Red'
);
The returned results:
ID Name
4 Pam
5 Alex
I am trying to make sure Pam does not return as you see she has two items in the Orders table, one with company Red and the other with company Yellow. I assume my query is returning her due to her order with company Yellow. Any help or advice would be appreciated.
First, learn how to use proper join syntax. One method uses group by and having:
SELECT s.ID, s.Name
FROM Salesperson s JOIN
Orders o
ON s.ID = o.Sales_ID JOIN
Customer c
ON o.Cust_ID = c.ID
GROUP BY s.ID, s.Name
HAVING SUM(CASE WHEN c.name = 'Red' THEN 1 ELSE 0 END) = 0;

Search for all combinations of values in a column from a defined subset

I have a table with the following schema in MySQL
Recipe_Quantity_ID,Recipe_ID, Recipe_Quantity, Ingredients_ID, Ingredient_Measurement_ID
The sample data can be found in this SQL Fiddle http://sqlfiddle.com/#!2/d05fe .
I want to search the table for the given (one or many) Ingredients_ID and return the Recipe_ID that has this Ingredients_ID
I do this by using this SQL
select Recipe_ID
from recipe_quantity
group by Recipe_ID
having count(*) = {$ar_rows}
and sum(Ingredients_ID in ({$ids})) = {$ar_rows}
which may translate to
select Recipe_ID
from recipe_quantity
group by Recipe_ID
having count(*) = 4
and sum(Ingredients_ID in (8,5,45,88)) = 4
For searching for less Ingredients_ID I substract the last ID until I reach one Ingredient ID. By using this technique of course is not possible to search for all the combinations. Eg 8,5,45,85 | 8,45,85 | 45,85 | 5,45,85 etc.
Any ideas how I can search for all the combinations that may be true? Thanks in advance.
My understanding is that you want to get all recipes where you already have all the ingredients you need. you don't need to use all the ingredients you have but you don't want to have to go shopping.
Correct me if I am wrong but I don't think there is a recipe that fits your ingredients list so I have used other ingredients. note that ingredients 13,36 wont be used.
you should be able to put another select statement in the brackets that gets the ingredients that you have (select ingredients_id from ingredients_owned) it isn't good to specify them each time.
select distinct c.Recipe_id
from
(select Recipe_ID
from recipe_quantity
where Ingredients_ID in (5,6,1,11,8,12,13,36, 81,82,62,73,35)) c
left join (select Recipe_ID
from recipe_quantity
where Ingredients_ID not in (5,6,1,11,8,12,13,36, 81,82,62,73,35)) x
on c.Recipe_id = x.Recipe_id
where x.Recipe_id is null
How about something like this?
select Recipe_ID, group_concat(Ingredients_ID), count(*) as ingredients
from recipe_quantity
where Ingredients_ID IN (8,5,45,88)
group by Recipe_ID
having ingredients > 0
order by ingredients desc
Instead of grouping all recipe ingredients and then filtering out the ones that don't include the ingredients you're looking for, I match only the entries in recipe_quantity that match the ingredients in the first place. I use a group_concat so you can see the set of ingredients that match.
This orders by the number of ingredients that match, but still preserves partial matches on one or more ingredients. You change the 0 to the minimum number of ingredients to match.

Query to match rows in a table against a list of numbers, order by the amount of rows matched, and match against tags and do the same?

I have a database of recipes. In this database I have 5 tables:
- recipes
- ingredients
- ingredients_available
- tags
- tags_available
With the following structure:
- recipes
id, name
- ingredients
id, recipe_id, ingredient_id
- ingredients_available
id, name
- tags
id, recipe_id, tag_id
- tags_available
id, name
I want to query the database with a set of ingredients (which would be there i.d numbers), so for example 5, 2, 3 and find recipes that match this numbers partially or impartially. I then want to order recipes by the amount of ingredients matched. I also want to know what other ingredients are needed to complete the recipe and there corresponding ingredient_id's. I also wish to an infinite amount of tags in to the query, and so only find ingredient_id's which match the tags and again order by the amount of tags matched.
How would I go about creating such a query, can also this be done with one query? I am using a MySQL database. Any pointers in the right direction would be great. Thank you.
I think the power of 'joins' come in here.
I'm not sure what possible combinations you want to produce but here are some scenarios I think of with the structure you're presenting here:
//Get ingredients for recipes
Select ingredients.* from recipe recipe
Inner Join ingredients ingredients on recipe.id = ingredients.recipe_id
order by ingredients.id
//Get available ingredients for recipes
Select ingredients.* from recipe recipe
Inner Join ingredients ingredients on recipe.id = ingredients.recipe_id
Inner Join ingredients_available ingredients_available on ingredients.ingredient_id =
ingredients_available.id
order by ingredients.id
Here is a link to the 'join' concept of mysql to solve your questions:
http://dev.mysql.com/doc/refman/5.0/en/join.html
Cheers!
I managed to do the first part of my problem with the following query:
SELECT recipes.*, COUNT(ingredients.id) AS occurences FROM recipes INNER JOIN ingredients ON recipes.id = ingredients.recipe_id WHERE ingredient_id IN (1, 3) GROUP BY recipes.id ORDER BY occurences DESC;

mysql select and join on multiple tables

I'm having tables like
product table: product_id | code
group table: id | fk-product_id | id_code
grade table id | fk-product_id | id_grade
person table id | fk-product_id | id_person
my sql query is:
"SELECT *
FROM product
JOIN group ON group.product_id = product_id
JOIN grade ON grade.product_id = product_id
JOIN person ON person.product_id = product_id
WHERE product.code='".$productCode."'");
I get the wright result, but there is too much of rows. I thing that I'm doing overkill.
All product are for sure in the table "product" but it's not necessary that the same "id_product" is in the table "group", "grade" or "person".
In my result are a lot of rows where my result is repeted. I there any way to avoid those duplication?
Is there better way to perform my query?
From your original query, you have listed the column in the group, grade and person table are
'fk-product_id' but your query is showing as just 'product_id'. So, I am implying your real column is just 'product_id' and the 'fk-' was just a reference that it was the foreign key to products table.
Now, that said, the equality comparison is just product_id. Since you are not qualifying it with alias.field, it is probably grabbing everything since each record in group will always have its own product_id = its own product_id.
In addition, you mention that not all tables will have a matching product ID, so you will need LEFT-JOINs for the other tables... Adjust to something like this
SELECT
p.*,
gp.id_code,
gd.id_grade,
per.id_person
FROM
product p
LEFT JOIN group gp
ON p.product_id = gp.product_id
LEFT JOIN grade gd
ON p.product_id = gd.product_id
LEFT JOIN person per
ON p.product_id = per.product_id
WHERE
p.code='".$productCode."'";
But I would head caution for sql-injection as you could get malicious values in your $productCode variable. Make sure you have it properly cleaned and escaped.
#5er, Left-Join says for each record on the left-side (first in this case is the Product Table), I want all records... and oh... by the way... I have the other tables (group, grade and persons). They MAY have a record too that has the same Product_ID value as the product table. If so, grab those pieces too, but don't exclude the original product record.
Now, why your query was failing, and I thought I described it well, but apparently not. You were getting a Cartesian result which means for every one record in the left-table (product), you were getting EVERY record in the RIGHT-side table... So, for a single product, and if you had 20 group, 10 grades and 100 people, you were basically getting 20,000 records.
So your JOIN
JOIN group ON group.product_id = product_id
WOULD have worked, but had less records IF you qualified with the PRODUCT reference
JOIN group ON group.product_id = PRODUCT.product_id
Otherwise, it was just comparing its own product_ID to itself and saying... Yup, these to product IDs match (even though it was the same record), it returned it. The engine can't guess for you which table you meant for the second part of the join, especially when there were a total of 4 tables referenced in the query, and EACH had a "Product_ID" column. So, I strongly suggest that for ALL your queries, qualify ALL fields as alias.field, including those of the select field list. Then, anyone else trying to help you in the future, or even take over where you left-off know where the fields are. Prevent ambiguity in your queries.
Your select does not match the table/column names above it. For example in table product you say you have column id_product, but in select you use product.id instead of product.id_product. That might be totally different column.
Your results are repeating because the JOIN is joining tables, but you are not filtering those cases where one JOIN matches, while the other isn't.
Try this:
SELECT *
FROM product
JOIN group ON group.product_id = product.id
JOIN grade ON grade.product_id = product.id
JOIN person ON person.product_id = product.id
WHERE product.code='".$productCode."'
GROUP BY product.id

Checking that a recipe contains an ingredient - MYSQL

Hey everyone. I'm having a bit of trouble running a query / php combination efficiently. I seem to be just looping over too many result sets in inner loops in my php. I'm sure there is a more efficient way of doing this. Any help very much appreciated.
I've got a table that holds 3500 recipes ([recipe]):
rid | recipe_name
And another table that holds 600 different ingredients ([ingredients])
iid | i_name
Each recipe has x number of ingredients associated to it, and I use a nice joining table to create the association ([recipe_ingredients])
uid | rid | iid
(where uid is just a unique id for the table)
For example:
rid: 1 | recipe_name: Lemon Tart
.....
iid: 99 | i_name: lemon curd
iid: 154 | i_name: flour
.....
1 | 1 | 99
2 | 1 | 154
The query I'm trying to run, allows the user to enter what ingredients they have, and it will tell you anything you can make with those ingredients. It doesn;t have to use all ingredients, but you do need to have all the ingredients for the recipe.
For instance if I had flour, egg, salt, milk and lemon curd I could make 'Pancakes', and 'Lemon Tart' (if we assume lemon tart has no other ingredients:)), but couldn't make 'Risotto' (as I didnt have any rice, or anything else thats needed in it).
In my PHP I have an array containing all the ingredients the user has. At the moment they way I'm running this is going through every recipe (loop 1) and then checking all ingredients in that recipe to see if each ingredient is contained in my ingredients array (loop 2). As soon as it finds an ingredient in the recipe, that isnt in my array, it says "no" and goes onto the next recipe. If it does, it stores the rid in a new array, that I use later to display the results.
But if we look at the efficiency of that, if I assume 3500 recipes, and Ive got 40 ingredients in my array, the worst case scenario is it running through 3500 x 40n, where n = number of ingredients in the recipe. The best case is still 3500 x 40 (doesn't find an ingredient first time for every recipe so exits).
I think my whole approach to this is wrong, and I think there must be some clever sql that I'm missing here. Any thoughts? I can always build up an sql statement from the ingredient array I have ......
Thanks a lot in advance, much appreciated
I'd suggest storing the count of the number of ingredients for the recipe in the recipe table, just for efficiency's sake (it will make the query quicker if it doesn't have to calculate this information every time). This is denormalization, which is bad for data integrity but good for performance. You should be aware that this can cause data inconsistencies if recipes are updated and you are not careful to make sure the number is updated in every relevant place. I've assumed you've done this with the new column set as ing_count in the recipe table.
Make sure you escape the values in for NAME1, NAME2, etc if they are provided via user input - otherwise you are at risk for SQL injection.
select recipe.rid, recipe.recipe_name, recipe.ing_count, count(ri) as ing_match_count
from recipe_ingredients ri
inner join (select iid from ingredients where i.name='NAME1' or i.name='NAME2' or i.NAME='NAME3') ing
on ri.iid = ing.iid
inner join recipe
on recipe.rid = ri.rid
group by recipe.rid, recipe.recipe_name, recipe.ing_count
having ing_match_count = recipe.ing_count
If you don't want to store the recipe count, you could do something like this:
select recipe.rid, recipe.recipe_name, count(*) as ing_count, count(ing.iid) as ing_match_count
from recipe_ingredients ri
inner join (select iid from ingredients where i.name='NAME1' or i.name='NAME2' or i.NAME='NAME3') ing
on ri.iid = ing.iid
right outer join recipe
on recipe.rid = ri.rid
group by recipe.rid, recipe.recipe_name
having ing_match_count = ing_count
You could an "IN ANY" type query:
select recipes.rid, count(recipe_ingredients.iid) as cnt
from recipes
left join recipe_ingredients on recipes.rid = recipe_ingredients.rid
where recipes_ingredients in any (the,list,of,ingredients,the,user,hash)
group by recipes.rid
having cnt > some_threshold_amount
order by cnt desc
Doing this off the top of my head, but basically pull out any recipes where at least one of the user-provided ingredients are listed, sort by the total ingredient count, and then only return the recipes where more than a threshold amount of ingredients are present.
I've probably got the threshold bit wrong - sneaky suspicion it'll count the recipes's ingredients, and not the user-provided ones, but the rest of the query should be a good start for what you need.
Question: why isn't your query directly sql?
You can optimize by eliminating the wrong recipes:
firstly eliminate the recipes that have more ingridients than you user ingredients
make a recursive greedy by:
pick the first rid|iid
if it's in the user ingredients, continue,
if not, eliminate from the Recipe_Ingredients table all the rows with rid => new_table
restart using the new_table | stop new_table count = 0
It should have the best statistical results.
Hope it helped
Something like this:
SELECT r.*, COUNT(ri.iid) AS count FROM recipe r
INNER JOIN recipe_ingredient ri ON r.rid = ri.rid
INNER JOIN ingredient i ON i.iid = ri.iid
WHERE i.name IN ('milk', 'flour')
GROUP BY r.rid
HAVING count = 2
It's pretty easy to understand. count hold the number of ingredients within the list (milk, flour) that were matched for each recipe. If count matches the number of ingredients in the WHERE clause (in this case: 2), then return the recipe.
SELECT irl.ingredient_amount, r . * , i.thumbnail
FROM recipes r
LEFT JOIN recipe_images i ON ( i.recipe_id = r.recipe_id )
LEFT JOIN ingredients_recipes_link irl ON ( irl.recipe_id = r.recipe_id )
WHERE irl.recipe_id
IN (
SELECT recipe_id
FROM `ingredients_recipes_link`
WHERE ingredient_id
IN ( 24, 21, 22 )
HAVING count( * ) =3
)
GROUP BY r.recipe_id

Categories