WHERE Clause for One-To-Many Association - sql

I have two tables Products and ProductProperties.
Products
name - string
description - text
etc etc
ProductProperties
product_id - integer
property_id - integer
There is also a table Properties which basically stores the list of property names and their attributes
How can I implement a SQL command that finds a product with the property_ids (A or B or C) AND (X or Y or Z)
I've got upto here:
SELECT DISTINCT "products".*
FROM "products"
INNER JOIN "product_properties" ON "product_properties"."product_id" = "products"."id" AND "product_properties"."deleted_at" IS NULL
WHERE "products"."deleted_at" IS NULL
AND (product_properties.property_id IN ('504, 506, 403'))
AND (product_properties.property_id IN ('520, 501, 502'))
But it doesn't really work since it's looking for a Product Property which has both values 504 and 520, which will never exist.
Would appreciate some help!

You need to define intermediate resultsets on a property group basis:
SELECT DISTINCT p.*
FROM products p
JOIN product_properties groupA ON groupA.product_id = p.id AND groupA.deleted_at IS NULL AND groupA.property_id IN ('504')
JOIN product_properties groupB ON groupB.product_id = p.id AND groupB.deleted_at IS NULL AND groupB.property_id IN ('520')
WHERE p.deleted_at IS NULL
You see, you detected the problem yourself very nicely: "But it doesn't really work since it's looking for a Product Property which has both values 504 and 520, which will never exist."
Indeed, recordsets are immutable within a query, all single criteria applied to them are applied all at once. You need to duplicate each table and apply individual criteria to them.

One method uses exists or in:
select p.*
from products p
where p.id in (select pp.product_id
from product_properties pp
where pp.propertyid in ('504', '520')
);
This saves you from having to use distinct in the outer query.
If, perchance, you really mean finding the products that have all the properties, then a join and group by work:
select p.*
from products p join
product_properties pp
on p.id = pp.product_id
where pp.propertyid in ('504', '520')
group by p.id -- yes, this is allowed in Postgres
having count(*) = 2;

Hi try this queries i just thinking about it so i didn't try any of them check i got the idea i want to do
SELECT DISTINCT "products".*
FROM products pr
WHERE id IN
(
SELECT product_id FROM ProductProperties WHERE property_id IN (504,520)
GROUP BY product_id
HAVING Count(*) = 2
) AND "products"."deleted_at" IS NULL
SELECT DISTINCT "products".*
FROM products pr, INNER JOIN (
SELECT product_id,count(*) as nbr FROM ProductProperties WHERE property_id IN (504,520)
GROUP BY product_id
) as temp ON temp.product_id = pr.id
WHERE "products"."deleted_at" IS NULL AND temp.nbr = 2
and also you can check this one as well ( you can use also the join in where clause instead of using INNER JOIN)
SELECT DISTINCT products.* FROM products as p
INNER JOIN product_properties as p1 ON p1.product_id = p.id
INNER JOIN product_properties as p2 ON p2.product_id = p.id
WHERE p.deleted_at IS NULL
AND p1.property_id = '504' AND p1.deleted_at IS NULL
AND p2.property_id = '520' AND p2.deleted_at IS NULL

Related

Subquery in FROM not working in Oracle SQL

I'm working on converting a query from having a subquery in the WHERE section to the FROM section.
As I understand it, the FROM clause will return a table, which I'm calling "product_locations" and then my outer query can retrieve information from that table. I can't see if the outer query is working at this point, because I'm getting stuck on an error that I'm missing a right parenthesis.
At this point, I think my main problem is not understanding how to properly identify the two parts of an IN clause that constitutes the subquery.
Here is the original query:
SELECT size_option,
product.product_name
FROM sizes
JOIN available_in ON sizes.sized_id = available_in.sizes_id
JOIN product ON product.product_id = available_in.product_id
WHERE (SELECT COUNT (store_name)
FROM store_location IN (SELECT COUNT (store_location_id)
FROM sells
JOIN product ON sells.product_id = product.product_id
GROUP BY sells.store_location)
This is the query I'm attempting:
SELECT size_option,
product_location.product_name
FROM (SELECT COUNT(store_name)
FROM store_location) IN (SELECT COUNT (store_location_id)
FROM sells
JOIN product ON sells.product_id = product.product_id
GROUP BY sells.store_location_id) product_location
JOIN sizes ON sizes.size_option = product_location.size_option
JOIN available_in ON sizes.sizes_id = available_in.sizes_id
JOIN product ON product.product_id = available_in.product_id
and the error I'm getting:
SELECT size_option,
product_location.product_name
FROM (SELECT COUNT(store_name)
FROM store_location) IN (SELECT COUNT (store_location_id)
FROM sells
JOIN product ON sells.product_id = product.product_id
GROUP BY sells.store_location_id) product_location
JOIN sizes ON sizes.size_option = product_location.size_option
JOIN available_in ON sizes.sizes_id = available_in.sizes_id
JOIN product ON product.product_id = available_in.product_id
Error at Command Line : 4 Column : 31
Error report -
SQL Error: ORA-00933: SQL command not properly ended
00933. 00000 - "SQL command not properly ended"
*Cause:
*Action:
Here's a picture of the error in my editor
Here's the ERD
Using an IN condition in a FROM clause is likely to give you grief, as
"A condition specifies a combination of one or more expressions and logical (Boolean) operators and returns a value of TRUE, FALSE, or UNKNOWN."
(see documentation). Thus, a construct like (SELECT ...) IN (SELECT ...) - if syntactically correct - will return true, false, or unknown - which is useful in a WHERE clause (not in a FROM clause).
In one of your comments, you are explaining that ...
" The intention is to show all products that are available in all locations."
Using a small test data set (see dbfiddle), encompassing 5 of your tables, the following query may give you
a starting point for finding the solution:
-- Find the store count for each product (table SELLS)
-- and return all product_ids that are available everywhere (STORE_LOCATION count)
select product_id
from (
select
product_id
, count( store_location_id ) store_count
from sells
group by product_id
having count( store_location_id ) = ( select count(*) from store_location )
) ;
-- result
PRODUCT_ID
----------
10
Alternative
-- ---------------------------------------------------
-- use analytics -> we don't need GROUP BY and HAVING
-- ---------------------------------------------------
-- 1 count all store_locations (SL)
-- 2 count the amount of stores a product is located in (S)
-- 3 JOIN the 2 result sets (equijoin)
-- 4 return the product_id found
-- NOTE: we are working with Oracle -> don't use AS when defining table aliases (AS can be used for column aliases)
select product_id -- 4
from (
select count(*) store_count from store_location -- 1
) SL join (
select unique
product_id
, count( store_location_id ) over ( partition by product_id ) store_count -- 2
from sells
) S on SL.store_count = S.store_count -- 3
;
-- result
PRODUCT_ID
----------
10
Once you get the correct product_id(s), you can "bolt on" the remaining tables needed for your query (using JOINs) and write all required column names (including table aliases) into the SELECT.
E.g.
select
S.product_id
, P.product_name
, SZ.size_option
from (
select count(*) store_count from store_location
) SL join (
select unique
product_id
, count( store_location_id ) over ( partition by product_id ) store_count
from sells
) S on SL.store_count = S.store_count
join product P on S.product_id = P.product_id
join available_in AI on AI.product_id = P.product_id
join sizes SZ on AI.sizes_id = SZ.sizes_id
;
-- result
PRODUCT_ID PRODUCT_N SIZE_OP
---------- --------- -------
10 product10 option1
10 product10 option2
you query may like below
SELECT size_option,
product_location.product_name,
(
SELECT COUNT(store_name)
FROM store_location
) as store_name_cnt,
(
SELECT COUNT (store_location_id)
FROM sells
JOIN product ON sells.product_id = product.product_id
GROUP BY sells.store_location_id
) as location_count
from product_location
JOIN sizes ON sizes.size_option = product_location.size_option
JOIN available_in ON sizes.sizes_id = available_in.sizes_id
your query error
SELECT size_option,
product_location.product_name
FROM (SELECT COUNT(store_name) --invalid from
FROM store_location)
IN (SELECT COUNT (store_location_id) -- there is no where but you use in
FROM sells
JOIN product ON sells.product_id = product.product_id
GROUP BY sells.store_location_id) product_location -- inconsistent table alias
JOIN sizes ON sizes.size_option = product_location.size_option
JOIN available_in ON sizes.sizes_id = available_in.sizes_id
JOIN product ON product.product_id = available_in.product_id
Per the OP in comments: The intention is to show all products that are available in all locations.
Consider joining two aggregate query derived tables (or subqueries in FROM or JOIN clause):
one that counts stores at specific product
one that counts stores across all products
Then join the subqueries by corresponding product_id and store_count value. Be sure to use table aliases for readability.
SELECT s.size_option,
p.product_name
FROM sizes AS s
JOIN available_in AS a ON s.sized_id = a.sizes_id
JOIN product AS p ON p.product_id = a.product_id
JOIN
--- STORE COUNT BY PRODUCT
(
SELECT sub_p.product_id, COUNT(sl.store_location_id) AS store_count
FROM sells AS sl
JOIN product AS sub_p ON sl.product_id = sub_p.product_id
GROUP BY sub_p.product_id
) AS agg_p
ON agg_p.product_id = p.product_id
JOIN
--- STORE COUNT ACROSS ALL PRODUCTS
(
SELECT COUNT(sl.store_location_id) AS store_count
FROM sells AS sl
JOIN product AS sub_p ON sl.product_id = sub_p.product_id
) AS agg_s
ON agg_s.store_count = agg_p.store_count

Designing and Querying Product / Review system

I created a product / review system from scratch and I´m having a hard time to do the following query in SQL Server.
My schema has different tables, for: products, reviews, categories, productPhotos and Brand. I have to query them all to find the brand and category name, photos details, Average Rating and Number of Reviews.
I´m having a hard time to get No. of reviews and average rating.
Reviews can be hidden (user has deleted) or blocked (waiting for moderation). My product table doesn't have No. of Reviews or Average Rating columns, so I need to count it on that query, but not counting the blocked and hidden ones (r.bloqueado=0 and r.hidden=0).
I have the query below, but it´s counting the blocked and hidden. If I uncomment the "and r.bloqueado=0 and r.hidden=0" part I get the right counting, but then it doesn't show products that has 0 reviews (something I need!).
select top 20
p.id, p.brand, m.nome, c.name,
count(r.product) AS NoReviews, Avg(r.nota) AS AvgRating,
f.id as cod_foto,f.nome as nome_foto
from
tblBrands AS m
inner join
(tblProducts AS p
left join
tblProductsReviews AS r ON p.id = r.product) ON p.brand = m.id
left join
tblProductsCategorias as c on p.categoria = c.id
left join
(select
id_product, id, nome
from
tblProductsFotos O
where
id = (SELECT min(I.id)
FROM tblProductsFotos I
WHERE I.id_product = O.id_product)) as f on p.id = f.id_product
where
p.bloqueado = 0
//Problem - and r.bloqueado=0 and r.hidden=0
group by
p.id, p.brand, p.modalidade, m.nome, c.name, f.id,f.nome"
Need your advice:
I have seen other systems that has Avg Rating and No. of Reviews in the product table. This would help a lot in the complexity of this query (probably also performance), but then I have to do extra queries in every new review, blocked and hidden actions. I can easily to that. Considering that includes and updates occurs much much less than showing the products, this sounds nice.
Would be a better idea to do that ?
Or is it better to find a way to fix this query ? Can you help me find a solution ?
Thanks
For count the number of product you can use case when and sum assigning 1 there the value is not r.bloqueado=0 or r.hidden=0 and 0 for these values (so you can avoid the filter in where)
"select top 20 p.id, p.brand, m.nome, c.name, sum(
case when r.bloqueado=0 then 0
when r.hidden=0 then 0
else 1
end ) AS NoReviews,
Avg(r.nota) AS AvgRating, f.id as cod_foto,f.nome as nome_foto
from tblBrands AS m
inner join (tblProducts AS p
left join tblProductsReviews AS r ON p.id=r.product ) ON p.brand = m.id
left join tblProductsCategorias as c on p.categoria=c.id
left join (select id_product,id,nome from tblProductsFotos O
where id = (SELECT min(I.id) FROM tblProductsFotos I
WHERE I.id_product = O.id_product)) as f on p.id = f.id_product where p.bloqueado=0
group by p.id, p.brand, p.modalidade, m.nome, c.name, f.id,f.nome"
for avg could be you can do somethings similar
It's very easy to lose records when combining a where clause with an outer join. Rows that do not exist in the outer table are returned as NULL. Your filter has accidentally excluded these nulls.
Here's an example that demonstrates what's happening:
/* Sample data.
* There are two tables: product and review.
* There are two products: 1 & 2.
* Only product 1 has a review.
*/
DECLARE #Product TABLE
(
ProductId INT
)
;
DECLARE #Review TABLE
(
ReviewId INT,
ProductId INT,
Blocked BIT
)
;
INSERT INTO #Product
(
ProductId
)
VALUES
(1),
(2)
;
INSERT INTO #Review
(
ReviewId,
ProductId,
Blocked
)
VALUES
(1, 1, 0)
;
Outer joining the tables, without a where clause, returns:
Query
-- No where.
SELECT
p.ProductId,
r.ReviewId,
r.Blocked
FROM
#Product AS p
LEFT OUTER JOIN #Review AS r ON r.ProductId = p.ProductId
;
Result
ProductId ReviewId Blocked
1 1 0
2 NULL NULL
Filtering for Blocked = 0 would remove the second record, and therefore ProductId 2. Instead:
-- With where.
SELECT
p.ProductId,
r.ReviewId,
r.Blocked
FROM
#Product AS p
LEFT OUTER JOIN #Review AS r ON r.ProductId = p.ProductId
WHERE
r.Blocked = 0
OR r.Blocked IS NULL
;
This query retains the NULL value, and ProductId 2. Your example is a little more complicated because you have two fields.
SELECT
...
WHERE
(
Blocked = 0
AND Hidden = 0
)
OR Blocked IS NULL
;
You do not need to check both fields for NULL, as they appear in the same table.

How would you construct this SQL query? (MySQL)

Let assume we have these tables:
product
product_id
product_name
category
category_id
category_name
product_in_category
product_in_category_id
product_id
category_id
how would you get all products that are not in specific category in the product_in_category table (without duplicates).
In other words, all products that are not been assigned to category 10, for instance.
Also, if one product is in categories 1, 5 and 10, it shouldn't come up the result.
Using LEFT JOIN/IS NULL:
SELECT p.*
FROM PRODUCT p
LEFT JOIN PRODUCT_IN_CATEGORY pic ON pic.product_id = p.product_id
AND pic.category_id = 10
WHERE pic.product_in_category_id IS NULL
Using NOT IN
SELECT p.*
FROM PRODUCT p
WHERE p.product_id NOT IN (SELECT pic.product_id
FROM PRODUCT_IN_CATEGORY pic
WHERE pic.category_id = 10)
Using NOT EXISTS
SELECT p.*
FROM PRODUCT p
WHERE NOT EXISTS (SELECT NULL
FROM PRODUCT_IN_CATEGORY pic
WHERE pic.product_id = p.product_id
AND pic.category_id = 10)
Which is best?
It depends on if the columns being compared are nullable (values can be NULL) or not. If they are nullable, then NOT IN/NOT EXISTS are more efficient. If the columns are not nullable, then LEFT JOIN/IS NULL is more efficient (MySQL only).
SELECT * FROM product_in_category WHERE category_id!=10
or am I missing something? I guess I was missing the no duplicates part, look at InSane's better answer.
SELECT product_id FROM product
LEFT OUTER JOIN product_in_category
ON product.product_id = product_in_category.product_id
WHERE product_in_category.product_id IS NULL
GROUP BY product_id

Many to many query

I have two tables products and sections in a many to many relationship and a join table products_sections. A product can be in one or more sections (new, car, airplane, old).
Products
id name
-----------------
1 something
2 something_else
3 other_thing
Sections
id name
-----------------
1 new
2 car
Products_sections
product_id section_id
--------------------------
1 1
1 2
2 1
3 2
I want to extract all products that are both in the new and the car sections. In this example result returned should be product 1. What is the correct mysql query to obtain this?
SELECT Products.name
FROM Products
WHERE NOT EXISTS (
SELECT id
FROM Sections
WHERE name IN ('new','car')
AND NOT EXISTS (
SELECT *
FROM Products_sections
WHERE Products_sections.section_id = Sections.id
AND Products_sections.product_id = Products.id
)
)
In other words, select those products for which none of the desired Section.id values is missing from the Products_sections table for that product.
Answer andho's comment:
You can put
NOT EXISTS (<select query>)
into a WHERE clause like any other predicate. It will evaluate to TRUE if there are no rows in the result set described by <select query>.
Stepwise, here's how to get to this query as an answer:
Step 1. The requirement is to identify all products that are "in both the 'new' and 'car' sections".
Step 2. A product is in both the 'new' and 'car' sections if both the 'new' and 'car' sections contain the product. Equivalently, a product is in both the 'new' and 'car' sections if neither of those sections fails to contain the product. (Note the double negative: neither fails to contain.) Restated again, we want all the products for which there is no required section failing to contain the product.
The required sections are these:
SELECT id
FROM Sections
WHERE name IN ('new','car')
Therefore, the desired products are these:
SELECT Products.name
FROM Products
WHERE NOT EXISTS ( -- there does not exist
SELECT id -- a section
FROM Sections
WHERE name IN ('new','car') -- that is required
AND (the section identified by Sections.id fails to contain the product identified by Products.id)
)
Step 3. A given section (such as 'new' or 'car') does contain a particular product if there's a row in Products_sections for the given section and particular product. So a given section fails to contain a particular product if there is no such row in Products_sections.
Step 4. If the query below does contain a row, the section_id section does contain the product_id product:
SELECT *
FROM Products_sections
WHERE Products_sections.section_id = Sections.id
AND Products_sections.product_id = Products.id
So the section_id section fails to contain the product (and that's what we need to express) if the query above does not produce a row in its result, or if NOT EXISTS ().
Seems complicated, but once you get it in your head, it sticks: Are all required items present? Yes, so long as there does not exist a required item that is not present.
The way I always do these is this:
Start at what you're trying to get (products), and then go through your lookup table (products_sections) to what you're trying to filter by (sections). This way, you can have it in plain view what you're looking for, and you never have to memorize surrogate keys (which are a great thing to have, not to memorize).
select distinct
p.name
from
products p
inner join products_sections ps on
p.product_id = ps.product_id
inner join sections s1 on
ps.section_id = s1.section_id
inner join sections s2 on
ps.section_id = s2.section_id
where
s1.name = 'new'
and s2.name = 'car'
Voila. Three inner joins, and you have a nice, clear, concise query that is obvious what it's bringing back. Hope this helps!
SELECT product_id, count(*) AS TotalSection
FROM Products_sections
GROUP BY product_id
WHERE section_id IN (1,2)
HAVING TotalSection = 2;
See if this works in mysql.
The query below is a little unwieldy, but it should answer your question:
select products.id
from products
where products.id in
(
select products_sections.product_id
from products_sections
where products_sections.section_id=1
)
and products.id in
(
select products_sections.product_id
from products_sections
where products_sections.section_id=2
)
Self-join on two subsets of join table and then selecting unique product ids.
SELECT DISTINCT car.product_id
FROM ( SELECT product_id
FROM Product_sections
WHERE section_id = 2
) car JOIN
( SELECT product_id
FROM Product_sections
WHERE section_id = 1
) neww
ON (car.product_id = neww.product_id)
This query is a variation of more general solution:
SELECT DISTINCT car.product_id
FROM product_sections car join
product_sections neww ON (car.product_id = neww.product_id AND
car.section_id = 2 AND
neww.section_id = 2)
Less efficient but more straight forward solution is:
SELECT p.name FROM Products p WHERE
EXISTS (SELECT 'found car'
FROM Products_sections ps
WHERE ps.product_id = p.id AND ps.section_id = 2)
AND
EXISTS (SELECT 'found new'
FROM products_sections ps
WHERE ps.product_id = p.id AND ps.section_id = 1)
----------------
I manipulated with ids for clarity. If necessary replace expressions section_id = 2 and section_id = 1 with
section_id = (SELECT s.id FROM Sections s WHERE s.name = 'car')
section_id = (SELECT s.id FROM Sections s WHERE s.name = 'new')
Also, you can select product names by plugging in any of the queries above like this:
SELECT Products.name FROM Products
WHERE EXISTS (
SELECT 'found product'
FROM product_sections car join
product_sections neww ON (car.product_id = neww.product_id AND
car.section_id = 2 AND
neww.section_id = 2)
WHERE car.product_id = Products.id
)
SELECT p.*
FROM Products p
INNER JOIN (SELECT ps.product_id
FROM Products_sections ps
INNER JOIN Sections s
ON s.id = ps.section_id
WHERE s.name IN ("new","car")
GROUP BY ps.product_id
HAVING Count(ps.product_id) = 2) pp
ON p.id = pp.product_id
This query will get you the result without having to add more inner joins when you need to search more sections. What will change here are:
values inside the IN () paranthesis
The value in the where clause for count which should be replaced with the number of sections you are searching
SELECT id, name FROM
(
SELECT
products.id,
products.name,
sections.name AS section_name,
COUNT(*) AS count FROM products
INNER JOIN products_sections
ON products_sections.product_id=products.id
INNER JOIN sections
ON sections.id=products_sections.section_id
WHERE sections.name IN ('car', 'new')
GROUP BY products.id
) AS P
WHERE count = 2
select
`p`.`id`,
`p`.`name`
from `Sections` as `s`
join `Products_sections` as `ps` on `ps`.`section_id` = `s`.`id`
join `Products` as `p` on `p`.`id` = `ps`.`product_id`
where `s`.`id` in ( 1,2 )
having count( distinct `s`.`name` = 2 )
will return...
id name
-----------------
1 something
Is that what you were looking for?

Join two tables where all child records of first table match all child records of second table

I have four tables: Customer, CustomerCategory, Limit, and LimitCategory. A customer can be in multiple categories and a limit can also have multiple categories. I need to write a query that will return the customer name and limit amount where ALL the customers categories match ALL the limit categories.
I'm guessing it would be similar to the answer here, but I can't seem to get it right. Thanks!
Edit - Here's what the tables look like:
tblCustomer
customerId
name
tblCustomerCategory
customerId
categoryId
tblLimit
limitId
limit
tblLimitCategory
limitId
categoryId
I THINK you're looking for:
SELECT *
FROM CustomerCategory
LEFT OUTER JOIN Customer
ON CustomerCategory.CustomerId = Customer.Id
INNER JOIN LimitCategory
ON CustomerCategory.CategoryId = LimitCategory.CategoryId
LEFT OUTER JOIN Limit
ON Limit.Id = LimitCategory.LimitId
Updated!
Thanks to Felix for pointing out a flaw in my existing solution (3 years after I originally posted it, hehe). After looking at it again, I think this might be correct. Here I'm getting (1) the customers and limits with matching categories, plus the number of matching categories, (2) the number of categories per customer, (3) the number of categories per limit, (4) I then ensure the number of categories for customer and limits is the same as the number of the matches between the customers and limits:
UNTESTED!
select
matches.name,
matches.limit
from (
select
c.name,
c.customerId,
l.limit,
l.limitId,
count(*) over(partition by cc.customerId, lc.limitId) as matchCount
from tblCustomer c
join tblCustomerCategory cc on c.customerId = cc.customerId
join tblLimitCategory lc on cc.categoryId = lc.categoryId
join tblLimit l on lc.limitId = l.limitId
) as matches
join (
select
cc.customerId,
count(*) as categoryCount
from tblCustomerCategory cc
group by cc.customerId
) as customerCategories
on matches.customerId = customerCategories.customerId
join (
select
lc.limitId,
count(*) as categoryCount
from tblLimitCategory lc
group by lc.limitId
) as limitCategories
on matches.limitId = limitCategories.limitId
where matches.matchCount = customerCategories.categoryCount
and matches.matchCount = limitCategories.categoryCount
I don't know if this will work or not, just a thought i had and i can't test it, I'm sures theres a nicer way! don't be too harsh :)
SELECT
c.customerId
, l.limitId
FROM
tblCustomer c
CROSS JOIN
tblLimit l
WHERE NOT EXISTS
(
SELECT
lc.limitId
FROM
tblLimitCategory lc
WHERE
lc.limitId = l.id
EXCEPT
SELECT
cc.categoryId
FROM
tblCustomerCategory cc
WHERE
cc.customerId = l.id
)