SQL select posts without specific tag - mysql

Straight to the point.
I have three tables POSTS, TAGS, POST_TAGS
POSTS { p_id, title }
TAGS { t_id, name }
POST_TAGS { p_id, t_id }
One post can have multiple tags and i want to select all post that don't have a specific tag. for example take this demo data:
TASKS
p_id | title
1 MyPost 1
2 MyPost 2
3 MyPost 3
TAGS
t_id | name
1 red
2 green
POST_TAGS
p_id | t_id
1 1
2 1
2 2
3 2
Now i want to see all POSTS that do not have the TAG 'green'. My current SQL query looks like this:
SELECT DISCTINCT
p.p_id, p.title
FROM
POSTS as p,
POST_TAGS as pt
WHERE
pt.p_id = p.p_id AND pt.t_id != 2
but this is going to return me this
RESULT
p_id | title
1 MyPost 1
2 MyPost 2
because 'MyPost 2' also has the TAG red it is listet.
Desired result is:
RESULT
p_id | title
1 MyPost 1
EDIT:
Thanks to all of you guys, i accepted GarethD answer because NOT EXISTS is more self-explanatory. NOT IN is working but not NULL save (even if i wasn't asking for it - thanks to Nico Haase as well)
GermanC solution also is correct and working, but isn't as self-explanatory as the selected answer. thanks to you too.

You can do this using NOT EXISTS:
SELECT p.p_id, p.title
FROM POSTS AS p
WHERE NOT EXISTS
( SELECT 1
FROM POST_TAGS AS pt
WHERE pt.p_id = p.p_id
AND pt.t_id = 2
);

You can explicitely join looking for the Green tag, and show those posts where the join was not successful:
SELECT
p.p_id, p.title
FROM
POSTS as p
LEFT OUTER JOIN
POST_TAGS as pt on pt.p_id = p.p_id AND pt.t_id = 2
WHERE
pt.p_id is null

This will do the job, as it searches for all postings which are tagged with 2 in an inner query and excludes them in the outer one
SELECT DISTINCT p.p_id WHERE p.p_id NOT IN(
SELECT DISCTINCT
p.p_id
FROM
POSTS as p,
POST_TAGS as pt
WHERE
pt.p_id = p.p_id AND pt.t_id = 2
)

Related

Cleaning up SQL query with nested query and inner join

Been trying to reintroduce myself to SQL through some practice questions I've developed for myself, but struggling to find a better way of approaching the following problem:
playlists
id title
1 Title1
2 Title2
playlist_clips
id playlist_id clip_id
1 Title1 3
2 Title2 1
playlist_tags
playlist_id tag_id
1 1
1 2
2 2
Clips and Tags are two entirely separate tables, and I am using the playlist_tags and playlist_clips to connect them to the playlists table, to represent the two-way one-to-many relationships.
I wanted to select all the playlists that have a given title, and have ALL of the tags provided in the query (in this example [1, 2]), not just "at least one of them".
This is what I've come up with:
select p_clips.* from
(
select p.id, p.title, count(pc.id) as number_of_clips
from playlists p
left join playlist_clips pc on p.id = pc.playlist_id
where p.title like "Test1"
group by id
) as p_clips
inner join
(
select *
from playlists p
left join playlist_tags pt on p.id = pt.playlist_id
where pt.tag_id in (1, 2)
group by id
having count(*) = 2
) as p_tags
on p_clips.id = p_tags.id
Whilst, from my testing I've found this to work, it doesn't look particularly elegant, and I also assume it's not terribly efficient performance-wise. (I've removed irrelevant parameters from the code for this example, such as select parameters.)
What would be a cleaner way of approaching this, or at the least, a more optimized approach?
Expected Result:
id title
260 Title1
EDIT: I apologize for my initial confusing post, I've tried to clean up my tables and the information they contain.
I wanted to select all the playlists that have a given title, and have ALL of the tags provided in the query (in this example [1, 2]), not just "at least one of them".
You don't need the clips table at all. You don't need left joins or the playlists table in the subquery.
That suggests:
select p.*
from playlists p join
(select pt.playlist_id
from playlist_tags pt
where pt.tag_id in (1, 2)
group by id
having count(*) = 2
) pt
on p.id = pt.playlist_id
where p.title like 'Test1';
You could phrase this without a subquery as well:
select p.*
from playlists p join
playlist_tags pt
on p.id = pt.id
where p.title like 'Test1' and
pt.tag_id in (1, 2)
group by p.id
having count(*) = 2

Entry with most matching relations

I'm trying to create little "recommended" functionality based on the posts with the most matching tags.
I got a layout like this:
Posts
id
---
1
2
3
4
post_tags
post_id | tag_id
---------+---------
1 | 1
1 | 2
2 | 2
2 | 3
2 | 4
3 | 1
3 | 2
3 | 4
4 | 5
tags
id
----
1
2
3
4
5
So if I would retrieve recommendations for the post with id 1 the list should go
3 (2/2 matches)
2 (1/2 matches)
4 (0/2 matches)
My Query so far looks like this:
SELECT DISTINCT
p.id,
p.title,
count(*) as cnt
FROM
posts p
INNER JOIN posts_tags pt ON pt.post_id= p.id
INNER JOIN tags t ON pt.tag_id = t.id
WHERE
t.id IN (
SELECT
pt.tag_id
FROM
posts_tags pt
WHERE
pt.post_id = '30213'
)
GROUP BY
t. NAME
order by count(*) desc
LIMIT 0, 4
I know DISTINCT isn't working because of the count but I wanted to see just what he counted, so the result looks like this:
4 Foo 4881
4 Foo 2560
11 Bar 2094
12 Baz 1998
So what happened? It counted the occurences of the tag in general. So appearantly the first associated tag of "Post 1" is 4881 associated and then pulls the first entry that matches... the one with the lowest id.
I see the problem but I can't solve it.
Your group by makes no sense. You want to aggregate by the post not the tag:
SELECT p.id, p.title, count(*) as cnt
FROM posts p INNER JOIN
posts_tags pt
ON pt.post_id = p.id
WHERE pt.tag_id IN (SELECT pt2.tag_id
FROM posts_tags pt2
WHERE pt2.post_id = 30213
)
GROUP BY p.id, p.title
ORDER BY count(*) desc
LIMIT 0, 4;
This will not return 0. If that is important, you need to use a LEFT JOIN instead of WHERE . . . IN . . ..
Also:
SELECT DISTINCT is almost never used with GROUP BY. It is hard (but not impossible) to come up with a use-case for it.
You don't need the tags table, so I removed it.
Don't use single quotes around numbers. I am guessing that post_id is really a number.
The fix is in the GROUP BY.

MySQL Query for Item List View with tags

Hi what is the right query for this result:
+---------------------------------------+
item_id | item_title | tag_list |
----------------------------------------+
1 | Title 1 | tag1 , tag2, tag3|
2 | Title 2 | tag7 , tag2 |
3 | Title 3 | tag9 , tag5, tag4|
4 | Title 4 | tag7 , tag6, tag3|
-----------------------------------------
I have the following tables:
items -> item_id (PK), item_title
tags -> tag_id (PK), tag_name (unique)
items_tags_xref -> items_tags_xref_id (PK), item_id , tag_id
SQL Fiddle -> http://sqlfiddle.com/#!2/33dea8/1
I tried the following query with no success:
SELECT
items.item_id,
items.title,
(
SELECT GROUP_CONCAT(DISTINCT tags.tag_name)
FROM tags
INNER JOIN items_tags_xref
ON tags.tag_id = items_tags_xref.tag_id
INNER JOIN items
ON items_tags_xref.item_id = items.item_id
WHERE items_tags_xref.item_id = items.item_id
) AS tag_list
FROM items
The result of the above query is showing all the tag_names inside tag_list
But if I do this query:
SELECT
items.item_id,
items.title,
(
SELECT GROUP_CONCAT(DISTINCT tags.tag_name)
FROM tags
INNER JOIN items_tags_xref
ON tags.tag_id = items_tags_xref.tag_id
INNER JOIN items
ON items_tags_xref.item_id = items.item_id
WHERE items_tags_xref.item_id = 4
) AS tag_list
FROM items
"4 is a specific item_id" I get the correct result only for that item. If only 4 is dynamic when I use items.item_id inside tag_list..
I'm trying to solve this problem for days and been searching for an answer in google but can't find anything. Maybe i'm using the wrong keywords :( but if anybody can give me at least a tip on how to do this right. it would be really helpful. thanks!
P.S. i'm new in mysql
I'm a little confused, what part of the problem does the following not solve?
SELECT i.*
, GROUP_CONCAT(tag_name) tags
FROM items i
JOIN items_tags_xref it
ON it.item_id = i.item_id
JOIN tags t
ON t.tag_id = it.tag_id
GROUP
BY i.item_id;
http://sqlfiddle.com/#!2/33dea8/6
Also, your surrogate key on the items_tags_xref table seems entirely redundant, as the remaining columns would serve as a perfectly viable composite natural PRIMARY KEY

MySQL Join Subcategory

I have the categories and pages tables. My table is structure is something like this :
pages one has three columns
id | category_id | content
1 2 example page of subexample
2 1 example page of example
categories one has four
id | is_parent | parent | name
1 1 NULL example
2 0 1 subexample
I want to get the all pages of that category, if its parent, i want to include the pages which are the member of its sub categories also.
With the example i gave, think like, when user selected to see the whole contents of the example category, i want him to see the example page of example and example page of subexample.
$query = "SELECT * FROM `pages` WHERE `category_id` = :cid
union
select * from `pages` p
join `categories` k ON k.id = p.category_id
where k.parent = :cid";
i've tried the above code, but not worked for me. i'm not sure with my logic also.
You can try
SELECT *
FROM pages
WHERE category_id IN
(
SELECT c.id
FROM categories c LEFT JOIN categories p
ON c.parent = p.id
WHERE p.id = :cid OR c.id = :cid
)
or
SELECT *
FROM pages p
WHERE category_id IN
(
SELECT :cid
UNION ALL
SELECT id
FROM categories
WHERE parent = :cid
)
Here is SQLFiddle demo
Note: it will only work for one level deep meaning category and its subcategories only. If a subcategory were to have other subcategories then you need to use dynamic SQL to traverse the categories tree.
SELECT pages.* FROM pages,categories
WHERE pages.category_id=categories.id
AND (categories.id=:cid OR parent=:cid)

MySQL join with LIMIT 1 from two tables

I have two tables. One with information about properties. The other stores 3 images for each property. Of these three images - one is marked as being the "main" image.
So I have:
Properties:
p_id name
1 villa a
2 villa b
3 villa c
4 villa d
and
Images
i_id p_id main
1 1 0
2 1 0
3 1 1
4 2 0
5 2 1
6 2 0
I need to produce a query which returns all of the properties with the id of their main image. e.g.
p_id name i_id
1 villa a 3
2 villa b 5
I know this will involve using LIMIT 1 and a join, but not sure where to start, I have already attempted doing this by using a subquery but felt it must be less complicated than what I was doing....
* HOW DO I *
Make it so it orders the query by "main" selecting the top 1 (i.e. so if main is not set it will still select an image) ?
Here's one way:
select *
from properties p
left join images i
on p.p_id = i.p_id
and i.main = 1
The left join will return a NULL image if no main image is found.
Here you go:
SELECT p_id, name, i_id
FROM properties p INNER JOIN images i ON (p.p_id = i.p_id AND i.main = 1)
or
SELECT p_id, name, i_id
FROM properties p INNER JOIN images i ON (p.p_id = i.p_id)
WHERE i.main = 1
SELECT p.p_id, p.name i.i_id FROM properties p JOIN images i USING p_id where p_id = 1;
select p.p_id, p.name, i.i_id
from properties p join images i on p.p_id = i.p_id
where i.main=1
You want to use a query like the following:
SELECT p.p_id, p.name, i.i_id FROM Images i INNER JOIN Properties p ON p.p_id = i.p_id WHERE i.main = 1