Sql Count is returning all row values - sql

I have two table users and messages. Messages have status type 1 = unread . I want to get users data and latest message and message time. and also count of messages where messages.status = 1
SELECT users.id, users.name,users.gender,users.status,users.image,users.device_id,users.created_at,users.updated_at,
MAX(messages.created_at) as message_at,
messages.user_id, messages.body as message,
(SELECT COUNT(messages.id) WHERE messages.status = 1 and messages.user_id = users.id) as unread
from messages
JOIN users on users.id = messages.user_id OR users.id = messages.to_id
GROUP BY user_id
ORDER BY message_at DESC
Above queries works. But count of unread it returns wrong numbers.
Edits
I changed This query many problems solves like duplication. But still message count is wrong. I don't know. When All the messages.status != 1 its returns 0 as unread. But When there is one and more messages.status = 1 its shows wrong number :(
Here is updated query.
SELECT users.*,messages.body as message,messages.created_at as message_at,messages.type as message_type,
(SELECT COUNT(messages.id) WHERE (messages.status = 1 and users.id = messages.user_id) ) as unread
from users
JOIN (
SELECT messages.*
FROM messages
ORDER BY messages.created_at DESC
)
messages on users.id = messages.user_id OR users.id = messages.to_id
GROUP BY users.id
ORDER BY message_at DESC
Edits 2.
I have two table.
1 - users
2 - messages [user_id = sender id & to_id = receiver id]
Desired Result.
I want to query all users. with latest message from messages ( for this I have to query all messages with user_id=id or to_id=id). Also with count on unread ( for this messages.status=1) messages.

I think It's because of OR operator .In this query you need exactly " JOIN on users.id = messages.user_id", but if You use OR operator this condition will has wrong numbers. I don't find out column "message.to_id". It works without OR:
SELECT users.id,
users.name,users.gender,users.status,users.image,users.device_id,
users.created_at,
users.updated_at,
MAX(messages.created_at) as message_at,
messages.user_id, messages.body as message,
(SELECT COUNT(messages.id) WHERE messages.status = 1 and messages.user_id =
users.id) as unread
from messages
JOIN users on users.id = messages.user_id
GROUP BY user_id
ORDER BY message_at DESC

Use window functions like MAX(), FIRST_VALUE() and SUM():
SELECT DISTINCT u.*,
MAX(m.created_at) OVER (PARTITION BY u.id) AS message_at,
FIRST_VALUE(m.body) OVER (PARTITION BY u.id ORDER BY m.created_at DESC) AS message,
SUM(m.status IS 1) OVER (PARTITION BY u.id) AS unread
FROM users u LEFT JOIN messages m
ON u.id IN (m.user_id, m.to_id)
This returns the number of messages with status = 1 of each user as a sender or receiver.
If you want only the number of messages that the user sent:
SUM(m.status IS 1 AND m.user_id IS u.id ) OVER (PARTITION BY u.id) AS unread
or the number of messages that the user received:
SUM(m.status IS 1 AND m.to_id IS u.id ) OVER (PARTITION BY u.id) AS unread

Related

Optimizing a nested SQL query through (preferably) joins

I am currently trying to fetch a list of Posts from a database, along with the likes, dislikes and checking whether the user has liked the post or not.
What I have tried:
Here's what the first version of the query looked like:
SELECT
announcements.*,
users.FIRSTNAME,
users.LASTNAME,
((SELECT COUNT(USER_ID) FROM likes_posts WHERE POST_ID = announcements.ID) - (SELECT COUNT(USER_ID) FROM dislikes_posts WHERE POST_ID = announcements.ID)) as TLIKES,
(SELECT COUNT(USER_ID) FROM likes_posts WHERE USER_ID = ? AND POST_ID = announcements.ID) AS USER_LIKED,
(SELECT COUNT(USER_ID) FROM dislikes_posts WHERE USER_ID = ? AND POST_ID = announcements.ID) AS USER_DISLIKED FROM announcements LEFT JOIN users ON announcements.OWNER_ID = users.ID
WHERE announcements.CHANNEL = ? AND announcements.ID < ? ORDER BY announcements.ID DESC
I have tried optimizing it through serval JOINS, but the results are quite messed up:
SELECT
announcements.*,
users.FIRSTNAME,
users.LASTNAME,
COUNT(likes_posts.USER_ID) AS TLikes,
COUNT(dislikes_posts.USER_ID) AS TDislikes,
UserLiked.ID AS userLiked,
UserDisliked.ID AS userDisliked
FROM announcements
LEFT JOIN likes_posts ON likes_posts.POST_ID = announcements.ID
LEFT JOIN dislikes_posts ON dislikes_posts.POST_ID = announcements.ID
LEFT JOIN likes_posts AS UserLiked ON UserLiked.USER_ID = ?
LEFT JOIN likes_posts AS UserDisliked ON UserDisliked.USER_ID = ?
LEFT JOIN users ON announcements.OWNER_ID = users.ID
WHERE announcements.CHANNEL = ? AND announcements.ID < ?
GROUP BY announcements.ID
ORDER BY announcements.ID DESC
Queries' results
The first query manages to constantly fetch the correct number of likes and dislikes (example: 5 and 3).
For the second one, however, it constantly fetches a number that is the double of the current likes or dislikes, whichever is bigger (eg. if there are 5 likes and 6 dislikes, the result would be 16 likes and 16 dislikes)
Problem
I'm guessing the second query is somehow fetching the likes_posts table 2 times, which causes the discrepancy between the likes and dislikes.
Here's one way you could do it, by aggregating the like and dislike counts first, then joining them to the base table. This way you're only doing the counts once each instead of twice
SELECT
a.*,
u.FIRSTNAME,
u.LASTNAME,
coalesce(likes.cnt, 0) - coalesce(dislikes.cnt, 0) as TLIKES,
coalesce(likes.cnt, 0) AS USER_LIKED,
coalesce(dislikes.cnt, 0) AS USER_DISLIKED
FROM
announcements a
LEFT JOIN
users u ON a.OWNER_ID = u.ID
left join
(
select post_id, count(user_id) cnt
from likes_posts
group by post_id
) likes on likes.post_id = a.id
left join
(
select post_id, count(user_id) cnt
from dislikes_posts
group by post_id
) dislikes on dislikes.post_id = a.id
WHERE
announcements.CHANNEL = ? AND announcements.ID < ?
ORDER BY
announcements.ID DESC

Get users with item count <= 1 in sql

We have these tables in PostgreSQL 12:
User -> id, name, email
items -> id, user_id, description
We want to run a query to find users that have 1 item or less.
I tried using a join statement and in the WHERE clause tried to put the count of users < 1 with this query
select * from "user" inner join item on "user".id = item.user_id where count(item.user_id) < 1;
but it failed and gave me this error.
ERROR: aggregate functions are not allowed in WHERE
LINE 1: ...inner join item on "user".id = item.user_id where count(item...
so im thinking the query needs to be more techincal.
Can anyone please help me with this? thanks
You can do:
select u.*
from user u
left join (
select user_id, count(*) as cnt from items group by user_id
) x on x.user_id = u.id
where x.cnt = 1 or x.cnt is null
You don't technically need a JOIN for this. You can get all the necessary data from the item table with GROUP BY. The trick is you need to use HAVING instead of WHERE for aggregated data like COUNT()
SELECT user_id
FROM item
GROUP BY user_id
HAVING COUNT(id) > 1
But we can add a JOIN if you want to see more fields from the user table:
SELECT u.id, u.name, u.email
FROM item i
INNER JOIN "user" u on u.id = i.user_id
GROUP BY u.id, u.name, u.email
HAVING COUNT(i.id) > 1

Retrieving data from PostgreSQL DB in a more efficient way

I'm developing a real-time chat app using PostgreSQL and I'm having the following issue:
When a user logs in, I need to fetch all the users that are not the logged-in user, in order to display them on the sidebar of the app.
Below each user should be displayed the latest message that was sent either by the logged-in user or by the other user.
I'm trying to execute an efficient query in order to retrieve all the users with their latest message at once but with no success.
Here are my tabels:
I tried at first to do something like that:
SELECT users.id, users.first_name, users.last_name, users.image, messages.sender_id, messages.recipient_id, messages.content
FROM users LEFT JOIN messages on users.id = messages.sender_id OR users.id = messages.recipient_id
WHERE (messages.sender_id = 1 OR messages.recipient_id = 1) AND users.id != 1
GROUP BY users.id
ORDER BY messages.created_at DESC;
And I got this error:
"1" refers to the logged user id
My temporary solution is to fetch all the users from the db, mapping over them on the server and executing another query which sends back the latest message between the users using - ORDER BY created_at DESC LIMIT 1.
I'm sure there are more efficient ways, and I would appreciate any help!
If I follow you correctly, you can use conditional logic to select the messages exchanged (sent or received) between the logged-in user and any other user, and then a join to bring the corresponding user records. To get the latest message per user, distinct on comes handy in Postgres.
Consider:
select distinct on (u.id) u.id, ... -- enumerate the columns you want here
from (
select m.*,
case when sender_id = 1 then recipient_id else sender_id end as other_user_id
from messages m
where 1 in (m.sender_id, m.recipient_id)
) m
inner join users u on u.id = m.other_user_id
order by u.id, m.created_at desc
We could also phrase this with a lateral join:
select distinct on (u.id) u.id, ...
from messages m
cross join lateral (values
(case when sender_id = 1 then recipient_id else sender_id end as other_user_id)
) as x(other_user_id)
inner join users u on u.id = x.other_user_id
where 1 in (m.sender_id, m.recipient_id)
order by u.id, m.created_at desc

How to count and group by column across one to many relationship while handling 0 case?

I am trying to formulate a single SQL query that will count a table across a one to many relationship. Here is the short version of my schema:
User(id)
Group(id)
UserGroup(user_id, group_id)
Post(id, user_id, group_id)
The goal is to return the count of posts for each user in a group. The specific issue I am running into is my current query cannot return 0 for a user that has no posts. Here is my naive query:
SELECT
COUNT(*) as total,
user_id
FROM
posts
WHERE
group_id = ?
GROUP BY user_id
ORDER BY
total DESC
This works fine when every user has a post, but when some have no posts, they do not show up in the list. How can I write a single query that handles this scenario and returns count 0 for said users? I know I need to somehow incorporate UserGroup to get the list of users, but am stuck from there.
Use a left join:
SELECT u.id, COUNT(*) as total
FROM users u LEFT JOIN
posts p
ON p.user_id = u.id AND
p.group_id = ?
GROUP BY u.id
ORDER BY total DESC
I think I got it, but not sure how performant.
select count(p), u.id from users u left join (select * from workouts where group_id = ?) p on p.user_id = u.id where u.id in (select user_id from user_group where group_id = ?) group by u.id;

Duplicates results in query

I have an apps table. Each app has many conversations and users. A conversation has many messages and each message can either belong to a visitor or user and a visitor can have many conversations.
For each of my conversations, I want to attach the name and avatar of the user who most recently wrote in the conversation.
If no user has replied, then instead I'd like to grab the 3 most recently created user's avatars, along with the name of the app, and use these instead.
This is what I've got so far, but it returns multiple results for the same conversation id, and I haven't found a solution to getting the app users avatars
select
c.id,
c.last_message,
c.last_activity,
coalesce(last.display_name, a.name || ' Team') as name,
array_agg(last.avatar)
from messages m
left join conversations c on c.id = m.conversation_id
left join apps a on a.id = c.app_id
left join lateral (
select u.id, u.display_name, u.avatar
from users u
where u.id = m.user_id
) as last on true
where c.visitor_id = 'c6p77hu9v000a4zcth4lnefn9'
group by c.id, last.display_name, last.avatar, a.name
order by c.inserted_at desc
Any help is greatly appreciated
For each of my conversations, I want to attach the name and avatar of the user who most recently wrote in the conversation.
To do that, you can use a LATERAL subquery, but you also need to add ORDER BY in such way that the last message is first, then use LIMIT 1 to get only that last row. So, if I assume you have a column message_datetime in message table, which stores the date and time the message has been sent, you can use:
select
c.id,
c.last_message,
c.last_activity,
coalesce(last.display_name, a.name || ' Team') as name,
last.avatar
from
conversations c
left join apps a on a.id = c.app_id
left join lateral (
select
u.id, u.display_name, u.avatar
from
users u
inner join messages m on u.id = m.user_id
where
c.id = m.conversation_id
order by
m.message_datetime desc
limit 1
) as last on true
where
c.visitor_id = 'c6p77hu9v000a4zcth4lnefn9'
order by
c.inserted_at desc
If no user has replied, then instead I'd like to grab the 3 most recently created user's avatars, along with the name of the app, and use these instead.
That is simpler, as this query is uncorrelated to the previous. Assuming your users have an created_datetime column with the date and time the user has been created, you can use the simple query:
select
u.id, u.display_name, u.avatar
from
users u
order by
u.created_datetime desc
limit 3
And so you can use it as a subquery in the previous query, using COALESCE to control which information to use:
select
c.id,
c.last_message,
c.last_activity,
coalesce(last.display_name, a.name || ' Team') as name,
coalesce(array[last.avatar], last_all.avatar) as avatar
from
conversations c
left join apps a on a.id = c.app_id
left join lateral (
select
u.id, u.display_name, u.avatar
from
users u
inner join messages m on u.id = m.user_id
where
c.id = m.conversation_id
order by
m.message_datetime desc
limit 1
) as last on true
left join (
select
array_agg(u.avatar) as avatar
from
users u
order by
u.created_datetime desc
limit 3
) last_all on true
where
c.visitor_id = 'c6p77hu9v000a4zcth4lnefn9'
order by
c.inserted_at desc