Count(*) won't display when result is 0 when using group by - mysql

Sorry I couldn't think of a better title...
So I have this database where I have a table 'bibitems' (basicly texts) 'tags' (the tags the text is filed under) and 'taglinks' (Associative entity between a bibitem and a tag)
So on my page I want to display every tag, directly followed by the amount of documents that are filed under that tag
so if 2 documents are under 'java' it's say
java (2)
SELECT
tags.id,
naamnl,
COUNT(*) AS 'count'
FROM
tags,
bibitems,
taglinks
WHERE
bibitems.id=taglinks.item_id
AND tag_id=tags.id
GROUP BY
naamnl
This works well when there are documents filed under a certain tag.
But when there's a tag that's unused so far, it won't pop up.
I want it to display java (0) in that case

SELECT tags.id,
tags.naamnl,
COUNT(bibitems.id) AS 'count'
FROM tags
LEFT
OUTER
JOIN taglinks
ON taglinks.tag_id = tags.id
LEFT
OUTER
JOIN bibitems
ON bibitems.id = taglinks.item_id
GROUP
BY tags.id,
tags.naamnl
;
or
SELECT tags.id,
tags.naamnl,
( SELECT COUNT(*)
FROM bibitems
WHERE bibitems.id IN
( SELECT taglinks.item_id
FROM taglinks
WHERE taglinks.tag_id = tags.id
)
)
FROM tags
GROUP
BY tags.id,
tags.naamnl
;
(Take your pick.)

Read on the difference between INNER and OUTER joins.
SELECT tag.id, naamnl, COUNT(taglink.tag_id) AS 'count'
FROM tag
LEFT OUTER JOIN taglink ON taglink.tag_id=tag.id
LEFT OUTER JOIN bibitem ON bibitem.id=taglink.item_id
GROUP BY tag.id, naamnl

Something like thisshould do it. (It is difficult since you did not provide the full table schemas.)
SELECT
i.naamnl,
COALESCE(t.Count, 0) AS Count
FROM
bibitems i
LEFT JOIN
(
SELECT
tag_id,
COUNT(*) AS Count
FROM
taglinks
GROUP BY
item_id
) t
ON
t.tag_id = i.tag_id

Related

MySQL Query show results based on multiple filters/tags

This has been asked in different ways before, but I can't seem to get something that works for what I need exactly.
The goal here is to make a search query that returns Photos based on tags that are selected. Many tags can be applied to the filter simultaneously, which would need to make it so that the query only returns photos that have ALL of the tags selected. Think of any major web shop where you are narrowing down results after performing a basic keyword search.
Table1: Photos
ID|Title|Description|URL|Created
Table2: PhotosTagsXref
ID|PhotoId|TagId
Table3: PhotosTags
ID|Title|Category
What I have:
SELECT p.* FROM `PhotosTagsXref` AS pt
LEFT JOIN `Photos` AS p ON p.`ID` = pt.`PhotoId`
LEFT JOIN `PhotosTags` AS t ON pt.`TagId` = t.`ID`
WHERE p.`Description` LIKE "%test%" AND
????
GROUP BY p.`ID`
ORDER BY p.`Created` DESC LIMIT 20
The ???? is where I've tried a bunch of things, but stumped. Problem is I can easily find a result set that contains photos with one tag or another, but if applying 2, 3, or 4 tags we'd need to only return photos that have entries for all of those tags in the database. I think this will involve combining result sets but not 100% sure.
Example:
Photo 1 Tags: Blue, White, Red
Photo 2 Tags: Blue
Searching for a photo with tags of 'blue' returns both photos, searching for a photo with tags of 'blue' and 'white' returns only Photo 1.
Supposing the requested set of tags is (red,blue) you can do:
SELECT * FROM `Photos`
WHERE `Description` LIKE "%test%"
AND `ID` IN (
SELECT pt.`PhotoId` FROM `PhotosTagsXref` AS pt
JOIN `PhotosTags` AS t ON pt.`TagId` = t.`ID`
WHERE t.Title in ('red','blue') /* your set here */
GROUP BY pt.`PhotoId` HAVING COUNT(DISTINCT t.`TagId`)=2 /* # of tags */
)
ORDER BY `Created` DESC LIMIT 20
Apparently, the tag set needs to be created dynamically, as well as its count.
Note: I'm counting DISTINCT TagIDs because I don't know your table's constraints. If PhotosTagsXRef had a PK/UNIQUE (PhotoId,TagId) and PhotosTags had a PK/UNIQUE (TagId), then COUNT(*) would suffice.
Admittedly a bit ugly. But assuming that PhotosTags.Category has the 'Blue', 'White', etc, try something along this line.
SELECT p.*
From `Photos` AS p
WHERE p.`Description` LIKE "%test%" AND
AND Exists
( Select 1 FROM `PhotosTagsXref` AS pt
Inner JOIN `PhotosTags` AS t ON pt.`TagId` = t.`ID`
Where pt.`PhotoId` = p.`ID`
And t.Category = 'FirstCatToSearch'
)
AND Exists
( Select 1 FROM `PhotosTagsXref` AS pt
Inner JOIN `PhotosTags` AS t ON pt.`TagId` = t.`ID`
Where pt.`PhotoId` = p.`ID`
And t.Category = 'SecondCatToSearch'
)
AND Exists
( ...
)
...
SELECT p.* FROM `PhotosTagsXref` AS pt
LEFT JOIN `Photos` AS p ON p.`ID` = pt.`PhotoId`
LEFT JOIN `PhotosTags` AS t ON pt.`TagId` = t.`ID`
inner join (select PhotoId from PhotosTagsXref
LEFT JOIN `PhotosTags` AS t
ON pt.`TagId` = t.`ID`
where (t.title = 'cond 1' or t.title = 'cond 2' ...)
--where t.title in (list condition) **this works as well**
having count(1) = (count of conditions) ) filter
on filter.photoID = pt.PhotoID
WHERE p.`Description` LIKE "%test%"
GROUP BY p.`ID`
ORDER BY p.`Created` DESC LIMIT 20
That should work, I made some assumptions on what column to use for the filter and joins, you may need to retool...the inner join functions as a filter and should pull out only records that have the number of matches equal to the total of the number of matches submitted. Now you just need a language to plug in those conditions and condition count values.

mysql GROUP_CONCAT DISTINCT multiple columns

I have a tag field for a blog posts. tags have unique id but their displayName might be duplicated. What I want is a query that selects posts and in all_tags field we get couples of (id,displayName) is this way:
id1,name1;id2,name2;id3,name3
My query looks like:
select ....
CONCAT_WS(';', DISTINCT (CONCAT_WS(',',tags.id,tags.displayName))) AS all_tags
Join ...post content ...
Join ...post_tags ...
Join ...tags ...
ORDER BY posts.id
This line causes problem:
CONCAT_WS(';', DISTINCT (CONCAT_WS(',',tags.id,tags.displayName))) AS all_tags
How should I modify it?
Some people use an inner (SELECT .. FROM) but as I have heard, it is so inefficien
SELECT `posts`.*,`categories`.*,`creators`.*,`editors`.*
CONCAT_WS(';', DISTINCT GROUP_CONCAT(CONCAT_WS(',',tags.id,tags.displayName))) AS all_ids
FROM (`posts`)
LEFT JOIN `languages` ON `posts`.`language_id`=`languages`.`id`
LEFT JOIN `users` as creators ON `posts`.`creatorUser_id`=`creators`.`id`
LEFT JOIN `users` as editors ON `posts`.`lastEditorUser_id`=`editors`.`id`
LEFT JOIN `userProfiles` as editors_profile ON `editors`.`profile_id`=`editors_profile`.`id`
LEFT JOIN `categories` ON `posts`.`category_id`=`categories`.`id`
LEFT JOIN `postTags` ON `postTags`.`post_id`=`posts`.`id`
LEFT JOIN `tags` ON `postTags`.`tag_id`=`tags`.`id`
LEFT JOIN `postTags` as `nodetag_checks` ON `nodetag_checks`.`post_id`=`posts`.`id`
LEFT JOIN `tags` as `tag_checks` ON `nodetag_checks`.`tag_id`=`tag_checks`.`id`
WHERE ( 9 IN(`tag_checks`.`id`,`tag_checks`.`cached_parents`) OR 10 IN(`tag_checks`.`id`,`tag_checks`.`cached_parents`) OR 11 IN(`tag_checks`.`id`,`tag_checks`.`cached_parents`))
GROUP BY `posts`.`id` ORDER BY `posts`.`created` desc LIMIT 0, 20
Try this:
GROUP_CONCAT(
DISTINCT CONCAT(tags.id,',',tags.displayName)
ORDER BY posts.id
SEPARATOR ';'
)
As advised by #Willa, I add my comment as an anwser :
GROUP_CONCAT allows you to concat multiple fields :
GROUP_CONCAT(tags.id, ',', tags.displayName)
The only difference with Stephan's answer is in case your code allows the same tag to be affected several times to one post OR if you JOIN sequence leads you to multiselect the same record in the tag table. In those case, my solution will return the same tags multiple times.
On top of #Stephan's great answer, to prevent the same content showing up multiple times due to multiple JOIN's in your query but you don't want the id to show in the output...
GROUP_CONCAT(
DISTINCT
tags.displayName,
'||__', tags.id, '__||'
SEPARATOR '\n'
)
And then loop over the result in the end removing everything between ||__ and __|| .
This example is for php:
$data = preg_replace("/\|\|__.*__\|\|/", '', $data);

Select records that match a field, but order them by a different field

my tables look like this:
tags: id, name, description
tag_relations: tag, item
item references the id of another table and tag references the id of the tags table.
So I'm trying to select the most used tags:
SELECT t.*, COUNT(r.item) AS item_count
FROM tag_relations as r
INNER JOIN tags as t ON r.tag = t.id
GROUP BY t.id
ORDER BY item_count
which works, but if I add
WHERE t.id = ?
the item_count is always 1...
Is there any way I could still have the global tag count with a select statement that selects only 1 tag or a specific set of tags?
Sql fiddle at
http://www.sqlfiddle.com/#!2/ba97d/1
SELECT name,count(item) as counter_item
FROM tag_relations
INNER JOIN tags ON
tag_relations.tag =tags.id
order by counter_item
the line
where tags.id=1
Can be added if needed
I don't have access to MySQL, but I do have access to Microsoft SQLServer. I realize your tags specify mysql. Even so, the query you presented fails in SQLServer with error
Msg 8120, Level 16, State 1, Line 1
Column 'tags.name' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
... because the select t.* is not included in the group by clause.
Anyways, to address your specific question you can derive a global number while still selecting a specific record using cross join...
select
t.*
, vTagRelations.GlobalCountOfTagRelations
, vTags.GlobalCountOfTags
from
tags t
cross join (select
count(tag_relations.tag) as GlobalCountOfTagRelations
from
tag_relations) vTagRelations
cross join (select
count(tags.id) as GlobalCountOfTags
from
tags) vTags
where
t.id = 2
In SQLite, using sub-query:
SELECT *, (SELECT COUNT() FROM tag_relations WHERE tag=tags.id) AS item_count FROM tags WHERE id=?;

Exclusion of rows with many-to-many relationships

I have three tables: posts, tags and posts_has_tags (which facilitate the many-to-many relationship between posts and tags). A post can have any number of tags.
The 'posts' table has the following columns:
idposts
text
The 'tags' table has these:
idtags
name
As for the 'posts_has_tags' table:
posts_idposts
tags_idtags
What I can't do is come up with a query to select all posts except for those which have a tag (or tags) with a certain value in the 'name' column assigned to them. It seems that it should contain a "NOT EXISTS", but I can't really fully wrap my head around it.
Thank you for your help in advance.
EDIT:
Also, is it possible to restrict the result set to certain tags at the same time? For example:
tags for exclusion: a, b
tags for inclusion: c
Post with tags 'a', 'f' doesn't make it into result set (because none if it's tags are included).
Post with tags 'a', 'b', 'c' doesn't make it into result set either (because it's 'a' and 'b' are excluded tags).
Post with tags 'c', 'f' does make it into result set, because 'c' is the included tag.
FINAL EDIT
I finally found a solution that seems to be working and reasonably well-performing: http://www.mysqldiary.com/a-many-to-many-relationship-table-solving-the-exclude-relation-problem/
You can use an anti-join.
SELECT p.*
FROM posts p
LEFT JOIN post_has_tags pt ON (pt.post_id = p.id)
LEFT JOIN tags t ON (t.id = pt.tag_id AND t.name IN ('test','test1','test2'))
WHERE t.id IS NULL
GROUP BY p.id
If you want to force other tags to be included, you do another join.
SELECT p.*
FROM posts p
LEFT JOIN post_has_tags pt ON (pt.post_id = p.id)
LEFT JOIN tags t ON (t.id = pt.tag_id AND t.name IN ('a','b'))
INNER JOIN tags t2 ON (t2.id <> t.id AND t2.id = pt.tag_id AND t2.name IN ('c'))
WHERE t.id IS NULL
GROUP BY p.id
This will prioritize exclusion over inclusion.
If you want to prioritize inclusion then replace the inner join with:
INNER JOIN tags t2 ON (t2.id = pt.tag_id AND t2.name IN ('c'))
SELECT p.*
FROM posts AS p
WHERE NOT EXISTS
( SELECT *
FROM posts_has_tags AS pt
JOIN tags AS t
ON pt.tags_idtags = t.idtags
WHERE pt.posts_idposts = p.idposts
AND t.name = #CertainForbiddenTagName
)
If you have many tag names to forbid, use this instead:
AND t.name IN (List of ForbiddenTagNames)
For your updated 2nd question, just add a similar EXISTS:
AND EXISTS
( SELECT *
...
)
select * from posts where idposts in
(select posts_has_tags.posts_idposts from posts_has_tags
join tags on tags.idtags = posts_has_tags.tags_idtags
where tags.name not in ('value1','value2',...))
I finally found a solution that seems to be working and reasonably well-performing: http://www.mysqldiary.com/a-many-to-many-relationship-table-solving-the-exclude-relation-problem/

Mysql Tag Query - Repeated values

I'm working on a tag-based search. The user can search one tag or multiple tags. I've got 3 tables: Content, Content_Tags and Tags. Content_Tags does the link between content and tags. The query has to return all the information of the content retrieved, this includes a concatenated string of all tags from that content.
Using an example from other question I've managed to reach this query:
SELECT content.Name,GROUP_CONCAT(t2.Tag SEPARATOR ' ') FROM content
JOIN content_tags ct1 ON content.ContentID = ct1.ContentID
JOIN tags t1 on ct1.TagID = t1.TagID AND t1.Tag IN('grass','texture')
JOIN content_tags ct2 ON ct2.ContentID = content.ContentID JOIN tags t2 ON ct2.TagID = t2.TagID GROUP BY content.ContentID;
The query works fine when searching for 1 tag, but using IN('grass','texture') the GROUP_CONCAT will return 'grass grass texture texture' (repeating the tags).
I don't know MySQL this deep, how could I fix this problem?
SELECT content.name,
(
SELECT GROUP_CONCAT(cta.tag)
FROM content_tags cta
WHERE cta.contentID = ct.contentID
)
FROM content_tags ct
JOIN content c
ON c.contentId = ct.contentId
WHERE ct.tag IN ('grass', 'texture')
GROUP BY
ct.contentId
HAVING COUNT(*) = 2