MySQL Select Where In Many to Many - mysql

I'm having trouble with a SQL query. My schema describes a many to many relationship between articles in the articles table and categories in the categories table - with the intermediate table article_category which has an id, article_id and category_id field.
I want to select all articles which only have the categories with id 1 and 2. Unfortunately this query will also select any articles which have those categories in addition to any others.
For example, this is a sample output from the SQL (with categories shown for descriptive purposes). You can see that while the query selected the article with id of 10, it also selected the article with an id of 11 despite having one extra category.
+-------+------------+
| id | categories |
+-------+------------+
| 10 | 1,2 |
| 11 | 1,2,3 |
+-------+------------+
This is the output that I want to achieve from selecting articles with only categories 1and 2.
+-------+------------+
| id | categories |
+-------+------------+
| 10 | 1,2 |
+-------+------------+
Likewise, this is the output what I want to achieve from selecting articles with only categories 1, 2 and 3.
+-------+------------+
| id | categories |
+-------+------------+
| 11 | 1,2,3 |
+-------+------------+
This is the SQL I have written. What am I missing to achieve the above?
SELECT articles.id
FROM articles
WHERE EXISTS (
SELECT 1
FROM article_category
WHERE articles.id = article_id AND category_id IN (1,2)
GROUP BY article_id
)
Many thanks!

Assuming you want more than just the article's id:
SELECT a.id
,a.other_stuff
FROM articles a
JOIN article_category ac
ON ac.article_id = a.id
GROUP BY a.id
HAVING GROUP_CONCAT(DISTINCT ac.category_id ORDER BY ac.category_id SEPARATOR ',') = '1,2'
If all you want is the article's id then try this:
SELECT article_id
FROM article_category
GROUP BY article_id
HAVING GROUP_CONCAT(DISTINCT category_id ORDER BY category_id SEPARATOR ',') = '1,2'
See it in action at http://sqlfiddle.com/#!2/9d213/4
Should also add that the advantage of this approach is that it can support the checking of any number of categories without having to change the query. Just make '1,2' a string variable and change what gets passed into the query. So, you could just as easily search for articles with categories 1, 2, and 7 by passing a string of '1,2,7'. No additional joins are needed.

You can left join category_id on category.id and then GROUP_CONCAT to get all categories, like you wrote in explanation (1st table) and than using HAVING match with any set you like ( '1,2' from example)
also with that approach you can easily make this query dynamic with php or any other language
SELECT articles.id
FROM articles
WHERE EXISTS (
SELECT GROUP_CONCAT(c.id) AS grp
FROM article_category
LEFT OUTER JOIN categories AS c ON c.id = article_category.category_id
WHERE articles.id = article_id
GROUP BY article_id
HAVING grp = '1,2'
)

Please Use Below Query You can do the Thing by Using Simple Query.
SELECT a.id, a.name
FROM articles a, categories c, articles_categories ac
WHERE
a.id = ac.article_id AND c.id = ac.category_id
AND c.id = 1 OR c.id = 2;
NOTE- If You have Many to many Relationship between two Tables Remove ID from the article_category table and make composite primary key Using article_id and category_id.
Thank you.

Maybe, something like:
select distinct article_id from article_cathegory
where category_id in (1,2)
minus
select distinct article_id from article_cathegory
where category_id not in (1,2)

Looks like simple solution for this could be the following:
SELECT
ac.article_id
, SUM(ac.category_id IN (1, 2)) AS nb_categories
, COUNT(ac.category_id) AS nb_all_categories
FROM
article_categories ac
GROUP BY
ac.article_id
HAVING
nb_categories=2 AND nb_all_categories=2
Here I count how many required categories we have and also count how many categories we have in total. We only need exactly 2 categories, so both required and total should be equal to 2.
This is quite flexible and for adding more categories just change categories list and numbers in HAVING statement.

SELECT articles.id
FROM articles
INNER JOIN articles_category ac ON articles.id = ac.article_id
WHERE articles.id IN (
SELECT ac1.article_id
FROM article_category ac1
WHERE ac1.category_id = 1;
)
AND ac.article_id = 2;
AND articles.id NOT IN (
SELECT ac2.article_id
FROM article_category ac2
WHERE ac2.category_id NOT IN (1, 2)
)
Far from the prettiest one I have written.
Basically, it limits first by ID that have a category id of 1, then it makes sure the records have a category of 2 as well, finally, it makes sure that it does not have any other categories

I like to approach these queries using group by and having. Here is an example:
select ac.article_id
from article_category ac
group by ac.article_id
having sum(case when category_id = 1 then 1 else 0 end) > 0 and
sum(case when category_id = 1 then 2 else 0 end) > 0;
Each condition in the having clause is testing for the presence of one of the categories.
I find that this approach is the most flexible for answering many different types of "set-within-sets" problems.
EDIT:
A slight variation on the above might be easier to generate:
having sum(category_id in (1, 2, 3)) = count(*) and
count(*) = 3
This will work assuming there are no duplicates in the data. You need to update the 3 to be the number of items in the in list.

to help just without changing your query very much, i think the logic has a bug. you dont want articles where a categegory 1,2 exists. You need articles where does not exist categories different than 1 and 2.
thanks & regards

In SQL Server I would do it with an INTERSECT and an EXCEPT. In MySQL, try this:
SELECT DISTINCT article_id
FROM article_category
WHERE category_id=1
AND article_id IN
(SELECT article_id
FROM article_category
WHERE category_id=2)
AND article_id NOT IN
(SELECT article_id
FROM article_category
WHERE category_id NOT IN (1,2))

Use this SQL query.
SELECT articles.id
FROM articles
WHERE articles.id in (
SELECT *
FROM article_category,articles
WHERE article_category.articles.id = articles.article_id AND article_category.category_id IN (1,2)
)

Related

remove some data from many to many result

I have issue with my many to many database .
I have simple blog system
post |
category |
post_has_category
when I used join to select the posts has not in category "3" ,it's filed (3 is example entry )
because in my database store post category like this
post(id):1
category(id):3
post_has_category:
post_id category_id
1 3
1 4
after I select the post don'n in cat 3 , the post 1 is selected but I don't want it , because it's belongs to cat 3 in other row ..
We can use a subquery to identify all post_id in which a category_id of 3 appeared. Then, we can left join the original table to this subquery and retain only those post_id which did not match to any of the offending ones in the subquery.
SELECT p1.*
FROM posts p1
LEFT JOIN
(
SELECT DISTINCT post_id
FROM posts
WHERE category_id = 3
) p2
ON p1.post_id = p2.post_id
WHERE p2.post_id IS NULL
Note that DISTINCT in the subquery should be unnecessary if a given post_id can only be associated with a given category_id once.

How to properly count rows in table2 related to items in table1 with clause non related to table1

I have simple magazine, and have tables with posts, comments, categories, etc. When listing single category, I want to have sum of comments per post in a listing, but that number is just wrong and it is driving me crazy. Note that single post can be in multiple categories.
Here are the simple tables structures
posts
id | title | categoryid | content | published
---------------------------------------------
comments
id | postid | comment
---------------------
category_rel
postid | categoryid
-------------------
categories
id | category
-------------
I use following sql (simplified to this example):
SELECT posts.*, categories.id AS categoryid,
categories.category AS categorytitle,
COUNT(comments.postid) AS CNT
FROM posts
LEFT JOIN comments ON posts.id = comments.postid
INNER JOIN category_rel ON posts.id = category_rel.postid
INNER JOIN categories ON category_rel.categoryid = categories.id
WHERE posts.published=1
GROUP BY posts.id;
This statement is giving me wrong results, sometning like it's cumulating number of categories post is member of and multiplying with actual number of comments. If I remove category part of SQL (which is not good, I need category Id and name) I receive proper values:
SELECT posts.*, COUNT(comments.postid) AS CNT
FROM posts
LEFT JOIN comments ON posts.id = comments.postid
WHERE posts.published=1
GROUP BY posts.id;
To be more specific:
One of posts have 1 comment and it is member of 7 categories. value CNT is going to 7, not 1.
Any idea how to change first SQL to get proper values?
You want to count the comments per post - not per category. So one way of achieving this would be to do the count first (in a subselect as MySQL has no CTE so far) and then join the results into category table:
SELECT countpost.*, categories.id AS categoryid,
categories.category AS categorytitle
FROM
-- subselect post and comment count
(
SELECT posts.*, count(comments.postid) as CNT FROM posts
LEFT JOIN comments ON posts.id = comments.postid
WHERE posts.published = 1
GROUP BY posts.id
) as countpost
-- join category table
INNER JOIN category_rel ON countpost.id = category_rel.postid
INNER JOIN categories ON category_rel.categoryid = categories.id ;
See this fiddle: http://sqlfiddle.com/#!9/f9c6f/1

MYSQL Query search in relationship

For the sake of clarity and this question i will rename the tables so it is a bit clearer for everybody and explain what i want to achieve:
There is an input form with options that return categories ID's. If a 'Product' has 'Category', i want to return/find the 'Product' which lets say has multiple categories(or just 1) and all of its categories are inside the array that is passed from the form.
Products table
ID Title
1 Pizza
2 Ice Cream
Categories table
ID Title
1 Baked food
2 Hot food
ProductsCategories table
ID ProductId CategoryId
1 1 1
2 1 2
So if i pass [1,2] the query should return Product with id 1 since all ProductsCategories are inside the requested array, but if i pass only 1 or 2, the query should return no results.
Currently i have the following query which works, but for some reason if i create a second Product and create a ProductCategory that has a CategoryId same as the first product, the query returns nulll...
SELECT products.*
FROM products
JOIN products_categories
ON products_categories.product_id= products.id
WHERE products_categories.category_id IN (1, 2)
HAVING COUNT(*) = (select count(*) from products_categories pc
WHERE pc .product_id = products.id)
All help is deeply appretiated! Cheers!
In order to match all values in IN clause, you just need to know in addition the number of passed categories which you must use it in HAVING clause:
SELECT
p.*,
GROUP_CONCAT(c.title) AS categories
FROM
Products p
INNER JOIN ProductsCategories pc ON pc.productId = p.ID
INNER JOIN Categories c ON c.ID = pc.categoryId
WHERE
pc.categoryId IN (1,2)
GROUP BY
p.id
HAVING
COUNT(DISTINCT pc.categoryId) = 2 -- this is # of unique categories in IN clause
So in case IN (1,2) result is:
+----+-------+---------------------+
| id | title | categories |
+----+-------+---------------------+
| 1 | Pizza | Baked Food,Hot Food |
+----+-------+---------------------+
1 row in set
In case IN (1,3) result is Empty set (no results).
#mitkosoft, thanks for your answer, but sadly the query is not producing the needed results. If the product's categories are partially in the passed categories the product is still returned. Additionally i might not know how many parameters are sent by the form.
Luckily I managed to create the query that does the trick and works perfectly fine (at least so far)
SELECT products.*,
COUNT(*) as resultsCount,
(SELECT COUNT(*) FROM products_categories pc WHERE pc.product_id = products.id) as categoriesCount
FROM products
JOIN products_categories AS productsCategories
ON productsCategories.product_id= products.id
WHERE productsCategories.category_id IN (7, 15, 8, 1, 50)
GROUP BY products.id
HAVING resultsCount = categoriesCount
ORDER BY amount DESC #optional
That way the query is flexible and gives me exactly what I needed! - Only those products that have all their categories inside the search parameters(not partially).
Cheers! :)

select count items each group

I have two tables category and adverts, i need to select all categories and the number of adverts it has in the adverts table that has at least count greater than zero
the category table
cat_id | Name
----------------
1 | Toys
2 | fashion
3 | electronics
The adverts table
cat_id | title
----------------
1 | a
2 | b
2 | c
1 | d
2 | e
what i expect
cat_id | count | Name
-----------------------
1 |2 | a
2 |3 | b
The query i tried
Select
c.name, c.cat_id,c.parent_id, #count:= (Select Count(av.cat_id) From adsview av Where av.cat_id = c.cat_id)
from
category c WHERE #count > 0
i am getting and empty result, what am i doing wrong?
If you want to make sure that the cat_id from category table are in adverts table you need to join as
select
c.cat_id,
c.Name,
count(a.cat_id) as `count`
from category c
join adverts a on a.cat_id = c.cat_id
group by c.cat_id ;
select cat_id, count(*)
from adverts
group by cat_id;
So the mySQL query engine will grab every single row from the adverts table, it'll put them into neat piles where all rows in the pile have the same category, it'll count the number of rows in each pile, and then it'll return to you a result row for each pile with the pile's id and the number of rows.
Now lets add something: we want to also get the category's name. So we indicate that in the select clause, and add a join to the from clause. The join says "for every row in table a, consider it alongside every row in table b. if some condition holds, put this combined row into the from set". You can see that joins are actually quite slow in SQL (relatively).
select c.cat_id, count(*) as count, c.name
from adverts as a join categories as c on a.cat_id = c.cat_id
group by c.cat_id;
Note also that I've aliased the tables as a and c respectively, so as to remove the ambiguity over the column name cat_id (otherwise the mySQL query engine may get confused).
You can try this, mate:
SELECT
c.cat_id,
COUNT(a.cat_id) AS count,
a.title
FROM
category c
LEFT JOIN adverts a ON a.cat_id = c.cat_id
GROUP BY
c.cat_id
HAVING
count > 0;
or this:
SELECT
c.cat_id,
COUNT(a.cat_id) AS count,
a.title
FROM
category c
INNER JOIN adverts a ON a.cat_id = c.cat_id
GROUP BY
c.cat_id;
You have to use group by function like below
select cat_id, count(*) as count
from adverts
group by cat_id;

Ordering items with matching tags by number of tags that match

I'm trying to figure out how to order items with matching tags by the number of tags that match.
Let's say you have three MySQL tables:
tags(tag_id, title)
articles(article_id, some_text)
articles_tags(tag_id, article_id)
Now let's say you have four articles where:
article_id = 1 has tags "humor," "funny," and "hilarious."
article_id = 2 has tags "funny," "silly," and "goofy."
article_id = 3 has tags "funny," "silly," and "goofy."
article_id = 4 has the tag "completely serious."
You need to find all articles related to article_id = 2 by at least one matching tag, and return the results in order of the best matches. In other words, article_id = 3 should come first, with article_id = 1 second, and article_id = 4 should not show up at all.
Is this something that's doable in SQL queries or alone, or is this better suited for something like Sphinx? If the former, what kind of query should be done, and what sort of indexes should be created for the most performant results? If the latter, please do expand.
Try something like this:
select article_id, count(tag_id) as common_tag_count
from articles_tags
group by tag_id
where tag_id in (
select tag_id from articles_tags where article_id = 2
) and article_id != 2
order by common_tag_count desc;
Syntax may need a little tweaking for MySQL.
or this one that actually works: ;-)
SELECT at1.article_id, Count(at1.tag_id) AS common_tag_count
FROM articles_tags AS at1 INNER JOIN articles_tags AS at2 ON at1.tag_id = at2.tag_id
WHERE at2.article_id = 2
GROUP BY at1.article_id
HAVING at1.article_id != 2
ORDER BY Count(at1.tag_id) DESC;
Something resembling:
SELECT a.*
FROM articles AS a
INNER JOIN articles_tags AS at ON a.id=at.article_id
INNER JOIN tags AS t ON at.tag_id = t.id
WHERE t.title = 'funny' OR t.title = 'goofy' OR t.title = 'silly' AND a.id != <article_id>
GROUP BY a.id
ORDER BY COUNT(a.id) DESC
With just the usual indexes, assuming articles_tags has PK of (article_id, tag_id), and an index on tags.title