Display only best result out of one-to-many select - mysql

I'm having trouble getting my head around how to handle something I've thought of in SQL. I have bookmarks, and comments that the users posted about them. I'm using a single table for all comments. So we have a one-to-many relationship between the bookmarks and the comments.
There is another table, acting as the middle-man, linking each bookmark to all its comments.
There are two types of comments. Suggested titles for the bookmark, and general comments. Suggested titles have both a title and a description, while general comments have only a description. There's also a rating system for the suggested titles, so that the home page can pick the top-rated title for each bookmark to display.
So, main things to make clear. There's the Bookmarks table with BID and URL, and also the Comments table with CID, Title, Comment, and Rating. The BooksNComms is the connecting table between them.
SELECT comments.title, comments.comment
FROM comments
INNER JOIN booksncomms ON comments.cid=booksncomms.cid
WHERE booksncomms.bid=1
AND comments.title is not null
ORDER BY comments.rating
LIMIT 0, 1;
The above works in getting the best Title and Description (Comment) for a certain BID. What I want to do is make the above work for, say, the 10 newest bookmarks.
SELECT bookmarks.url, comments.title, comments.`comment`, comments.rating
FROM bookmarks
INNER JOIN booksncomms
ON bookmarks.bid=booksncomms.bid
INNER JOIN comments
ON comments.cid=booksncomms.cid
JOIN (
SELECT bookmarks.bid
FROM bookmarks
ORDER BY bookmarks.datecreated DESC
LIMIT 1
)
AS a
ON a.bid=bookmarks.bid
WHERE comments.title IS NOT NULL
ORDER BY bookmarks.url;
The above gives me all titles for the 10 newest bookmarks.
Is there a way I can select only the highest rated title for each of the 10 newest bookmarks?

(OP's own solution, split off from the question)
#LefterisAslanoglou says:
I realized I knew the answer just a few minutes after I posted the question here. It's been bugging me for hours, but it was a simple matter of getting a table that has the best rated title for each bookmark and then joining that to the one with all the titles for the latest bookmarks.
SELECT bookmarks.url, comments.title, comments.comment, comments.rating
FROM bookmarks
INNER JOIN booksncomms ON bookmarks.bid=booksncomms.bid
INNER JOIN comments ON comments.cid=booksncomms.cid
JOIN (
SELECT bookmarks.bid
FROM bookmarks
ORDER BY bookmarks.datecreated DESC
LIMIT 10
) AS a ON a.bid=bookmarks.bid
JOIN (
SELECT comments.cid, MAX(comments.rating)
AS maxrating
FROM comments
INNER JOIN booksncomms ON comments.cid=booksncomms.cid
GROUP BY booksncomms.bid
) AS b ON b.cid=comments.cid
WHERE comments.title IS NOT NULL
ORDER BY bookmarks.datecreated DESC;

Try
SELECT bookmarks.url, comments.title, comments.`comment`, MAX(comments.rating) rating
FROM bookmarks
INNER JOIN booksncomms
ON bookmarks.bid = booksncomms.bid
INNER JOIN comments
ON comments.cid = booksncomms.cid
WHERE comments.title IS NOT NULL
ORDER BY bookmarks.datecreated DESC, bookmarks.url
LIMIT 10

Related

How to query for both posts and post upvotes in MySQL?

I am attempting to model post upvotes and posts in MySQL. Currently I have an elements table for posts and a likes table for upvotes, structured as follows.
likes (id, elementID, googleID)
elements (id, googleID, title, body, type)
I have a URL route that will either return the most recent posts, or the posts with the most upvotes depending on a parameter. I want to query for a set of posts, each listing how many upvotes they have. The website won't display who upvoted, but the database should keep track of this to prevent multiple upvotes.
I tried to do something such as:
SELECT elements.id, elements.googleID, elements.title, likes.id, likes.elementID
FROM elements
INNER JOIN likes
ON elements.id=likes.elementID
This did not work well.
How would I get a set of posts, each showing how many upvotes they have when the upvotes are stored in a separate table?
I want to query for a set of posts, each listing how many upvotes
they have.
You can try this query. I used LEFT JOIN so you can still get the posts without likes.
SELECT E.id
, E.googleID
, E.title
, L.likeCount
FROM elements E
LEFT JOIN (
SELECT elementId
, COUNT(id) AS likeCount
FROM likes
GROUP BY elementId
) L ON L.elementId = E.id
Or (not pretty sure if this will run properly in MySQL)
SELECT E.id
, E.googleID
, E.title
, COUNT(L.id)
FROM elements E
LEFT JOIN likes L ON L.elementID = E.id
GROUP BY E.id
The website won't display who upvoted, but the database should keep
track of this to prevent multiple upvotes
You can create a UNIQUE CONSTRAINT in your LIKES table, for elementID and googleID columns. This will make sure that a googleID can only like one elementID. Otherwise, it will throw a unique constraint violation.
With that, when a user is upvoting a post, you can check in the database first if the user has already an existing record in the LIKES table before inserting one.

MySQL EXISTS and ORDER BY

I have two tables 'Comments' and 'Likes' and I can show all the comments that have been liked and order them by when the comments where added. What I can't seem to do at the moment is order the comments according to when they were liked.
This is what I have at the moment:
SELECT *
FROM comments AS c
WHERE EXISTS (
SELECT *
FROM likes AS l
WHERE c.commentID=l.commentID)
Could anyone help me with the SQL to show the comments in order with the one that was most recently liked first and so on...
Just to add - I only want to show the comment once and avoid showing any comments that have not been liked.
You want to join the tables.
SELECT comments.*
FROM comments JOIN likes ON comments.commentID = likes.commentID
GROUP BY comments.commentID
ORDER BY MAX(likes.date) DESC;
The JOIN makes rows with all of the fields from comments and likes. If you use LEFT JOIN it will include comments that have not been liked, but using a plain JOIN should do what you want.
The GROUP BY collapses rows so you only have one per comment.
The ORDER BY orders the rows by the like date. I used MAX(likes.date) because you will have potentially many like dates for each comment, and you want to choose a specific one. You could try MIN(likes.date) as well, depending on what you're looking for (most recently liked vs first liked).
If you have multiple likes for a given comment, then you need an aggregation, such as:
SELECT c.*
FROM comments c join
(select l.commentId, MIN(likedate) as FirstLikeDate, MAX(likedate) as MaxLikeDate
from likes l
group by l.commentId
) l
on c.commentId = l.CommentId
order by MaxLikeDate desc
Since you only want to show the comments that have like, you can do this:
SELECT * FROM comments INNER JOIN likes USING(commentID) ORDER BY like_date DESC;

Attempting left join with a 'where' that only applies if the join succeeds

I have articles, stored in an article table. Some articles have one or more photos, stored in a photo table with an article_id specified. Some photos are 'deactivated' (photo.active = '0') and should not be retrieved.
I'm trying to get articles for the home page, and one photo for each article with one or more photos. Like so:
SELECT article.id, article.date, article.title, photo.filename_small
FROM (article)
LEFT JOIN photo ON photo.article_id=article.id
WHERE photo.active = '1'
GROUP BY article.id
ORDER BY article.date desc
LIMIT 10
(The "group by" is so that I don't get multiple results for articles with multiple photos. It strikes me as awkward.)
When I have the WHERE photo.active = '1' like that, I only get results with a photo, which defeats the purpose of making the join a left join. The where is only relevant if the join matches the article with a photo, but it's ruling out all articles without active photos. Any ideas?
(Yes, there are similar questions, but I've read a lot of them and am still struggling.)
Try something like
SELECT article.id,
article.date,
article.title,
photo.filename_small
FROM (article) LEFT JOIN
photo ON photo.article_id=article.id
AND photo.active = '1'
GROUP BY article.id
ORDER BY article.date desc
LIMIT 10
Two options.
Put it in the join clause:
LEFT OUTER JOIN photo ON photo.article_id=article.id AND photo.active = 1
Explicitly allow nulls again:
WHERE (photo.active = 1 OR photo.id IS NULL)
The second seems unnecessarily complicated though as you already have the outer join. I'd recommend the first.

Making a user profile activity feed

I need to build an activity feed to go on each users profile page showing what they have been doing on the site.
There is three tables: comments, ratings, users
I want the feed to include the comments and ratings that the user has posted.
in the comments and ratings table it stores the user id of the user who posted it, not the username, so in for each item in the news feed it needs to select from the users table where the user id is the same to retrieve the username.
All the entries in the feed should be ordered by date.
Here is what ive got even though i know it is not correct because it is trying to match both with the same row in the users table.
SELECT comments.date, comments.url AS comment_url, comments.user_id, ratings.date, ratings.url AS rating_url, ratings.user_id, users.id, users.username
FROM comments, ratings, users
WHERE comments.user_id=%s
AND comments.user_id=users.id
AND ratings.user_id=%s
AND ratings.user_id=users.id
ORDER BY ratings.date, comments.date DESC
JOIN. It seems you know that, but here's how:
SELECT * FROM comments LEFT JOIN users ON comments.user_id = users.id
Thus, as far as I can tell, you're trying to order two separate things at the same time. The closest I think I can come up with would be something like:
(SELECT comments.date AS date, users.username AS name, comments.url AS url CONCAT('Something happened: ',comments.url) AS text
FROM comments LEFT JOIN users ON comments.user_id = users.id
WHERE users.id = %s)
UNION
(SELECT ratings.date AS date, users.username AS name, ratings.url AS url CONCAT('Something happened: ',ratings.url) AS text
FROM comments LEFT JOIN users ON comments.user_id = users.id
WHERE users.id = %s)
ORDER BY date DESC LIMIT 0,10
Note that the columns of both parts of the union match up. I'm pretty sure that that is required for something like this to work. That's why I have that CONCAT statement, which lets you build a string that works differently between ratings and comments.

Creating a custom forum, help with building a query that counts posts in a categories etc

So my database setup is fairly simple.
I have a forum_cat table (a forum category) and forum_post table.
The forum_post has a field fk_forum_cat_id which ties each forum post to a category.
Each forum_post also has a field fk_parent_forum_post_id which basically says it belongs to an original post.
Further more, there is a date_added and date_edited field in forum_post.
Now, I am trying to generate the front page for my forum. I want to show a list of forum categories. Each one category should have a post count and the latest post. Could someone give me some direction with a query that does that all in one. I don't want to run 20 separate queries!
If I read your question correctly, you are seeking the category, the count of posts in the category, and the latest post in that category. Perhaps this simplification of laurent-rpnet's answer will do the trick...
SELECT c.forum_cat_id,
COUNT(p.fk_forum_cat_id),
MAX(p.date_added),
(SELECT p2.post_title
FROM forum_post AS p2
WHERE p2.forum_cat_id = c.forum_cat_id
ORDER BY date_added DESC
LIMIT 1)
FROM forum_cat AS c INNER JOIN
forum_post AS p ON p.fk_forum_cat_id = c.forum_cat_id
GROUP BY c.forum_cat_id;
If forum_post primary key is auto-incremented (should be but we never know...), this will return what you need:
SELECT c.forum_cat_id, COUNT(p.fk_forum_cat_id), MAX(p.date_added),
(SELECT p2.post_title FROM forum_post AS p2
WHERE p2.forum_post_id = (SELECT MAX(p3.forum_post_id) FROM forum_post AS p3
WHERE p3.fk_forum_cat_id = p2.fk_forum_cat_id) AND p2.fk_forum_cat_id = c.forum_cat_id)
FROM forum_cat AS c INNER JOIN
forum_post AS p ON p.fk_forum_cat_id = c.forum_cat_id
GROUP BY c.forum_cat_id;
I had to guess some field names:
forum_cat_id = forum _cat primary key
forum_post_id = forum_post primary key
post_title = post title or begining of post text in forum_post (depends on what you want to show).
The COUNT(p.fk_forum_cat_id) column will contain the post count in category
In addition to what you asked, you will get the date of the lastest post in the category as I think you'll need it if it is a good forum ;).
Obs: I didn't test it so you may need some debugging. If you have problems, let me know.
You can adapt this example to your problem:
SELECT *
FROM `test_post` AS p3
JOIN (
SELECT MAX( id ) AS id
FROM `test_post` AS p1
JOIN (
SELECT MAX( `test_post`.date ) AS DATE, cat
FROM `test_post`
GROUP BY cat
) AS p2 ON p1.date = p2.date
AND p1.cat = p2.cat
GROUP BY p1.cat
) AS p4 ON p3.id = p4.id;
Queries to dynamically count things tend to get slow very quickly and consume a lot of cpu. Even with good indexes, MySQL has to do a lot of work every single time to count all those rows.
An alternative to a query like this would be to summarize the counts of posts in the forum_cat table. Create a column named something like posts_count. Every time a post is created, it's easy enough to run a query increment or decrement the count.
UPDATE forum_cat SET posts_count=posts_count+1;
When you go to create the front page, your query becomes much more simple, and performant.