So I created a sql fiddle to explain my problem much clearer:
http://sqlfiddle.com/#!9/3122282/1
As you can see I have 3 tables and 1 of them links the 2 others.
I want to make it so if I say "give me the products that is (color green OR red) and PET (dog)"?
I tried doing:
select `ptl`.`product_id`
from `tags` inner join `tags` as `ptl`
on `tags`.`id` = `ptl`.`tag_id`
where ((`tags`.`tag` = "color" and `tags.value` in ("green", "red"))
or (`tags`.`tag` = "pet" and `tags.value` in ("dog")))
having count(distinct `ptl.tag_id`) = 2
// 2 in that case is the number of tag "category".
but this doesn't seem to work. since having is just checking the count, it will also return the products with 2 color tags without any pet.
You can join the 3 tables, group by product and set the conditions in the HAVING clause:
SELECT p.id, p.name
FROM products p
INNER JOIN product_tags_link pt ON pt.product_id = p.id
INNER JOIN tags t ON pt.tag_id = t.id
GROUP BY p.id, p.name
HAVING SUM(t.tag = 'color' AND t.value IN ('green', 'red')) > 0
AND SUM(t.tag = 'pet' AND t.value IN ('dog')) > 0
See the demo.
You are not joining tags table with product_tags_link and products.
Take this query as a base and add the conditions on the where clause
select *
from products p
inner join product_tags_link ptl on ptl.product_id = p.id
inner join tags t on t.id = ptl.tag_id
where CONDITIONS
a CONDITIONS that can be taken as example
p.id = 1 and t.tag = 'color' and t.value = 'green'
Related
I have following database structure to store product options.
Now i have problem to filter out products that match only given options. First i did WHERE option_id IN (array of options), but that would give me products that match any of the options and that is not solution. User wants to filter out only products with given material, color, and size for instance. And if i do WHERE option_id = 4 AND option_id = 6 for instance i get nothing.
Here is my query:
SELECT DISTINCT p.id AS id,
...
FROM products p
LEFT JOIN product_categories pc ON p.id = pc.product_id
LEFT JOIN product_images pi ON p.id = pi.product_id
LEFT JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1
AND po.option_id = 1 // only to get the idea
GROUP BY id
ORDER BY id DESC
LIMIT 0,
12
Just to mention it is PHP application , where user select options from select element with or without multiple attribute.
How to acomplish this?
You can use having:
SELECT p.id AS id, ...
FROM products p JOIN
product_categories pc
ON p.id = pc.product_id LEFT JOIN
product_images pi
ON p.id = pi.product_id JOIN
product_options po
ON p.id = po.product_id
WHERE p.product_active = 1 AND
po.option_id IN (4, 6)
GROUP BY p.id
HAVING COUNT(DISTINCT po.option_id) = 2
ORDER BY p.id DESC
LIMIT 0, 12;
The HAVING clause is specifying that a given id has two matching options. Because of the WHERE clause, these are the only two options that you care about.
I didn't change your approach (you didn't supply the complete query), but you are doing joins along different dimensions -- categories, images, and options. This creates a Cartesian product for each product, and that is often not the best approach to such a query.
There is no need for LEFT JOIN in the solution.
SELECT DISTINCT p.id AS id
FROM products p
JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1
AND po.option_id IN (1, 2, 3)
GROUP BY p.id
HAVING COUNT(po.option_id) = 3
My solution keep only tables necessary to find the products with specified options.
In the case you want products having exactly this options and no others you can use NOT EXISTS:
SELECT DISTINCT p.id AS id
FROM products p
JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1 AND
po.option_id IN (1, 2, 3) and
NOT EXISTS (
SELECT 1
FROM product_options po2
WHERE p.id = po2.product_id and po2.option_id NOT IN (1, 2, 3)
)
GROUP BY p.id
HAVING COUNT(po.option_id) = 3
If you want to select products accoding to the other conditions (like product categories and so on) then use IN in the WHERE clause. This approach avoids generating duplicate po.option_id and the outer query will still work correctly even without DISTINCT in COUNT.
SELECT DISTINCT p.id AS id
FROM products p
JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1 AND
po.option_id IN (1, 2, 3) AND
-- use the following IN predicate to select products with specific features without introducing duplicates in your query
p.id IN (
select product_id FROM product_categories WHERE <your_condition>
)
GROUP BY p.id
HAVING COUNT(po.option_id) = 3
You select products with image lists. Something like:
select products.*, group_concat(product_images.id)
Additionally there may be options the product must all meet. This is criteria that belongs in the WHERE clause.
select
p.*,
(select group_concat(image) from product_images i where i.product_id = p.id) as images
from products p
where product_active = 1
and id in
(
select product_id
from product_options
where option_id in (1,3,55,97)
group by product_id
having count(*) = 4 -- four options in this example
);
Thanks guys, i've managed to return exactly what i wanted.
Now i just have problem with pagination query for the filtered products.
Final search query:
SELECT DISTINCT p.id AS id,
main_price,
promotion_price,
NEW,
sale,
recommended,
COUNT(pi.filename) AS image_count,
GROUP_CONCAT(DISTINCT pi.filename
ORDER BY pi.main_image DESC, pi.id ASC) AS images,
name_sr,
uri_sr,
description_sr
FROM products p
LEFT JOIN product_categories pc ON p.id = pc.product_id
LEFT JOIN product_images pi ON p.id = pi.product_id
LEFT JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1
AND po.option_id IN(1)
AND p.main_price BETWEEN 5250.00 AND 14000.00
GROUP BY id
HAVING COUNT(DISTINCT po.option_id) = 1
ORDER BY id DESC
LIMIT 0,
12
Pagination query is something like this i modified it accorgin to new filter query:
SELECT COUNT(DISTINCT p.id) AS number
FROM products p
LEFT JOIN product_categories pc ON p.id = pc.product_id
LEFT JOIN product_images pi ON p.id = pi.product_id
LEFT JOIN product_options po ON p.id = po.product_id
WHERE p.product_active = 1
AND po.option_id IN(1)
AND p.main_price BETWEEN 5250.00 AND 14000.00
GROUP BY(p.id)
HAVING COUNT(DISTINCT po.option_id) = 1
If i leave out DISTINCT in SELECT COUNT i don't get filtered pagination, if i set DISTINCT i get number of rows that corespond to pagination. I suppose i could add another count(*) to all of this with subquery, but not sure if that is way to go and if there is more efficient and elegant way to do this.
I have product table and product_attributes table. I want filter products with necessary attributes, here is my sql:
SELECT * FROM product p
INNER JOIN product_attributes p2 ON p.id = p2.product_id
WHERE p2.attribute_id IN (637, 638, 629))
But, it gives me all products even if product have only one attribute (637 for example). But i need products with all given attributes (637, 638, 629).
There's a fairly standard approach:
select * from product
where id in (
SELECT id
FROM product p
JOIN product_attributes p2 ON p.id = p2.product_id
AND p2.attribute_id IN (637, 638, 629)
GROUP BY id
HAVING COUNT(distinct attribute_id) = 3)
The HAVING clause ensures there were 3 different attribute ids (ie they were all found).
This can be expressed as a straight join (rather than the ID IN(...)), but it's simpler to read and should perform OK like thus.
Of slight interest may be the moving of the attribute id condition into the JOIN's ON condition.
This is an example of a "set-within-sets" subquery. I like to solve these with aggregation and the having clause, because this is the most flexible solution:
SELECT p.*
FROM product p join
product_attributes pa
on p.id = pa.product_id
group by p.id
having sum(pa.attribute_id = 637) > 0 and
sum(pa.attribute_id = 638) > 0 and
sum(pa.attribute_id = 629) > 0
An alternative having clause is:
having count(distinct case when pa.attribute_id IN (637, 638, 629)
then pa.attribute_id
end) = 3
You can use a query like this:
SELECT * FROM product p
INNER JOIN product_attributes p21
ON p.id = p21.product_id and p21.attribute_id = 637
INNER JOIN product_attributes p22
ON p.id = p22.product_id and p22.attribute_id = 638
INNER JOIN product_attributes p23
ON p.id = p23.product_id and p23.attribute_id = 629
Can anyone tell me ways to do this kind of search in a database?
I got these tables:
posts (id, tags_cache)
tags (id, name)
posts_tags (post_id, tag_id)
The user enters a search query (say "water blue") and I want to show the posts that have both tags.
The only way I can think of to search is using FIND_IN_SET, this way:
SELECT p.*, GROUP_CONCAT(t.name) AS tags_search
FROM posts p
LEFT JOIN posts_tags pt ON p.id = pt.post_id
LEFT JOIN tags t ON pt.tag_id = t.id
GROUP BY p.id
HAVING FIND_IN_SET('water', tags_search) > 0
AND FIND_IN_SET('blue', tags_search) > 0
The posts.tags_cache text column stores the names and id of the tags it belongs to (this way: water:15 blue:20).
To avoid JOINs by using this column for search, I've tried LIKE and INSTR but these will give inexact results since you can search for "ter" and you'll gets posts tagged 'water' and 'termal' for example. I've also tried REGEXP which gives exact results, but it's a slow process.
I can't use MATCH as tables use InnoDB.
So... is or are there other ways to accomplish this?
[Edit]
I forgot to mention that the user could search for many tags (not just 2), and even exclude tags: search posts tagged 'water' but not 'blue'. With FIND_IN_SET this works for me:
HAVING FIND_IN_SET('water', tags_search) > 0
AND NOT FIND_IN_SET('blue', tags_search) > 0
[Edit2]
I did some performance test (i.e. only checked how long the queries took, cached) as ypercube suggested, and these are the results:
muists | Bill K | ypercu | includes:excludes
--------------------------
0.0137 | 0.0009 | 0.0029 | 2:0
0.0096 | 0.0081 | 0.0033 | 2:1
0.0111 | 0.0174 | 0.0033 | 2:2
0.0281 | 0.0081 | 0.0025 | 5:1
0.0014 | 0.0013 | 0.0015 | 0:2
I don't know if this info is valid resource... But it shows that ypercube's method with a JOIN per tag is the quickest.
I don't understand why you don't want to use JOINs nor why you're trying to use LEFT JOINs. You're looking for things that are there (rather than might be there) so get rid of the LEFT JOINs and just JOIN. And get rid of the tags_cache column, you're only asking for trouble with that sort of thing.
Something like this is what you're looking for:
select p.id
from posts p
join posts_tags pt on p.id = pt.post_id
join tags t on pt.tag_id = t.id
where t.name in ('water', 'blue')
group by p.id
having count(t.id) = 2
The 2 in the HAVING clause is the number of tags you're looking for.
And if you want to exclude certain tags, you could just add that to the WHERE clause like this:
select p.id
from posts p
join posts_tags pt on p.id = pt.post_id
join tags t on pt.tag_id = t.id
where t.name in ('water', 'blue')
and p.id not in (
select pt.post_id
from posts_tags pt
join tags t on pt.tag_id = t.id
where t.name in ('pancakes', 'eggs') -- Exclude these
)
group by p.id
having count(t.id) = 2
Finding posts that match all of several conditions on different rows is a common problem.
Here are two ways to do it:
SELECT p.*
FROM posts p
INNER JOIN posts_tags pt ON p.id = pt.post_id
INNER JOIN tags t ON pt.tag_id = t.id
WHERE t.name IN ('water', 'blue')
GROUP BY p.id
HAVING COUNT(DISTINCT t.name) = 2;
Or:
SELECT p.*
FROM posts p
INNER JOIN posts_tags pt1 ON p.id = pt1.post_id
INNER JOIN tags t1 ON pt1.tag_id = t1.id
INNER JOIN posts_tags pt2 ON p.id = pt2.post_id
INNER JOIN tags t2 ON pt2.tag_id = t2.id
WHERE (t1.name, t2.name) = ('water', 'blue');
Re comment and edit:
The problem with the HAVING solution is that it must perform a table-scan, searching every row in the tables. This is often much slower than a JOIN (when you have appropriate indexes).
To support tag exclusion conditions, here's how I'd write it:
SELECT p.*
FROM posts p
INNER JOIN posts_tags pt1 ON p.id = pt1.post_id
INNER JOIN tags t1 ON pt1.tag_id = t1.id AND t1.name = 'water'
LEFT OUTER JOIN (posts_tags pt2
INNER JOIN tags t2 ON pt2.tag_id = t2.id AND t2.name = 'blue')
ON p.id = pt2.post_id
WHERE t2.id IS NULL;
Avoiding using JOINs because you read it somewhere that they are bad is senseless. You must understand that a JOIN is a basic operation in relational databases, and you should use it where the job calls for it.
For your additional request, excluding some tags, you could use the next approach. It will give you all posts that have both water and blue tags but neither black, white or red:
SELECT p.*
FROM posts p
INNER JOIN posts_tags pt1 ON p.id = pt1.post_id
INNER JOIN tags t1 ON pt1.tag_id = t1.id
INNER JOIN posts_tags pt2 ON p.id = pt2.post_id
INNER JOIN tags t2 ON pt2.tag_id = t2.id
WHERE (t1.name, t2.name) = ('water', 'blue') --- include
AND NOT EXISTS
( SELECT *
FROM posts_tags pt
INNER JOIN tags t ON pt.tag_id = t.id
WHERE p.id = pt.post_id
AND t.name IN ('black', 'white', 'red') --- exclude
)
SELECT SUM(case when p.status = 2 then p.value end) as 'val_accepted'
FROM
props AS p
INNER JOIN (p_contents AS pc
INNER JOIN contents AS c ON c.id = pc.library_id)
ON p.id = pc.prop_id
WHERE p.account_id = 3
GROUP BY (pc.library_id)
so, what's happening:
there are two p_contents that are associated with a prop. those two p_contents have the same library_id which points to a corresponding content.
So, the SUM of p.value is double what it should be because there are two p_contents that point to the same content
How do I not double SUM the p.value?
EDIT:
I figured out how to use DISTINCT, but I still need access to the inner columns...
SELECT c.name as 'library_name',
SUM(case when p.status = 2 then p.value end) as 'val_accepted',
FROM
props AS p
INNER JOIN
(
SELECT DISTINCT(pc.library_id), prop_id
FROM prop_contents AS pc
INNER JOIN
(
SELECT name, visibility, id, updated_at
FROM contents AS c
) as c
ON c.id = pc.library_id
)as pc
ON p.id = pc.prop_id
WHERE p.account_id = 3
GROUP BY (pc.library_id)
and now I get the error:
Unknown column 'c.name' in 'field list')
Here's one solution. First reduce the set to distinct rows in an derived table, then apply the GROUP BY to that result:
SELECT SUM(case when d.status = 2 then d.value end) as 'val_accepted'
FROM (
SELECT DISTINCT p.id, p.status, p.value, pc.library_id
FROM props p
INNER JOIN p_contents AS pc ON p.id = pc.prop_id
INNER JOIN contents AS c ON c.id = pc.library_id
WHERE p.account_id = 3) AS d
GROUP BY d.library_id
You use DISTINCT(pc.library_id) in your example, as if DISTINCT applies only to the column inside the parentheses. This is a common misconception. DISTINCT applies to all columns of the select-list. DISTINCT is not a function; it's a query modifier.
I've got 3 tables, products, products_tags and tags. A product can be connected to multiple tags via the products_tags table.
But if i would like to search on a product now with multiple tags, i do a query like this:
SELECT
*
FROM
products
LEFT JOIN
products_tags
ON
products_tags.product_id = products.id
LEFT JOIN
tags
ON
products_tags.tag_id = tags.id
WHERE
tags.name = 'test'
AND
tags.name = 'test2'
Which doesn't work :(.
If i remove the AND tags.name = 'test2' it works. So i can only search by one tag, i explained the query and it said impossible where.
How can i search on multiple tags using a single query?
Thanks!
Have you tried something like:
WHERE
(tags.name = 'test'
OR
tags.name = 'test2')
Or
WHERE
tags.name in( 'test', 'test2')
Because even if you join one product to multiple tags, each tag record only has one value for name.
you need to join twice for test and test2:
select products.*
from products
join product_tags as product_tag1 on ...
join tags as tag1 on ...
join product_tags as product_tag2 on ...
join tags as tag2 on ...
where tag1.name = 'test'
and tag2.name = 'test2'
for test or test2, you need one join and an in clause and a distinct:
select distinct products.*
from products
join product_tags on ...
join tags as tags on ...
where tags.name IN('test', 'test2')
You'll have to do a group by and COUNT(*) to ensure BOTH (or however many) are ALL found.
The first query (PreQuery) joins the products tags table to tags and looks for same with matching count of tags to find... THEN uses that to join to products for finalized list
SELECT STRAIGHT_JOIN
p.*
FROM
( select pt.product_id
from products_tags pt
join tags on pt.tag_id = tags.id
where tags.name in ('test1', 'test2' )
group by pt.product_id
having count(*) = 2
) PreQuery
join products on PreQuery.Product_ID = Products.ID
If you are searching for products that have BOTH the "test" and "test2" tags, then you will need to join to the product_tag and tag table twice each.
Also, use inner joins since you only want the products that have these tags.
Example:
SELECT products.*
FROM products
INNER JOIN products_tags pt1 ON pt1.product_id = products.id
INNER JOIN products_tags pt2 ON pt2.product_id = products.id
INNER JOIN tags t1 ON t1.id = pt1.tag_id
INNER JOIN tags t2 ON t2.id = pt2.tag_id
WHERE t1.name = 'test'
AND t2.name = 'test2'