MySQL Left Join and excluding values - mysql

UPDATE
There is a database model in sqfiddle: http://sqlfiddle.com/#!2/8dbb0/10
And I updated the question according to the annotations.
Original Post
I have three tables:
posts
tags
tag_to_post
Lets asume a tag_id 1 that has been used by user 2. Now I want to show user 2 all posts, that another user has tagged with tag_id 1, but user 2 has not tagged with tag_id 1 so far.
The query:
SELECT posts.id AS post_id, tags.id AS tag_id, tag_to_post.user_id AS
user_tagged_post
FROM posts
LEFT JOIN tag_to_post ON tag_to_post.post_id = posts.id
LEFT JOIN tags ON tags.id = tag_to_post.tag_id
WHERE tags.id =1
Produces something like:
post_id | tags_id | user_tagged_post
1 | 1 | 1
1 | 1 | 2
2 | 1 | 2
3 | 1 | 1
So there should only be left post id 3.
First I tried with where-statement like:
WHERE tags.id = 1 AND tag_to_post.user_id != '2'
But this of course doesn't exclude post_id 1 cause it is a douplicate. I think there should be a DISTINCT or GROUPED BY before the WHERE clause, but this seems not to be allowed. So the only way is a sub-query? I didn't find a solution so far. Any ideas?

If I understand you correctly, it would seem like a straight forward LEFT JOIN;
SELECT t1.post_id, p.title, t1.tag_id, t1.user_id
FROM tag_to_post t1
JOIN posts p ON t1.post_id = p.id
LEFT JOIN tag_to_post t2
ON t1.tag_id = t2.tag_id AND t1.post_id = t2.post_id AND t2.user_id = 2
WHERE t1.user_id <> 2 AND t2.user_id IS NULL
An SQLfiddle to test with.

May be you need something like this
SELECT posts.id, posts.title, tags.tag, tag_to_post.user_id
FROM posts
INNER JOIN tag_to_post ON tag_to_post.post_id = posts.id
INNER JOIN tags ON tags.id = tag_to_post.tag_id
WHERE tags.id = 1 AND tag_to_post.user_id <> 2
Based on comments:
SELECT DISTINCT posts.id, posts.title, tags.tag, A.user_id
FROM posts
INNER JOIN tag_to_post A ON A.post_id = posts.id
INNER JOIN tags ON tags.id = A.tag_id
WHERE tags.id = 1
AND A.post_id NOT IN (SELECT post_id FROM tag_to_post WHERE tags.id = 1 AND user_id = 2)

Related

How to perform left joins on multiple tables along with aggregate functions

SELECT
posts.id,posts.user_id,
username, title,posts.body as post_body, posts.created_at, posts.is_open, posts.views as views, users.photoUrl,
COUNT(DISTINCT answers.id) as answer_count,
COUNT(DISTINCT comments.id) as comment_count,
COUNT (CASE answers.approved WHEN 1 THEN 1 ELSE null END) as correct_count,
GROUP_CONCAT(tagname) as tags
FROM posts
LEFT JOIN posttag ON posts.id = post_id
JOIN tags ON tag_id = tags.id
JOIN users ON user_id = users.id
LEFT JOIN answers ON answers.post_id = posts.id
LEFT JOIN comments ON posts.id = comments.post_id
WHERE posts.id = ?;
I am trying to query every post with its corresponding answers, comments and tags.
However in my query result, I get 8 tags when there are 4
The COUNT correct_count changes from from 1 to 4
How can I get the correct data?
Use DISTINCT for correct_count and tags also:
.....................................
COUNT(DISTINCT CASE WHEN answers.approved THEN answers.id END) as correct_count,
GROUP_CONCAT(DISTINCT tagname) as tags

Mysql pivot join fails

I have following tables
Tags
id | tag_name | slug |
1 | tag1 |tag1
2 | tag1 |tag1
products
id | proudct_name
1|product1
2|product2
product_tags
id | product_id | tag_id
1|1||1
2|1|2
3|2|1
i need retrieve only those product which belongs to both tag1 and tag2
select * from `products` INNER JOIN product_tags ON products.id=product_tags.product_id
INNER JOIN tags ON tags.id=product_tags.tag_id WHERE product_tags.tag_id=1 AND product_tags.tag_id=2
But my query return empty result
This will look the product that in both tables:
select * from `products` a where exists(select 1 from product_tags b where b.tag_id = 1 and a.product_id = b.product_id) and exists(select 1 from product_tags b where b.tag_id = 2 and a.product_id = b.product_id)
You can do what you want with inner join, which appears to be what you are trying. You just need to inner join twice:
select p.*
from products p join
product_tags pt1
on pt1.product_id = p.id and pt1.tag_id = 1 join
product_tags pt2
on pt2.product_id = p.id and pt2.tag_id = 2;
This might be easier to code in Laravel.
Your version doesn't work because the tag cannot be both "1" and "2" at the same time -- it can have different rows with those values but only one value per row.
Also, the tags table is not needed for the query.

MySQL query for a many-to-many relationship table

i have some problems here and i need help.
I have a table called posts, another table called tags and many-to-many relationship between them called item_tags
Here is the strucutre:
posts
id
title
description
tags
id
name
seo
item_tags
id
post_id
tag_id
So let's say now i want to build a query to match multiple tags by having their tag_id already. For an example let's try to match all posts which has tag_id 11, tag_id 133 and tag_id 182. What i could do was selecting them with OR operator but these are not the results i want because what i want is to match all posts which has all mentioned tags only not just if contains some...
My query was:
SELECT * FROM item_tags WHERE tag_id='11' OR tag_id='133' OR tag_id='182'
Which is wrong...
Here is a screenshot of the table: https://i.imgur.com/X60HIM5.png
PS: I want to build a search based on multiple keywords (tags).
Thank you!
If you want all posts that have been tagged with all three tags, then you could use:
select p.*
from posts p
where exists (select 1 from item_tags a where a.post_id = p.id and a.tag_id = 11)
and exists (select 1 from item_tags a where a.post_id = p.id and a.tag_id = 133)
and exists (select 1 from item_tags a where a.post_id = p.id and a.tag_id = 182)
If you want posts tagged with any of those three tags use:
select p.*
from posts p
where exists (select 1 from item_tags a where a.post_id = p.id and a.tag_id = 11)
or exists (select 1 from item_tags a where a.post_id = p.id and a.tag_id = 133)
or exists (select 1 from item_tags a where a.post_id = p.id and a.tag_id = 182)
Important Note: I have not tested it!
SELECT * FROM posts WHERE id IN(SELECT post_id FROM item_tags WHERE item_tags.tag_id='11' OR item_tags.tag_id='133' OR item_tags.tag_id='182');
You can group result by post_id and then save only those having all tags linked
select *
from posts
where id in (
select post_id
from item_tags
where tag_id in (11,133,182)
group by post_id
having count(tag_id)=3
)

MySQL: order by value from second table, use default if value not set

This takes place inside WordPress, but it's a general MySQL question.
There are two tables, one of which contains posts, the other metadata, linked by ID.
post_title | ID post_id | meta_key | meta_value
-----------+--- --------+----------+-----------
title | 1 1 | key_1 | aaa
-----------+--- --------+----------+-----------
title | 2 1 | key_2 | bbb
--------+----------+-----------
1 | mykey | 1
--------+----------+-----------
2 | key_n | ccc ddd
I'm trying to order results on some column value, which might not be set for all rows. Basically, I want to see rows with this column/value pair set first, followed by all the others. Each post might have some metadata associated with it, based on meta_key and meta_value pairs. There may be more keys for a single post and they need not include the one I want to sort by.
The problem is that using a MySQL query with a WHERE meta_key = mykey will exclude all the posts where this key doesn't exist. So what I need is a way to display a default value for all those posts, where this meta key doesn't exist.
First step: It's easy to select all rows with a certain meta_key:
SELECT
p.ID, p.post_title, p.post_type, p.post_date, m.meta_value
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS m ON p.ID = m.post_id
WHERE
m.meta_key = 'mykey'
Second step: how do I select all the rows where this meta_key doesn't exist?
Here's what I mean, but this is probably a bad solution:
SELECT
p.ID, p.post_title, p.post_type, p.post_date, "some_default"
FROM wp_posts AS p
WHERE
p.ID NOT IN (
SELECT
p.ID
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS m ON p.ID = m.post_id
WHERE
m.meta_key = 'mykey'
)
Third step: show combined results. This could be a UNION of both queries above.
I'm sure there must be a better sulution. What's more important, I don't know how to specify additional paramaters – e. g., first find all posts with some given meta key, or title, or category etc. and then order by said mykey as layed out above.
FINAL EDIT
If anyone's interested, here's the final solution in context. RedFilter's answer made it possible, thanks again.
SELECT p1.ID, p1.post_title, p1.post_type, p1.post_date, m1.meta_value AS meta1, meta2
FROM wp_posts AS p1
LEFT JOIN wp_postmeta AS m1 ON m1.post_id = p1.ID
LEFT JOIN wp_term_relationships AS tr0 ON tr0.object_id = p1.ID
LEFT JOIN wp_term_taxonomy AS tt0 ON tr0.term_taxonomy_id = tt0.term_taxonomy_id
LEFT JOIN wp_terms AS t0 ON tt0.term_id = t0.term_id
LEFT JOIN
(
SELECT
p.ID, IF (m.meta_value = 'on', 1, 0) AS meta2
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS m
ON p.ID = m.post_id
and m.meta_key = 'mykey'
) as extra
ON extra.ID = p1.ID
WHERE 1 = 1
AND m1.meta_key = 'some-other-meta-key'
AND p1.post_type IN ('post', 'some-custom-post-type')
AND tt0.taxonomy = 'some-taxonomy'
AND t0.term_id = 'some-id'
ORDER BY meta2 DESC, meta1 ASC, p1.post_date DESC
SELECT p.ID, p.post_title, p.post_type, p.post_date,
ifnull(m.meta_value, 'default val') as meta_value
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS m ON p.ID = m.post_id
and m.meta_key = 'mykey'

How to search on MySQL using JOINs?

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
)