SQL to search by associative items - php

I have the below two tables and I need to be able to search by items to find the shopping_list_id. Also, I want to limit the query so that it doesn't bring back other shopping lists with additional items on it. Essentially, I'm checking to see if this is a shopping list the user has saved before. The below query does NOT handle if there are shopping lists that match but with additional items, I'm stumped as to how to do that.
tables:
shopping_list
shopping_list_id
user
shopping_list_name
shopping_list_item
shopping_list_item_id
shopping_list_id
category_id
qty
qty_unit_id
This example has three items, but there could be any number. My PHP code dynamically generates the SQL joins and where clause based on the user's input.
Query that I have:
SELECT DISTINCT sli.shopping_list_id
FROM shopping_list_item sli
JOIN shopping_list sl ON sli.shopping_list_id=sl.shopping_list_id
JOIN shopping_list_item sli0 on sli.shopping_list_id=sli0.shopping_list_id
JOIN shopping_list_item sli1 on sli.shopping_list_id=sli1.shopping_list_id
JOIN shopping_list_item sli2 on sli.shopping_list_id=sli2.shopping_list_id
WHERE sl.user_id=:webuser_id
AND sli0.category_id=3 AND sli0.qty=1 AND sli0.qty_unit_id=3
AND sli1.category_id=683 AND sli1.qty=1 AND sli1.qty_unit_id=3
AND sli2.category_id=309 AND sli2.qty=1 AND sli2.qty_unit_id=7

You can do this pretty easily with the group by/having approach to this type of query:
select sli.shopping_list_id
from shopping_list_item sli
group by sli.shopping_list_id
having sum(sli.category_id = 3 AND sli.qty = 1 AND sli.qty_unit_id) = 1 and
sum(sli.category_id = 683 AND sli.qty = 1 AND sli.qty_unit_id = 3) = 1 and
sum(sli.category_id = 309 AND sli.qty = 1 AND sli.qty_unit_id = 7) = 1 and
count(*) = 3;

Related

How can I make this query run faster

I am running this query on my website in order to find a ToDo list based on specific criteria. But it runs too slow and it is probably possible to write it in another way.
SELECT * FROM lesson WHERE
id IN
(SELECT `lesson_id` FROM `localization_logging`
WHERE `language_id` = 2 AND `action_id` = 1)
AND `id` NOT IN
(SELECT `lesson_id` FROM `localization_logging`
WHERE `language_id` = 2 AND `part_id` = 1 AND `action_id` = 6)
What the query does is that it looks in the lesson table to find all lesson list names and then checks if a specific task is done. If the task is done in one todo than show it in the next. Action 1 is done but not action 6 in this case.
I hope I'm explaining this good enough. On my local machine the query takes 1.8 seconds, and sometimes I have to print multiple lists next to each others and then it takes 1.8 times the lists which makes the page load super slow.
Something like this for mark id as completed:
SELECT l.*, SUM(ll.action_id=6) completed FROM lesson l
INNER JOIN localization_logging ll ON ll.lesson_id = l.id
WHERE ll.language_id = 2 AND
(
ll.action_id = 1
OR
ll.action_id = 6 AND ll.part_id == 1
)
GROUP BY l.id
And now we can wrap it with:
SELECT t.* FROM (...) t WHERE t.completed = 0
You'll usually get faster queries filtering rows with INNER/LEFT JOIN, but you need to test it.
SELECT lesson.* FROM lesson
INNER JOIN localization_logging task1
ON lesson.id = task1.lesson_id
LEFT JOIN localization_logging task2
ON lesson.id = task2.lesson_id
AND task2.language_id = 2
AND task2.part_id = 1
AND task2.action_id = 6
WHERE task1.language_id = 2
AND task1.action_id = 1
AND task2.lesson_id IS NULL
Second table is joined on multiple conditions, but have to list them within ON clause because only results that were in result "force joined" as nulls (left join means left side stays no matter what) are required.
Btw. You'll get multiple rows from lesson if task1 condition is not limiting results to one row - GROUP BY lesson.id then.

Need SQL query with good performance to select data that does NOT match criteria

I have a database with
a company table
a country table
a company_country n:n table which defines which company is available in which country
a product table (each product belongs to one specific categoryId)
and a company_product_country n:n:n table that defines which company offers which product in which country.
The latter has the three primary key columns companyId, productId, countryId and the additional columns val and limitedAvailability. val is an ENUM with the values yes|no|n/a, and limitedAvailability is an ENUM with the values 0|1.
Products within categories 1 or 2 are available in all countries and therefore get countryId = 0. But at the same time, only these very products may have a limitedAvailability = 1.
An SQLFiddle with a test database can be found here: http://www.sqlfiddle.com/#!9/a065a/1/0
It contains five countries, products and companies.
Background information on what I need to select from the database:
A PHP script generates a search form where an arbitrary list of countries and products can be selected. The products are separated by categories (I did not add the category table in the sample database, because it is not needed in this case). For the first category, I can select whether to exclude products with limited availability.
Generating the desired result works fine:
It displays all companies that are available in the selected countries and have at least one of the selected products available. The result offers a column that defines how many of the selected products are available by company.
If the user defines that one or more categories should not contain products with limited availability, then the products within the corresponding categories will not count as a match if the company offers them with limited availability only.
I am pleased with the performance of this query. My original database has got around 15 countries, 100 companies and 150 products. Selecting everything in the search form occupies the MySQL server for around two seconds which is acceptable for me.
The problem:
After generating the result list of companies which matches as many product search criteria as possible, I use PHP to iterate through those companies and run another SQL query that should give me the list of products that the company does not offer corresponding to the search criteria. The following is an example query for companyId 1 to find out which products are not available when
the desired products have the productIds 2, 4 and 5
the product's country availability should be at least one of the countryIds 1, 2 or 3
the product should not have a limitedAvailability when it is from categoryId = 2:
SELECT DISTINCT p.name
FROM `product` p
LEFT JOIN `company_product_country` cpc ON `p`.`productId` = `cpc`.`productId` AND `cpc`.`companyId` = 1
WHERE NOT EXISTS(
SELECT *
FROM company_product_country cpcTmp
WHERE `cpcTmp`.`companyId` = 1
AND cpcTmp.val = 'yes'
AND (
cpcTmp.limitedAvailability = 0
OR p.categoryId NOT IN(2)
)
AND cpcTmp.productId = p.productId
)
AND p.`productId` IN (2,4,5)
AND countryId IN(0,1,2,3);
The database along with this query can be found on the SQLFiddle linked above.
The query generates the correct result, but its performance dramatically decreases with the number of products. My local SQL server needs about 4 seconds per company when searching for 150 products in 15 countries. This is inaccpetable when iterating through 100 companies. Is there any way to improve this query, like avoiding the IN(...) function containing up to 150 products? Or should I maybe split the query into two like so:
First fetch the unmatched products that do not have country Id 0 and are IN the desired countryIds
Then fetch the unmatched products in countryId = 0 and if applicable filter limitedAvailability = 0
?
Your help is gladly appreciated!
I would suggest writing the query like this:
SELECT p.name
FROM product p
WHERE EXISTS (select 1
from company_product_country cpc
where p.productid = cpc.productid and
cpc.companyid = 1 and
cpc.countryid in (1, 2, 3)
) and
NOT EXISTS (select 1
from company_product_country cpcTmp
where cpcTmp.productId = p.productId and
cpcTmp.companyId = 1 and
cpcTmp.val = 'yes' and
cpcTmp.limitedAvailability = 0
) AND
NOT EXISTS (select 1
from company_product_country cpcTmp
where cpcTmp.productId = p.productId and
cpcTmp.companyId = 1 and
cpcTmp.val = 'yes' and
p.categoryId NOT IN (2)
)
p.`productId` IN (2, 4, 5) ;
Then, you want the following indexes:
product(productid, categoryid, name)
company_product_country(productid, companyid, countryid)
company_product_country(productid, companyid, val, limitedavailability)
company_product_country(productid, companyid, val, category)
Note: these indexes completely "cover" the query, meaning that all columns in the query come from the indexes. For most purposes, is probably sufficient to have a single index on company_product_country. Any of the three would do.
Take the query that identifies the products that match the user selection. Subquery it and outer join it to the products table. Exclude the matches.
SQL Fiddle
SELECT p.name
FROM
product p LEFT JOIN
(
SELECT productId
FROM company_product_country cpcTmp
WHERE companyId = 1 AND
countryId IN (0,1,2,3) AND
(
productId IN (4, 5) OR
(productId = 2 AND limitedAvailability = 0)
)
) t
ON p.productId = t.productId
WHERE
t.productId IS NULL AND
p.productId IN (2,4,5)

sql join not displaying correctly

I'm currently trying to join two tables with a left join:
--portal--
id_portal (index)
id_venue
name_portal
--access--
id_access (index)
id_event
id_portal
id_tickets
scan_access
'access' contains a number of ticket types per portal for each event. I need to combine these to get the sum total of the scan_access column for each portal but include the portals that have 'null' scan_access to come up with '0'. To achieve this I've used a left join:
SELECT portal.name_portal, SUM(access.scan_access) AS total_scan
FROM portal LEFT JOIN access ON portal.id_portal = access.id_portal
WHERE portal.id_venue = $venueId
GROUP BY portal.id_portal
ORDER BY portal.id_portal ASC
which means I get the following:
Portal 1 - Null
Portal 2 - 40
Portal 3 - 33
Portal 4 - Null
but I have an issue when I need to also get the above result when taking into account the event (id_event) because when I use the following:
SELECT portal.name_portal, SUM(access.scan_access) AS total_scan
FROM portal LEFT JOIN access ON portal.id_portal = access.id_portal
WHERE portal.id_venue = $venueId AND access.id_event = 20
GROUP BY portal.id_portal
ORDER BY portal.id_portal ASC
I get:
Portal 2 - 40
Portal 3 - 33
which makes sense as those are the only two rows that have an id_event value. But how can I take this col into account without losing the other portals? also, is there a way in sql to make the 'null' a zero when returning a result? (I can fix the null after with php but wanted to see if it was possible)
By putting access.id_event = 20 in your WHERE clause, you turn your LEFT JOIN into an INNER JOIN. Move access.id_event = 20 into your join criteria to preserve your LEFT JOIN. As #echo_me mentioned, you can use COALESCE() to get rid of your zeroes. I'd put it around the SUM(), instead of inside.
SELECT portal.name_portal, COALESCE( SUM(access.scan_access), 0 ) AS total_scan
FROM portal LEFT JOIN access ON portal.id_portal = access.id_portal AND access.id_event = 20
WHERE portal.id_venue = $venueId
GROUP BY portal.id_portal
ORDER BY portal.id_portal ASC
to convert NULL to 0 use this
COALESCE(col, 0)
in your example it will be
SUM(COALESCE(access.scan_access, 0)) AS total_scan

How do I echo out a name -only if- specific rows in one table match specific rows in another based on an id?

Here's my problem: I'm making a crafting system for a game, and I already have my database filled with information for resources required to craft items.
Here are what my relevant tables look like:
table #edible_resources
(edible_resource_id, edible_resource_name, hunger_points, degeneration_id)
table #edible_ground
(id, resource, amount, location)
table #req_crafting_edible
(req_crafting_edible_id, edible_resource_id, req_resource_amount, created_item_id)
table #items
(item_id, item_name, degeneration_id, is_grounded, is_stackable, can_equip, can_edit)
What I want to do is -only- echo out the craftable item's name if, and only if -all- required resources (and their required amounts) are on the ground in the location of the character.
I have a query that comes close:
SELECT items.item_name, items.item_id FROM items
INNER JOIN req_crafting_edible
ON req_crafting_edible.created_item_id = items.item_id
INNER JOIN edible_ground
ON edible_ground.resource = req_crafting_edible.edible_resource_id
AND edible_ground.amount >= req_crafting_edible.req_resource_amount
WHERE edible_ground.location = $current_location
GROUP BY items.item_name
ORDER BY items.item_name
But this shows me craftable items regardless if I have ALL the required items in the area. It shows me items as long as I have -one- of their required resources.
Is there a way to only show the name of a craftable item only if I have -all- the required resources (and their amounts) in edible_ground where location = $current_location?
For more information on what I've tried:
$get_char = mysql_query("SELECT current_char FROM accounts WHERE account_id ='".$_SESSION['user_id']."'");
$current_char = mysql_result($get_char, 0, 'current_char');
$get_loc = mysql_query("SELECT current_location FROM characters WHERE character_id = $current_char");
$current_location = mysql_result($get_loc, 0, 'current_location');
//---------------------------------------------------------------COOKED FOOD
$get_food = mysql_query("SELECT items.item_name, items.item_id FROM items
INNER JOIN req_crafting_edible
ON req_crafting_edible.created_item_id = items.item_id
INNER JOIN edible_ground
ON edible_ground.resource = req_crafting_edible.edible_resource_id
AND edible_ground.amount >= req_crafting_edible.req_resource_amount
WHERE edible_ground.location = $current_location
GROUP BY items.item_name
ORDER BY items.item_name");
while ($food = mysql_fetch_array($get_food)){
echo $food['item_name'].'<br>';
}
This returns:
Baked Fish
Charred Fish
Fish Soup
Glazed Berry
Cake
Grilled Fish
Sashimi
Seafood Soup
Sushi
Udon
On the ground:
1 fish
1 honey
Even though fish soup, berry cake, udon etc needs much more than just the one fish that's in the area.
Can anyone help me figure this out? I'd be forever grateful; I've spent a few days already trying to myself. Please?
And before anyone says anything, I know I need to start using mysqli; unfortunately I didn't even realize it existed when I started to make the game (and learn PHP at the same time months ago), so I'll have to painfully go back and change it all in an update.
You want a HAVING clause to check the count of the records you are grouping through the INNER JOINs.
HAVING count(*) = (
SELECT count(*)
FROM req_crafting_edible
WHERE req_crafting_edible.created_item_id = items.item_id
)
Edit:
So basically you need to know two pieces of information:
How many different resources are required
Do each of those resources have the required amounts
The first is solved by the sub query above.
Your query as-is satisfies the second point but only for 1 resource.
HAVING basically does some special magic on your group clause. HAVING count(*) means there are X records being grouped together. Because of how the join works, you will have 1 item.name for each resource. The sub select gives you the count of how many different resources, and therefore grouped records, are needed for that item. Comparing that sub query with the count(*) of the grouping ensures you have all the needed resources.
And here is the final query, modifying your code above:
SELECT items.item_name, items.item_id
FROM items
INNER JOIN req_crafting_edible
ON req_crafting_edible.created_item_id = items.item_id
INNER JOIN edible_ground
ON edible_ground.resource = req_crafting_edible.edible_resource_id
AND edible_ground.amount >= req_crafting_edible.req_resource_amount
WHERE edible_ground.location = $current_location
GROUP BY items.item_name
HAVING count(*) = (
SELECT count(*)
FROM req_crafting_edible
WHERE req_crafting_edible.created_item_id = items.item_id
)
ORDER BY items.item_name
You only actually want the data from the items table, right? If so I would move to using an exists model:
SELECT I.item_name, I.item_id FROM items I
WHERE NOT EXISTS
(SELECT created_item_id
FROM req_crafting_edible R
WHERE R.created_item_id = I.item_id
AND NOT EXISTS
(SELECT G.resource
FROM edible_ground G
WHERE G.resource = R.edible_resource_id
AND edible_ground.location = $current_location
AND G.amount >= R.req_resource_amount))
ORDER BY I.item_name
I don't have your database to check this, but the logic goes like this:
Find the items that don't have any unsatisfied requirements.
Find the unsatisfied requirements for the current item. (IE. Find
requirements that don't have resources on the ground)
Find the edible resources that match the current requirement, are at
this location, and have enough.
I don't work in mysql as much at the moment, but let me know if this doesn't work.

How can I update multiple mysql columns of a row using the result of a select query?

I have a reviews table that contains three ways to rate an item. The items themselves then have three columns to hold the average for each value respectively.
I could do this using three nested queries in an update query, but I feel like this is inefficient... Is there a way to update them all at once?
So far I've used this as my select query:
SELECT AVG(rating_1),AVG(rating_2),AVG(rating_3) FROM items_reviews WHERE item_id = 1
I just don't know how to use the result of that query to update an item row.
You could use an join in the UPDATE:
UPDATE items a
INNER JOIN
(
SELECT
item_id,
AVG(rating_1) AS avg1,
AVG(rating_2) AS avg2,
AVG(rating_3) AS avg3
FROM items_reviews
WHERE item_id = 1
GROUP BY item_id
) b ON a.item_id = b.item_id
SET
a.avgrating1 = b.avg1,
a.avgrating2 = b.avg2,
a.avgrating3 = b.avg3

Categories