MYSQL Query search in relationship - mysql

For the sake of clarity and this question i will rename the tables so it is a bit clearer for everybody and explain what i want to achieve:
There is an input form with options that return categories ID's. If a 'Product' has 'Category', i want to return/find the 'Product' which lets say has multiple categories(or just 1) and all of its categories are inside the array that is passed from the form.
Products table
ID Title
1 Pizza
2 Ice Cream
Categories table
ID Title
1 Baked food
2 Hot food
ProductsCategories table
ID ProductId CategoryId
1 1 1
2 1 2
So if i pass [1,2] the query should return Product with id 1 since all ProductsCategories are inside the requested array, but if i pass only 1 or 2, the query should return no results.
Currently i have the following query which works, but for some reason if i create a second Product and create a ProductCategory that has a CategoryId same as the first product, the query returns nulll...
SELECT products.*
FROM products
JOIN products_categories
ON products_categories.product_id= products.id
WHERE products_categories.category_id IN (1, 2)
HAVING COUNT(*) = (select count(*) from products_categories pc
WHERE pc .product_id = products.id)
All help is deeply appretiated! Cheers!

In order to match all values in IN clause, you just need to know in addition the number of passed categories which you must use it in HAVING clause:
SELECT
p.*,
GROUP_CONCAT(c.title) AS categories
FROM
Products p
INNER JOIN ProductsCategories pc ON pc.productId = p.ID
INNER JOIN Categories c ON c.ID = pc.categoryId
WHERE
pc.categoryId IN (1,2)
GROUP BY
p.id
HAVING
COUNT(DISTINCT pc.categoryId) = 2 -- this is # of unique categories in IN clause
So in case IN (1,2) result is:
+----+-------+---------------------+
| id | title | categories |
+----+-------+---------------------+
| 1 | Pizza | Baked Food,Hot Food |
+----+-------+---------------------+
1 row in set
In case IN (1,3) result is Empty set (no results).

#mitkosoft, thanks for your answer, but sadly the query is not producing the needed results. If the product's categories are partially in the passed categories the product is still returned. Additionally i might not know how many parameters are sent by the form.
Luckily I managed to create the query that does the trick and works perfectly fine (at least so far)
SELECT products.*,
COUNT(*) as resultsCount,
(SELECT COUNT(*) FROM products_categories pc WHERE pc.product_id = products.id) as categoriesCount
FROM products
JOIN products_categories AS productsCategories
ON productsCategories.product_id= products.id
WHERE productsCategories.category_id IN (7, 15, 8, 1, 50)
GROUP BY products.id
HAVING resultsCount = categoriesCount
ORDER BY amount DESC #optional
That way the query is flexible and gives me exactly what I needed! - Only those products that have all their categories inside the search parameters(not partially).
Cheers! :)

Related

SQL LEFT JOIN with two tables - table order and performance

I got an LEFT JOIN exercise at school:
"List all category names with the number of their products."
Used were two tables from the northwind DB: products (77 rows) and categories (8 rows)
I thought the product table should come first, since the main-data (number of products) will be found there and only the 8 category names will be needed from the joined table. Our teacher argued, that the categories table needs to be the main table, but i still can't understand why.
The two queries are:
SELECT C.CategoryID, CategoryName, COUNT(ProductID) [Count]
FROM Categories C LEFT JOIN Products P
ON C.CategoryID = P.CategoryID
GROUP BY C.CategoryID, CategoryName
and
SELECT P.CategoryID, CategoryName, COUNT(ProductID) [Count]
FROM Products P LEFT JOIN Categories C
ON P.CategoryID = C.CategoryID
GROUP BY CategoryName, P.CategoryID
Can anybody explain to me why, in this case, a certain order of used tables matters in terms of theoretical performance?And if: how so? (does size matter?;))
The name of the exercise tells yo what is the first table in your case.
"List all category names with the number of their products."
So get the all category names. Category names is what you HAVE TO SHOW - ALL OF THEM. You want to show all of them regardless of the fact is there a matching CategoryID in the Products table.
For example, if you want to show all product names with number of their categories then you want to show all product names regardless if there exists matching ProductID in Categories table.
Here is the demo
This demo shows you what the two queries will return if we have 3 categories and one product. It is not the best demo in the world but it does the trick I believe.
The tables:
create table Categories (CategoryID int, CategoryName varchar(20))
create table Products (ProductID int, CategoryID int)
The data:
insert into Categories values(1, 'Cat1');
insert into Categories values(2, 'Cat2');
insert into Categories values(3, 'Cat3');
insert into Products values(1, 1);
Query1:
SELECT C.CategoryID, CategoryName, COUNT(ProductID) as Cnt
FROM Categories C
LEFT JOIN Products P ON C.CategoryID = P.CategoryID
GROUP BY C.CategoryID, CategoryName
Result1:
CategoryID CategoryName Cnt
1 Cat1 1
2 Cat2 0
3 Cat3 0
Query2:
SELECT P.CategoryID, CategoryName, COUNT(ProductID) as Cnt
FROM Products P
LEFT JOIN Categories C ON P.CategoryID = C.CategoryID
GROUP BY CategoryName, P.CategoryID
Result2:
CategoryID CategoryName Cnt
1 Cat1 1
I see in your question that you say:
"Used were two tables from the northwind DB: products (77 rows) and categories (8 rows)"
So maybe it is strange now for you how can my example be like this and yours "since the results of both queries are obviousely the same" ?
Here is the demo that will show you how it can be the same with different set of data.
As an aside, here is another way to get the desired results.
SELECT C.CategoryID, C.CategoryName
( SELECT COUNT(*)
FROM Products AS P
WHERE P.CategoryID = c.CategoryID
) AS "Count"
FROM Categories AS C
The performance will be about the same as the 'correct' LEFT JOIN formulation.
A further note: COUNT(x) does the extra check to see that x IS NOT NULL; COUNT(*) simply counts the number of relevant rows.
In some other situation, you may need COUNT(DISTINCT productID); I suspect you do not need it in this case.

Complex SQL Query Syntax

I want make a query to mysql database that gets me the products that are available, have sale_id of anything1 or anything2 or both and belong to categories of Ids a or b or both from table category_product which has (category_product table) 2 columns product_id and category_id since my products can belong to multiple categories.
HERE IS MY QUERY:
SELECT *
FROM products
WHERE available = 1
AND sale_id = 3
OR sale_id = 4
AND id IN (
SELECT product_id
FROM category_product
WHERE category_id = 2
OR category_id = 3
)
But It didn't work!!!
The Result was that it got the first query correct the one with available = 1 and sale_id = 3 or 4 or both.. But the products it got didn't belong to categories 2 or 3 or both
Any Help Please!!!
I think you just need parentheses or IN:
SELECT p.*
FROM products p
WHERE p.available = 1 AND
p.sale_id IN (3, 4) AND
p.id IN (SELECT product_id
FROM category_product cp
WHERE category_id IN (2, 3)
);
You seem to be learning SQL -- or at least boolean expressions. AND and OR don't automatically do what you want. When you are mixing them, use parentheses until you get the hang of it.
Your condition parses as:
WHERE (p.available = 1 AND p.sale_id = 3) OR
(p.sale_id = 4 AND
p.id IN (SELECT product_id
FROM category_product cp
WHERE category_id IN (2, 3)
)
)

Many-To-Many select only rows with exactly same tags

I have 3 tables: tags, products and relation table between them.
Relation table looks for example like this:
tagId | ProductId
1 | 1
2 | 1
2 | 9
The user can pick two options "All of these" or "One of these".
So if user picks All of these, it's means that the product must have exactly all of tags which the user chose.
So if user pick tags with id 1 and 2, it should select only product with id 1, because this product has exactly the same tags the user chose. (Another way is if the user picks the tag with id 2, it should select only product with id 9.)
So, the product has to have all tags which the user chose (no more, no less).
SQL that I already have for Any/One of these:
SELECT DISTINCT s.SKU
FROM SKUToEAN as s
LEFT JOIN ProductDetails as p ON s.ProductDetailID=p.id
JOIN ProductTagRelation as ptr ON (ptr.productId=p.id and ptr.tagId IN(Ids of selected tags))
Example behavior:
TagId = 1 it should select => None
TagId = 2 it should select => 9
TagId = 1,2 it should select = 1,9
So probably I need two queries. One for any/one of these ( I already have this one ) and the second for all of these.
With PHP I decide which query to use.
You can GROUP BY on the ProductID and use conditional aggregation based filtering inside the Having clause. MySQL automatically casts boolean values to 0/1 when using in numeric context. So, in order to have a specific tagID value available against a ProductID, its SUM(tagId = ..) should be 1.
All of these:
SELECT ptr.productId, s.SKU
FROM SKUToEAN AS s
LEFT JOIN ProductDetails AS p
ON p.id = s.ProductDetailID
JOIN ProductTagRelation AS ptr
ON ptr.productId = p.id
GROUP BY ptr.productId, s.SKU
HAVING SUM(ptr.tagID = 1) AND -- 1 should be there
SUM(ptr.tagID = 2) AND -- 2 should be there
NOT SUM(ptr.tagID NOT IN (1,2)) -- other than 1,2 should not be there
Is this you are looking for (for all condition)?
select product.id
from products
inner join <table> on products.id = <table>.productId
group by product.id
having group_concat(<table>.tagId order by <table>.tagId separator ',') = '1,2';

Matching items in one table that don't match in a subset of a second table

Suppose I have a Product table, and a
id product
1 Apple
2 Bag
3 Cat
4 Ducati
and a Cart table
id user_id product_id
1 1 2
2 1 3
3 2 1
4 3 1
So, I want to look at a particular user and see what he/she does NOT have in their Cart.
In other words, in the above example
SELECT ...... WHERE user_id=1 .....
would return Apple and Ducati because User 1 already has Bag and Cat.
(This may well duplicate another question but there are so many variations I couldn't find the exact match and put in these simple terms may help)
Perform a left join from product to all products purchased by user1, which can be retrieved with a subselect in the join. This will cause all product id's that are not in user1's care to have null product ids. The where clause will select all null product id's meaning they will not have been in a users cart, essentially filtering purchased items.
select p.name
from product p
left join (select product_id, user_id
from cart where user_id = 1)
c
on p.id = c.product_id
where c.product_id is null;
SQL Fiddle: http://sqlfiddle.com/#!2/5318eb/17
Select
*
From Product p
Where p.id Not In
(
Select c.product_id
From Cart c
Where User ID = ____
)
SELECT product FROM product_table
WHERE product NOT IN
(SELECT product_id FROM cart_table WHERE user_id = 1);
This will give you all product for all users which are not in there cart.
select c.user_id,a.Product
from cart c Cross Join product a
left Join
cart b on b.product_id=a.id and c.user_id=b.user_Id
where b.product_id is null
group by c.user_id,a.Product
Sql Fiddle Demo

Getting a COUNT() from a GROUP BY

I've got an incredibly convoluted SQL query (three INNER JOINS) that results in an easy to read result set as follows (simplified). I have inherited the db, so it's impossible to change the structure of any of the existing tables, and therefore I have to perform the convoluted query to get to this point:
product_category | product_code
------------------------------------
Hardware 102
Hardware 104
Hardware 102
Software 205
Software 104
If I then simply do a GROUP BY product_category, product_code, I get most of the final result set I'm interested in:
product_category | product_code
------------------------------------
Hardware 102
Hardware 104
Software 205
Software 104
However, what's missing is number in stock:
product_category | product_code | num_in_stock
--------------------------------------------------------
Hardware 102 2
Hardware 104 1
Software 205 1
Software 104 1
Since I want to be able to COUNT() directly from the processing done by the GROUP BY statement, I'm a little lost.
Here is the SQL query thus far:
SELECT categories.product_category, codes.product_code FROM stock
INNER JOIN products ON stock.product_id = products.id
INNER JOIN codes ON products.code_id = codes.id
INNER JOIN categories ON codes.category_id = categories.id
GROUP BY categories.product_category, codes.product_code
The tables are as follows:
CATEGORIES - e.g., "Hardware", "Software"
CODES - e.g., 100, 204 (belongs to a category)
PRODUCTS - combinations of categories + codes, with a useless version #
STOCK - entries of products, if more than one is in stock, there are multiple entries
So the reason this is so messy is because of the useless version # field in PRODUCTS. What this means is that for a particular combo (e.g., "Hardware 102") it can be entered in PRODUCTS multiple times, each with different version # values, which will then cause STOCK to refer to different ids from PRODUCTS, even though, to me, it's the same product. Ugh!
Any ideas?
Edit:
So let's say there's a product "Misc 999" that has two different versions. This means that there will an entry in CATEGORIES of "Misc", in CODES of "999" (with a category_id of that belonging to "Misc"), and two entries in PRODUCTS (both with the same code_id but with different version info - which I'm ignoring).
Then, if we have 10 of these in stock (3 of one version and 7 of the other, but I'm ignoring version info) there will be 10 entries in the STOCK table, each of which will refer to the PRODUCTS table through an id (two different ids, in this case).
Just add count(*) to your select clause:
SELECT categories.product_category, codes.product_code, count(*) qty_in_stock
FROM stock
INNER JOIN products ON stock.product_id = products.id
INNER JOIN codes ON products.code_id = codes.id
INNER JOIN categories ON codes.category_id = categories.id
GROUP BY categories.product_category, codes.product_code
SQLFiddle here.
It's not entirely clear what you want, but perhaps this works:
SELECT categories.product_category
, codes.product_code
, SUM(num_in_stock) as num_in_stock
FROM (
SELECT product_id
, count(*) as num_in_stock
FROM stock
group by product_id
) a
INNER JOIN products
ON a.product_id = products.id
INNER JOIN codes
ON products.code_id = codes.id
INNER JOIN categories
ON codes.category_id = categories.id
GROUP BY categories.product_category
, codes.product_code