Using a limit on a left join in mysql - mysql

The following query selects all posts and each post's owner, all of the comments that belong to each post, and the owner of each comment.
I need to only retrieve 5 comments per post. I rewrote the query, but I get an error of "each derived table must have it's own alias".
SELECT posts.id AS postId, posts.body, users.id AS userId, users.displayname, comments.id AS commentId, comments.text, commenters.id, commenters.displayname
FROM posts
JOIN users ON posts.owneruserid = users.id
LEFT JOIN comments ON posts.id = comments.postid
JOIN users AS commenters ON comments.userId = commenters.id
ORDER BY posts.createdAt
New Query:
SELECT posts.id AS postId, posts.body, users.id AS userId, users.displayname
FROM posts
JOIN users ON posts.owneruserid = users.id
LEFT JOIN (
SELECT comments.id AS commentId, comments.text AS commentText, commenters.id AS commenterId, commenters.displayname AS commenterDisplayName
FROM comments
JOIN users AS commenters ON comments.userid = commenters.id
LIMIT 0,5
) AS comments ON comments.postid = posts.id
ORDER BY posts.createdAt
UPDATE The query now works, but it does not produce the desired output. I want to output 10 posts, with 5 comments for each post. This limit clause will only apply for the comments of the first post encountered.

From the edits and comment feedback, here's the query I think you are looking for... The inner most will prequery gets the posts and who initiated the post, comments and who posted the comments. This inner query is also pre-sorted with the MOST RECENT COMMENTS to the top per postID. Using the result of that, I'm joining to the sql variables (#variables) to get the #varRow increased every time a new comment and reset back to 1 each time a post ID changes (hence the inner PreQuery orders by post ID FIRST). Finally, using the HAVING clause to have the comment's #varRow count < 6 will get at MOST 5 of each post.
If you want to limit what posts you are trying to retrieve, I would apply a WHERE clause (such as date/time if available) at the INNER most that generates the "PreQuery".
select straight_join
PreQuery.*,
#varRow := if( #LastPost = PreQuery.PostID, #varRow +1, 1 ) CommentRow,
#LastPost := PreQuery.PostID PostID2
from
( select
posts.id PostID,
posts.body,
posts.CreatedAt,
u1.id UserID,
u1.DisplayName NameOfPoster,
c.id,
c.userid CommentUserID,
c.text CommentText,
u2.DisplayName CommentUserName
from
posts
join users u1
on posts.ownerUserID = u1.id
LEFT JOIN comments c
on posts.id = c.PostID
join users u2
on c.userid = u2.id
where
posts.id = TheOneParentIDYouWant
OR posts.parentid = TheOneParentIDYouWant
order by
posts.ID,
c.id desc ) PreQuery,
(select #varRow := 0, #LastPost = 0 ) SQLVars
having
CommentRow < 6
order by
PreQuery.postid,
CommentRow
--- EDIT --- per comment
I THINK what you mean by which "Parent Post" the comments are associated with is because they have the post ID directly. Since the inner-most query does a join of all elements / tables across the board, all are coming along for the ride...
Post -> User (to get posting user name )
Post -> Comment (on Common Post ID -- left joined)
Comment -> User ( to get commenting user name)
Once THAT is all done and sorted by common Post ID and most recent comment sorted to the top, I then apply the #vars against ALL returned rows. The HAVING clause will strip out any comment where it's sequence is BEYOND the 5 you were looking for.

You need to give your derived table an alias:
SELECT posts.id AS postId, posts.body, users.id AS userId, users.displayname
FROM posts
JOIN users ON posts.owneruserid = users.id
LEFT JOIN (
SELECT comments.id AS commentId, comments.text AS commentText, commenters.id AS commenterId, commenters.displayname AS commenterDisplayName
FROM comments
JOIN users AS commenters ON comments.userid = commenters.id
LIMIT 0,5
) AS derived_table_alias
ORDER BY posts.createdAt

Since you're using a subquery (which is what it means by "derived table"), it must indeed have an alias. Thus, all you need to do is:
SELECT posts.id AS postId, posts.body, users.id AS userId, users.displayname
FROM posts
JOIN users ON posts.owneruserid = users.id
LEFT JOIN (
SELECT comments.id AS commentId, comments.text AS commentText, commenters.id AS commenterId, commenters.displayname AS commenterDisplayName
FROM comments
JOIN users AS commenters ON comments.userid = commenters.id
LIMIT 0,5
) as some_alias --This is what's triggering the error
ORDER BY posts.createdAt
Even if you're not selecting from the subquery, and just using it as a filter, you have to alias it.

By Error
Add an alias following your subquery.
Example: SELECT * FROM foo JOIN (select * from bar) AS <alias_here>
Make sure you have a field in the posts table and that it is called createdAt. I'm not sure MySQL is case-sensitive, but the error you posted says createdat (with the 'A' lowercased)
You have two LEFT JOINs, but only one ON statement. A join isn't anything without the hook to join it on. Example:
SELECT *
FROM foo JOIN bar ON (foo.id=bar.id)
LEFT JOIN (select * from foobar) AS baz **ON foo.id=baz.id**
In order to join on a field, the field needs to be present in the table involved in the join. So in the above example, if you match foo.id with baz.id, id needs to be returned in the subquery (baz).

Related

SQL Query to select posts from user and followed users is not including own posts

I'm building a query that should return the last 10 posts (default ordering) by $userId and the users it is following.
SQL Fiddle with a minimal example of the query: https://www.db-fiddle.com/f/i5ByFAXwADj5pfjCTn1g1m/2
The database structure is pretty simple:
posts (id, root, user, content)
users (id, username)
following (user_id, followed)
This is the query I'm currently using to get all posts:
SELECT posts.id, posts.content, users.id AS user_id, users.username
FROM posts
LEFT JOIN users ON posts.user = users.id
WHERE LIMIT 10
The query is working but it is listing posts from every user without distinction.
This is the query I built to exclude posts from users that $userId is not following, but it doesn't include $userId's own posts:
SELECT posts.id, posts.content, users.id AS user_id, users.username
FROM following
LEFT JOIN posts ON posts.user = '$userId' OR posts.user = following.followed
LEFT JOIN users ON posts.user = users.id
WHERE (following.user_id = '$userId' OR following.user_id = NULL) LIMIT 10
I've tried replacing the LEFT JOIN posts with an INNER JOIN and a RIGHT JOIN with no success whatsoever. I'm not able to find the error, why isn't the query including posts made by $userId?
I have also tried selecting from posts and joining followers, but it is returning duplicated content:
SELECT posts.id, posts.content, users.id AS user_id, users.username
FROM posts
LEFT JOIN following ON following.user_id = '$userId'
LEFT JOIN users ON posts.user = users.id
WHERE (posts.user = '$userId' OR posts.user = following.followed)
LIMIT 10;
I was about to post a UNION solution
SELECT
post_id,
content,
user_id,
username
FROM
(SELECT
posts.id post_id,
content,
users.id user_id,
username
FROM
posts INNER JOIN
users
ON user = users.id
UNION SELECT
posts.id,
content,
users.id,
username
FROM
posts INNER JOIN (
following INNER JOIN
users
ON user_id = users.id
) ON user = followed
) p
WHERE
user_id = 1
LIMIT 10;
Then I saw #Gordon Linoff's solution which might be better - more concise, at least - but I don't think it works as posted.
SELECT
posts.id,
content,
users.id,
username
FROM
posts INNER JOIN
users
ON user = users.id
WHERE
users.id = 1
OR EXISTS (
SELECT
*
FROM
following
WHERE
followed = user
AND user_id = 1
)
LIMIT 10;
Get the posts from the table posts under your conditions and join to users:
select p.id, p.content, u.id AS user_id, u.username
from (
select *
from posts
where user = '$user_id'
or user in (select user_id from following where followed = '$user_id')
) p inner join users u on u.id = p.user
order by p.id desc limit 10
Note that as it is your requirement the results may not contain posts by the user '$user_id' if the last 10 posts are from the users that this user follows.
See the demo.
I'm building a query that should return the last 10 posts by $userId and the users it is following.
So, there are two tasks here:
Get first N records per group
Apply query to given user PLUS the same for the related users
I would do something like this (pseudo code):
ids = query('SELECT user_id FROM following WHERE followed = :id)', userId).pluck('user_id');
ids.push(userId);
SELECT x.id, x.user_id, x.content
FROM (
SELECT #num := IF(#id = user_id, #num + 1, 1) AS num,
#id := posts.user_id as x_user_id,
posts.*
FROM
(SELECT #num := null, #id := null) x,
posts
WHERE posts.user_id IN(:ids)
ORDER BY posts.id DESC
) AS x
WHERE x.num <= 10
(https://www.db-fiddle.com/f/aiBUwqDApJp6foyq13ZZ2u/1)
See:
S.O.: get first N records per group
S.O.: prepared statement with WHERE IN
If I understand correctly, you basically want an EXISTS clause:
SELECT p.id, p.content, u.id AS user_id, u.username
FROM posts p JOIN
users u
ON p.user = u.id
WHERE u.id <> ? -- ? is the userid AND
EXISTS (SELECT 1
FROM following f
WHERE f.followed = ? AND
f.user_id = u.id
)
LIMIT 10;

MYSql query with inner joins returns duplicate results

I am trying to get the posts by the users that the userID (? in the query) follows, AND get the posts by ? user, themselves.
SELECT
posts.id AS postid, posts.user AS user, posts.images AS images, posts.post_created, posts.textvalue, posts.textpost,
users.username AS authorname
FROM posts
INNER JOIN follows ON (posts.user = follows.follows AND follows.user = ?) OR posts.user = ?
INNER JOIN users ON users.id = posts.user
ORDER BY posts.post_created DESC LIMIT 20
This query works, but for every post that is by ? it returns the row twice, when I remove OR posts.user = ? it works fine, but does not show the posts for the ? user id.
I believe you want:
SELECT p.*, u.username AS authorname
FROM posts p INNER JOIN
users u
ON u.id = p.user LEFT JOIN
follows f
ON p.user = f.follows AND f.user = ?
WHERE f.user IS NOT NULL OR -- a match on the "follows" condition
p.user = ? -- a match on the post condition
ORDER BY p.post_created DESC
LIMIT 20;
Your query is a but confusing. But basically you have this condition for posts from the user:
FROM posts p INNER JOIN
follows f
ON p.user = ?
(forget the OR part -- that only further increases the number of rows.)
There is no JOIN condition between the two tables, so you are getting a row for every user that follows the post.

Duplicate mysql results being returned with two left joins

I'm trying to determine if a user has liked a post but I'm getting duplicate results returned...Am I missing something or is there a better (more efficient) way of retrieving this data?
SELECT
p.*,
u.firstname,
u.lastname,
l.post_id AS liked
FROM `posts` p
LEFT JOIN `users` u ON u.id = p.author_id
LEFT JOIN `likes` l ON l.user_id = p.author_id
WHERE p.author_id=1 AND p.published = 1
ORDER BY p.post_date DESC LIMIT 0, 10
MySQL Tables:
**users table**
id, firstname, lastname
**posts table**
id, author_id, post, published, post_date
**likes table**
id, post_id, user_id, timestamp
Here is the output:
id, author_id, post, firstname, lastname, liked
"36","1","sdfsdf","John","Smith","36"
"36","1","sdfsdf","John","Smith","4"
"27","1","xcvxvc","John","Smith","36"
"27","1","xcvxvc","John","Smith","4"
"29","1","sdfsdf","John","Smith","36"
"29","1","sdfsdf","John","Smith","4"
"3","1",""fdgdfg","John","Smith","36"
"3","1",""fdgdf","John","Smith","4"
"28","1","data","John","Smith","36"
"28","1","data","John","Smith","4"
Any thoughts?
I didn't realize you could use conditional statements on JOINs so I ended up doing this to solve my issue:
SELECT
p.*,
u.firstname,
u.lastname,
l.post_id AS liked
FROM `posts` p
LEFT JOIN `users` u ON u.id = p.author_id
LEFT JOIN `likes` l ON l.user_id = p.author_id
AND l.post_id = p.id
WHERE p.author_id=1
AND p.published = 1
ORDER BY p.post_date DESC LIMIT 0, 10
You have a typical configuration which defines a many-to-many relationship between posts and users. However, there is a particular way to join these tables together, using the intersection table likes which you don't quite have down.
An intersection table contains two foreign keys, one to each of the two tables that relate to each other. In the case of likes, this is the user_id and post_id fields which reference the users table and posts table respectively. To properly join these tables, the likes joins to the users table using the user_id field and the posts table using the post_id field.
The order of the tables is not really significant to resolve the relationships -- to a point. In your case, because you want all the posts listed even if there are no "likes" associated with them, then the posts table goes first. Never, however, can the intersection table go last. It must precede both entity tables or go between them in the list. Each entity table joins to the intersection table, not to each other.
Like so:
SELECT p.*, u.firstname + ' ' + u.lastname AS LikedBy
FROM `posts` p
LEFT JOIN `likes` l
ON l.post_id = p.id
LEFT JOIN `users` u
ON u.id = l.user_id
WHERE ...

MySQL query with multiple INNER JOIN

I'm a little bit confused about a stupid query:
I get rows from the table posts joined with the table authors and the table comments, in a way like this:
SELECT posts.*, authors.name, COUNT(comments.id_post) AS num_comments
FROM posts JOIN authors ON posts.id_author = authors.id_author
LEFT JOIN comments ON posts.id_post = comments.id_post
WHERE posts.active = 1
AND comments.active = 1
this doesn't work, of course.
What I try to do is to retrieve:
1) all my active post (those that were not marked as deleted);
2) the names of their authors;
3) the number of active comments (those that were not marked as deleted) for each post (if there is at least one);
What's the way? I know it's a trivial one, but by now my brain is in offside…
Thanks!
Presumably, id_post uniquely identifies each row in posts. Try this:
SELECT p.*, a.name, COUNT(c.id_post) AS num_comments
FROM posts p JOIN
authors a
ON p.id_author = a.id_author LEFT JOIN
comments c
ON p.id_post = c.id_post
WHERE p.active = 1 AND c.active = 1
GROUP BY p.id_post;
Note that this uses a MySQL extension. In most other databases, you would need to list all the columns in posts plus a.name in the group by clause.
EDIT:
The above is based on your query. If you want all active posts with a count of active comments, just do:
SELECT p.*, a.name, SUM(c.active = 1) AS num_comments
FROM posts p LEFT JOIN
authors a
ON p.id_author = a.id_author LEFT JOIN
comments c
ON p.id_post = c.id_post
WHERE p.active = 1
GROUP BY p.id_post;
Since you are doing a count, you need to have a group by. So you will need to add
Group By posts.*, authors.name
You should you GROUP BY clause together with aggregate functions. Try something similar to:
SELECT posts.*, authors.name, COUNT(comments.id_post) AS num_comments
FROM posts JOIN authors ON posts.id_author = authors.id_author
LEFT JOIN comments ON posts.id_post = comments.id_post
-- group by
GROUP BY posts.*, authors.name
--
WHERE posts.active = 1
AND comments.active = 1
I found the correct solution:
SELECT posts.id_post, authors.name, COUNT(comments.id_post) AS num_comments
FROM posts JOIN authors
ON posts.id_author = authors.id_author
LEFT OUTER JOIN comments
ON (posts.id_post = comments.id_post AND comments.active = 1)
WHERE posts.active = 1
GROUP BY posts.id_post;
Thanks everyone for the help!

How to order a query by column if some of its values are null?

I have two tables posts and posts_replies I queried all posts and order them by timestamp (post time).
Now I want to do the following :
Any time a user make a reply for any post I want to put that post at the top of the wall
so I did get the max replies' timestamp of each post and order the posts using the max timestamp of the replies of specific post.
The problem is that some posts does not have any replies so the max timestamp_of_replies of this post will be NULL so I want to know : Does it possible to order the result by timestamp_of_replies when it is not null and by post_timestamp when it is NULL.
My query :
SELECT
posts.*,
u1.id as user_id,
u1.avatar,
u1.username as poster_username,
u2.username as posted_username ,
posts.timestamp,
f1.recent_reply_time
FROM
posts
INNER JOIN
users u1
ON posts.poster_id = u1.id
INNER JOIN
users u2
ON posts.posted_id = u2.id
LEFT JOIN (
SELECT
max(timestamp) as recent_reply_time,
post_id
FROM
posts_replies
GROUP BY post_id) f1
ON f1.post_id = posts.id
order by
f1.recent_reply_time DESC
Note : order by f1.recent_reply_time, posts.timestamp DESC did not give me right results
MySQL has an IF() function, that you may also use in the ORDER BY clause:
ORDER BY IF(column IS NULL, othercolumn, column)
in your case it would be:
ORDER BY IF(f1.recent_reply_time IS NULL, posts.timestamp, f1.recent_reply_time)
Use COALESCE:
SELECT posts.*, u1.id as user_id, u1.avatar, u1.username as poster_username,
u2.username as posted_username , posts.timestamp, f1.recent_reply_time
FROM posts
INNER JOIN users u1 ON posts.poster_id = u1.id
INNER JOIN users u2 ON posts.posted_id = u2.id
LEFT JOIN (SELECT max(timestamp) as recent_reply_time, post_id
FROM posts_replies
GROUP BY post_id) f1 ON f1.post_id = posts.id
ORDER BY COALESCE(f1.recent_reply_time,[alternative value (CURDATE())]) DESC
SORTING is easily readable and maintainable, if you make it based on what you select. Better have some sortkey in your select and use it in ORDER BY.
i have used COALESCE to handle nulls. Makes ure sortkey atleast have one NOT NULL value, by using suitable arguments to COALESCE
SELECT posts.*,
u1.id as user_id,
u1.avatar,
u1.username as poster_username,
u2.username as posted_username,
posts.timestamp,
f1.recent_reply_time,
COALESCE(f1.recent_reply_time,posts.timestamp) as sortkey
FROM posts
INNER JOIN users u1 ON posts.poster_id = u1.id
INNER JOIN users u2 ON posts.posted_id = u2.id
LEFT JOIN (SELECT max(timestamp) as recent_reply_time, post_id FROM posts_replies GROUP BY post_id) f1 ON f1.post_id = posts.id
order by sortkey DESC