SQL select rows having several values in common - mysql

I'm having a problem to select some articles rows depending on a condition.
Here's my problem : All my articles can have several 'tags' attached, so my structure looks like this :
articles articles_tags tags
¯¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯¯¯¯¯¯¯ ¯¯¯¯
id article_id id
title tag_id name
content
[...]
Now I'd like to select ALL articles which have BOTH tags 2 and 3 for example. I tried this :
SELECT * FROM articles a
JOIN articles_tags at
ON (a.id = at.article_id)
WHERE
at.tag_id IN(2, 3)
GROUP BY article_id
But this will select all articles which have AT LEAST tags IDs #2 or #3 (seems logic after all)
Is there any trick or something to get only the articles having a defined list of tag IDs ?
Thanks you

This problem is called Relational Division
SELECT a.*
FROM articles a
INNER JOIN
(
SELECT at.article_id
FROM articles a
INNER JOIN articles_tags at
ON a.id = at.article_id
WHERE at.tag_id IN(2, 3)
GROUP BY at.article_id
HAVING COUNT(*) = 2
) b ON a.id = b.article_id
SQL of Relational Division

Related

SELECT columns different from GROUP BY columns

Having this database schema (just for illustration purpose)
[articles (id_article, title)]
[articles_tags (id_tag, id_article)]
[tags (id_tag, name)]
using MySQL it's possible to do:
SELECT a.title, COUNT(at.id_tag) tag_count FROM articles a
JOIN articles_tags at ON a.id_article = at.id_article
JOIN tags t ON t.id_tag = at.id_tag
GROUP BY a.id_article
ORDER BY tag_count DESC
resulting in a result where you have on each row article's title and article's tag count, e.g.
mysql for beginner | 8
ajax for dummies | 4
Since ORACLE doesn't support non-aggregated columns in SELECT statement, is it possible to do this anyhow in one query? When you fulfill ORACLE's needs by either adding aggregate function to SELECT statement or adding the column to GROUP BY statement you already get different results.
Thanks in advance
Yes, it's possible. Return id_article in the SELECT list, instead of title, and wrap that whole query in parens to make it an inline view, and then select from that, and a join to the articles table to get the associated title.
For example:
SELECT b.title
, c.tag_count
FROM ( SELECT a.id_article
, COUNT(at.id_tag) tag_count
FROM articles a
JOIN articles_tags at ON a.id_article = at.id_article
JOIN tags t ON t.id_tag = at.id_tag
GROUP BY a.id_article
) c
JOIN articles b
ON b.id_article = c.id_article
ORDER BY c.tag_count DESC
You can also evaluate whether you really need the articles table included in the inline view. We could do a GROUP BY at.id_article instead.
I think this returns an equivalent result:
SELECT b.title
, c.tag_count
FROM ( SELECT at.id_article
, COUNT(at.id_tag) tag_count
FROM articles_tags at
JOIN tags t ON t.id_tag = at.id_tag
GROUP BY at.id_article
) c
JOIN articles b
ON b.id_article = c.id_article
ORDER BY c.tag_count DESC

MySQL - match all tags rather than any

I have an SQL setup akin to the following:
ARTICLES
id (PK)
name
TAGS
id (PK)
tag
...and a third table logging associations between the two, since there can be multiple tags to each article:
ARTICLE_TAG_ASSOCS
id (PK)
article_id (FK)
tag_id (FK)
Via this question I managed to construct a query that would find articles that were tagged with at least one of a number of tags, e.g.
SELECT articles.*
FROM articles
JOIN article_tag_assocs ata ON articles.id = ata.article_id
JOIN tags ON tags.id = ata.tag_id
WHERE tags.tag = 'budgie' OR tags.tag = 'parrot';
Question: How can I alter the above to find articles that match ALL tags, i.e. both 'budgie' and 'parrot', not just one?
Clearly modifying the logic to
WHERE tags.tag = 'budgie' && tags.tag = 'parrot';
...is logically flawed, since MySQL is considering each tag in isolation, one at a time, but hopefully you get what I mean.
There are several workable approaches.
One approach is to perform separate JOIN operations for each tag. For example:
SELECT articles.*
FROM articles
JOIN article_tag_assocs ata
ON ata.article_id = articles.id
JOIN tags ta
ON ta.id = ata.tag_id
AND ta.tag = 'budgie'
JOIN article_tag_assocs atb
ON atb.article_id = articles.id
JOIN tags tb
ON tb.id = atb.tag_id
AND tb.tag = 'parrot'
Note that this can return "duplicate" rows if a given articles is associated to the same tag value more than once. (Adding the DISTINCT keyword or a GROUP BY clause are ways to eliminate the duplicates.)
Another approach, if we are guaranteed that a given article has no duplicate tag values, is to use an inline view to get the list of article_id that are associated with both tags, and then JOIN that set to the articles table. For example:
SELECT a.*
FROM ( SELECT ata.article_id
FROM article_tag_assocs ata
JOIN tags t
ON t.id = ata.tag_id
WHERE t.tag IN ('budgie','parrot')
GROUP BY ata.article_id
HAVING COUNT(1) = 2
) s
JOIN articles a
ON a.id = s.article_id
Note that the literal "2" in the HAVING clause matches the number of values in the predicate on the tag column. The inline view (aliased as s) returns a distinct list of article_id, and we can join that to the articles table.
This approach is useful if you wanted to match, for example, at least three out of four tags. We could use lines like this in the inline view query.
WHERE t.tag IN ('fee','fi','fo','fum')
HAVING COUNT(1) >= 3
Then, any article that matched at least three of those four tags would be returned.
These aren't the only ways to return the specified result, there are several other approaches.
As Roland's answer pointed out, you can also do something like this:
FROM articles a
WHERE a.id IN ( <select article id values related to tag 'parrot'> )
AND a.id IN ( <select article id values related to tag 'bungie'> )
You could also use an EXISTS clause with a correlated subquery, though this approach doesn't usually perform as well with large sets, due to the number of executions of the subquery
FROM articles a
WHERE EXISTS ( SELECT 1
FROM article_tag_assocs s1
JOIN tags t1 ON t1.tag = 'bungie'
WHERE s1.article_id = a.id
)
AND EXISTS ( SELECT 1
FROM article_tag_assocs s2
JOIN tags t2 ON t2.tag = 'parrot'
WHERE s2.article_id = a.id
)
NOTE: in this case, it is possible to reuse the same table aliases within each subquery, because it doesn't lead to ambiguity, though I still prefer distinct aliases because the table aliases show up in the EXPLAIN output, and the distinct aliases make it easier to match the rows in the EXPLAIN output to the references in the query.)
What about this?
Will this give bad performance like EXISTS for large data sets?
This query is to check which rows of 'a1' table has some specified tags and not has some other specified tags
SELECT * FROM a1 WHERE a1.id IN
(SELECT taggables.taggable_id FROM taggables WHERE taggables.taggable_type = 'a1' AND taggables.tag_id IN (1))
AND a1.id NOT IN
(SELECT taggables.taggable_id FROM taggables WHERE taggables.taggable_type = 'a1' AND taggables.tag_id IN (2))
ORDER BY a1.file_count DESC LIMIT 0, 5

Select from 3 tables with one-to-many relation

I have a question.In my database I have 3 tables:
Articles:
id title content date
Tags:
id name
Tags_in_news:
id news_id tag_id
Where news_id is foreign key for news table and tag_id is foreign key for tag table...How to select the articles and all tags attached to them?
I create a query but it select a news for each tag:
SELECT * FROM articles join tags_in_news
ON articles.id = tags_in_news.news_id
join tags on tags.id = tags_in_news.tag_id
ORDER BY date DESC
Try GROUP BY article and grouping tags as comma separated value
something like this:
SELECT
date, a.title, GROUP_CONCAT(DISTINCT t.name) as tags_attached
FROM articles a
JOIN tags_in_news tin ON a.id = tin.news_id
JOIN tags t ON t.id = tin.tag_id
GROUP BY a.id
ORDER BY date DESC
Your query is pretty close, and since you are doing joining it will list all the matching rows and you will get multiple rows for article per tag.
In mysql there is a function called group_concat() which you can use along with group by so that all the tags associated with an article is concat by a comma and then display it for each article.
select
a.title,
a.content,
a.date,
group_concat(t.name) as name
from tags_in_news tin
inner join article a on a.id = tin.news_id
inner join tags t on t.id = tin.tag_id
group by a.id
DEMO

mysql - using joins to get several informations

I have the following tables:
TABLE article
article_id
title
article_text
user_id
TABLE tag_article
tag_article_id
tag_id
article_id
TABLE tag
tag_id
tag
TABLE user
user_id
What I want is to search all articles that have a string in the title OR in the content OR on a tag.
My current query is the following:
SELECT article.article_id,article.title,user.user_id,article.article_text,
FROM tag,user,article
WHERE (article_text LIKE ? OR title LIKE ? OR tag LIKE ?)
AND article.user_id=user.user_id
GROUP BY article.article_id
ORDER BY article_id DESC
First i woud advise you to use explcit joins since they are much easier to read and less error prone (like missing the join between article and tag you just did):
Second, unless you have %'s around your strings, you should add them in the query:
SELECT a.article_id, a.title, u.user_id, a.article_text,
FROM article a
INNER JOIN user u ON a.user_id = u.user_id
LEFT JOIN tag_article ta ON ta.article_id = a.article_id
LEFT JOIN tag t ON t.tag_id = ta.tag_id
WHERE a.article_text LIKE '%YOURSTRING%'
OR a.title LIKE '%YOURSTRING%'
OR t.tag LIKE '%YOURSTRING%'
GROUP BY a.article_id
ORDER BY a.article_id DESC
I'm using LEFT JOIN's, because in the case you have articles without tags, it will still search in the article columns.

MySQL - Select by some "tags", include all tags

I've successfully managed to fetch articles filtering by matching tags in an AND manner.
This is my current code:
SELECT *
FROM articles a
JOIN article_tags a_t ON a_t.article_id = a.id
LEFT JOIN tags t ON t.id = a_t.tag_id
WHERE t.caption IN ('fire', 'water', 'earth')
HAVING COUNT(DISTINCT t.caption) = 3
Where:
articles are the articles I want to fetch, with id, title, etc…
tags are the list of tags, with id and caption
article_tags a relationship table, with article_id and tag_id
Now The problem is that after matching, I want to retrieve all the tags that each article has. Even if they are matched by 3 different ones, one may have 5 tags, other 4 tags, and I want them included in each row. Something like "tag,tag,tag" or whatever I can parse, in some "tags" column.
Any ideas? I can't find a way around it...
You need to join your query as a subquery with a query that returns all the tags and combines them with GROUP_CONCAT().
select a.*, GROUP_CONCAT(DISTINCT t.caption) tags
from (select distinct a.*
from articles a
JOIN article_tags a_t on a_t.article_id = a.id
JOIN tags t on t.id = a_t.tag_id
WHERE t.caption IN ('fire', 'water', 'earth')
GROUP BY a.id
HAVING COUNT(DISTINCT t.caption) = 3) a
JOIN article_tags a_t on a_t.article_id = a.id
JOIN tags t on t.id = a_t.tag_id
GROUP BY a.id
BTW, there's no reason to use LEFT JOIN in your query, because you only care about rows with matches in tags.
I also wonder about the need for DISTINCT in the COUNT() -- do you really allow multiple tag IDs with the same caption?