MySQL query for a many-to-many relationship table - mysql

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
)

Related

mysql Select rows with exclusive AND

I have a table of posts and a table of post_tags
Here is my post table structure example:
post_id int(11)
post_user int(11)
post_title text
post_content longtext
and this is my post_tags structure example :
post_id int(11)
tag_id int(11)
What I need is selecting all posts from posts table that have tag_id of 1 AND 2 at the same time, I've tried different joins without success.
example of post_tags table data :
post_id tag_id
1 1
2 1
5 2
6 1
6 2
HERE for example my query should return post (from post table) whos id is 6, watch in the example post_id of 6 has tag_id 1 AND tag_id 2 NOT ONLY ONE OF THEM but BOTH at the same time.
You can do this with aggregation:
select post_id
from post_tags pt
group by post_id
having sum(tag_id = 1) > 0 and
sum(tag_id = 2) > 0;
If you want to see the actual information from posts, just join that table in.
EDIT (a bit of an explanation):
You have a "set-within-sets" query. This is a common query and I prefer to solve it using aggregation and a having clause, because this is the most general approach.
Each condition in the having clause is counting the number of rows that match one of the tags. That is, sum(tag_id = 1) is counting up the rows in post_tags where this is true. The condition > 0 is just saying "tag_id = 1 exists on at least one row".
The reason I like this approach is because you can generalize it easily. If you want tags 3 and 4 as well:
having sum(tag_id = 1) > 0 and
sum(tag_id = 2) > 0 and
sum(tag_id = 3) > 0 and
sum(tag_id = 4) > 0;
And so on.
select a.post_id, b.post_id, a.post_user, b.post_tags
from posts as a
inner join post_tags as b
where b.post_tags in(1, 2)
or
select a.post_id, b.post_id, a.post_user, b.post_tags
from posts as a
inner join post_tags as b
where b.post_tags =1 or b.post_tags = 2
This should work
select q.* from (
select p.post_id as post_id from post_tags p
where p.tag_id=1
and exists (
select 1 from post_tags p2
where p2.post_id=p.post_id
and p2.tag_id=2)
) as t
inner join posts q on posts_id=t.post_id;
Give this a try:
SELECT post.*
FROM (SELECT T1.post_id
FROM (SELECT * FROM post_tags WHERE 1 IN(tag_id)) T1
INNER JOIN (SELECT * FROM post_tags WHERE 2 IN(tag_id)) T2 ON T1.post_id = T2.post_id)
T3
INNER JOIN post ON T3.post_id=post.post_id;
SQL Fiddle Link: http://sqlfiddle.com/#!2/04f74/33

MySQL Left Join and excluding values

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)

SQL Query to match records with/without all defined many-to-many matches

I have three tables, posts, tags, & postTags. As you can probably guess, posts holds information about blog posts, tags holds information about that tags that are in use on the system, and postTags holds the relationships between posts and tags.
Now, lets assume I know the tagIDs of each tag I am looking for, as well as ones I dont, what would be a suitable, what would be a suitable query to fetch all the posts that match the criteria of having all the tagIDs I specify on one list, and having none of what I specify on another?
One way I can work out is:
SELECT
`posts`.*,
CONCAT(',', GROUP_CONCAT(`postTags`.`tagID`), ',') AS `pTags`
FROM
`posts`
INNER JOIN
`postTags`
ON
`postTags`.`postID` = `posts`.`postID`
GROUP BY
`posts`.`postID`
HAVING
`pTags` LIKE '%,2,%'
AND
`pTags` LIKE '%,3,%'
AND
`pTags` NOT LIKE '%,5,%'
This query will select all the posts that have been tagged by tagID 2 & 3, and not tagged by tagID 5. But this seems potentially quite slow, especially when the data is being filtered by a large number of tags.
EDIT
SQL Fiddle
You can try to optimize the query with EXISTS Strategy:
SELECT
`posts`.*
FROM
`posts`
WHERE
EXISTS (
SELECT 1 FROM `postTags`
WHERE `postTags`.`postID` = `posts`.`postID`
AND `postTags`.`tagID` = 2
)
AND
EXISTS (
SELECT 1 FROM `postTags`
WHERE `postTags`.`postID` = `posts`.`postID`
AND `postTags`.`tagID` = 3
)
AND NOT EXISTS (
SELECT 1 FROM `postTags`
WHERE `postTags`.`postID` = `posts`.`postID`
AND `postTags`.`tagID` = 5
)
I would do joins against a couple of sub selects, avoiding correlated sub queries.
Something like the following (not sure you need the concatenated list of tags in the SELECT, but left it there for now)
SELECT `posts`.*,
CONCAT(',', Sub1.TagList, ',') AS `pTags`
FROM `posts`
INNER JOIN (
SELECT postID, GROUP_CONCAT(`postTags`.`tagID`) AS TagList, COUNT(*) AS TagCount
FROM postTags
WHERE tagID IN (2, 3)
GROUP BY postID
HAVING TagCount = 2
) Sub1
ON posts.postID = Sub1.postID
LEFT OUTER JOIN (
SELECT postID
FROM postTags
WHERE tagID IN (5)
) Sub2
ON posts.postID = Sub2.postID
WHERE Sub2.postID IS NULL
I believe it bring the posts you want.
SELECT
p.*, GROUP_CONCAT(pt.tagID)
FROM
posts p
inner join postTags pt on p.postID = pt.postID
WHERE
not exists (
SELECT
1
FROM
Tags t
WHERE
t.tagID in (2,3)
AND not exists
(
select 1 from postTags pt where pt.postID = p.postID and pt.tagID = t.tagID
)
)
and not exists(
SELECT
1
FROM
Tags t
WHERE
t.tagID in (5)
AND exists
(
select 1 from postTags pt where pt.postID = p.postID and pt.tagID = t.tagID
)
)
GROUP BY p.postID

MySQL order by the number of matches in an intermediate table

So I have a query that is trying to grab "related posts".
Categories have a one-to-many relationship with posts. Tags have a many-to-many relationship. So my tables look roughly like this:
posts table:
id | category_id | ... | ...
tags table:
id | ... | ...
post_tag intermediate table:
id | post_id | tag_id | ... | ...
So if I have a single Post row already, and what to grab its "related" posts. My logic is roughly that I want to grab only posts that are in the same category, but to order those posts by the amount of tags that match the original post. So another post in the same category that has the exact same tags as the original post, should be a very high match, whereas a post that only matches 3/4 of the tags will show up lower in the results.
Here is what I have so far:
SELECT *
FROM posts AS p
WHERE p.category_id=?
ORDER BY ( SELECT COUNT(id)
FROM post_tag AS i
WHERE i.tag_id IN( ? )
)
LIMIT 5
BINDINGS:
Initial Posts Category ID;
Initial Posts Tag IDs;
Clearly this is not going to actually order the results by the correct values in the sub-select. I am having trouble trying to think of how to join this to achieve the correct results.
Thanks in advance!
If I undestood your question correctly this is what you're looking for:
SELECT p.*,
Count(pt.tag_id) AS ord
FROM posts AS currentpost
JOIN posts AS p
ON p.category_id = currentpost.category_id
AND p.id != currentpost.id
JOIN post_tag AS pt
ON pt.post_id = p.id
AND pt.tag_id IN (SELECT tag_id
FROM post_tag
WHERE post_id = currentpost.id)
WHERE currentpost.id = ?
GROUP BY p.id
ORDER BY ord DESC
BINDINGS: Initial posts.id;
and you only have to specify the id of the current post in my version so you don't have to fetch the posts tags beforehand and format them suitably for an in clause
EDIT:
This should be a faster query by avoiding double joining posts, if you don't like user variables just replace all currentpostid with ? and triple-bind post_id:
set #currentpostid = ?;
select p.*, count(pt.tag_id) as ord
from posts as p,
join post_tag as pt
on pt.post_id = p.id
and pt.tag_id in (select tag_id from post_tag where post_id = #currentpostid)
where p.category_id = (select category_id from posts where id=#currentpostid)
and p.id != #currentpostid
group by p.id
order by ord desc;
Try this,
SELECT posts.*
FROM posts,(SELECT p.id,
Count(pt.tag_id) AS count_tag
FROM posts AS p,
post_tag AS pt
WHERE p.category_id = '***'
AND pt.post_id = p.id
AND pt.tag_id IN(SELECT tag_id
FROM post_tag
WHERE post_tag.post_id = '***')
GROUP BY p.id
) temp
WHERE posts.id =temp.id ORDER BY temp.count_tag desc
Where you can fill *** as you already have 1 post row

Select query to get tags per post_id

I am trying to make simple mysql select query, I have 3 tables
post: post_id...
tags: tag_id, tag_name
post_tag: id_post, id_tag
and I wrote this query:
$sql=mysql_query("select * from post
LEFT JOIN post_tag
ON post_tag.id_post = post.post_id
LEFT JOIN tags
ON post_tag.id_tag = tags.tag_id
GROUP BY post_id
ORDER BY post_id
DESC LIMIT 5");
but I am getting only one tag per post even there is more tags with same post_id?
while($row=mysql_fetch_array($sql))
{
$post_id =$row['post_id '];
$tag_name=$row['tag_name'];
echo $post_id $tag_name;
}
You could use something like:
SELECT post_id, GROUP_CONCAT(tag_name) AS tag_name FROM post
LEFT JOIN post_tag
ON post_tag.id_post = post.post_id
LEFT JOIN tags
ON post_tag.id_tag = tags.tag_id
GROUP BY post_id
ORDER BY post_id
DESC LIMIT 5
This will give you one record for each post with a comma seperated list of every tagname that is linked to that post.
Your query is grouping by post_id. In other databases, this would cause an error. In MySQL this is considered a feature called hidden columns.
The values that you get are not guaranteed to come from the same row (although in practice think they do). You probaly want something like:
select *
from post LEFT JOIN
post_tag
ON post_tag.id_post = post.post_id LEFT JOIN
tags
ON post_tag.id_tag = tags.tag_id
ORDER BY post_id
DESC LIMIT 5
However, if you just want the tags on a post, you might consider using gruop_concat:
select post_id, group_concat(tag.tag_name separator ',') as tags
from post LEFT JOIN
post_tag
ON post_tag.id_post = post.post_id LEFT JOIN
tags
ON post_tag.id_tag = tags.tag_id
group by post_id