Retrieving data from PostgreSQL DB in a more efficient way - sql

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

Related

How to print two attribute values from your Sub query table

Suppose I have two tables,
User
Post
Posts are made by Users (i.e. the Post Table will have foreign key of user)
Now my question is,
Print the details of all the users who have more than 10 posts
To solve this, I can type the following query and it would give me the desired result,
SELECT * from USER where user_id in (SELECT user_id from POST group by user_id having count(user_id) > 10)
The problem occurs when I also want to print the Count of the Posts along with the user details. Now obtaining the count of user is not possible from USER table. That can only be done from POST table. But, I can't get two values from my subquery, i.e. I can't do the following,
SELECT * from USER where user_id in (SELECT user_id, **count(user_id)** from POST group by user_id having count(user_id) > 10)
So, how do I resolve this issue? One solution I know is this, but this I think it would be a very naive way to resolve this and will make the query much more complex and also much more slow,
SELECT u.*, (SELECT po.count(user_id) from POST as po group by user_id having po.count(user_id) > 10) from USER u where u.user_id in (SELECT p.user_id from POST p group by user_id having p.count(user_id) > 10)
Is there any other way to solve this using subqueries?
Move the aggregation to the from clause:
SELECT u.*, p.num_posts
FROM user u JOIN
(SELECT p.user_id, COUNT(*) as num_posts
FROM post p
GROUP BY p.user_id
HAVING COUNT(*) > 10
) p
ON u.user_id = p.user_id;
You can do this with subqueries:
select u.*
from (select u.*,
(select count(*) from post p where p.user_id = u.user_id) as num_posts
from users u
) u
where num_posts > 10;
With an index on post(user_id), this might actually have better performance than the version using JOIN/GROUP BY.
You can try by joining the tables, Prefer to do a JOIN than using SUBQUERY
SELECT user.*, count( post.user_id ) as postcount
FROM user LEFT JOIN post ON users.user_id = post.user_id
GROUP BY post.user_id
HAVING postcount > 10 ;

Sql Count is returning all row values

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

How to pull the count of occurences from 2 SQL tables

I am using python on a SQlite3 DB i created. I have the DB created and currently just using command line to try and get the sql statement correct.
I have 2 tables.
Table 1 - users
user_id, name, message_count
Table 2 - messages
id, date, message, user_id
When I setup table two, I added this statement in the creation of my messages table, but I have no clue what, if anything, it does:
FOREIGN KEY (user_id) REFERENCES users (user_id)
What I am trying to do is return a list containing the name and message count during 2020. I have used this statement to get the TOTAL number of posts in 2020, and it works:
SELECT COUNT(*) FROM messages WHERE substr(date,1,4)='2020';
But I am struggling with figuring out if I should Join the tables, or if there is a way to pull just the info I need. The statement I want would look something like this:
SELECT name, COUNT(*) FROM users JOIN messages ON messages.user_id = users.user_id WHERE substr(date,1,4)='2020';
One option uses a correlated subquery:
select u.*,
(
select count(*)
from messages m
where m.user_id = u.user_id and m.date >= '2020-01-01' and m.date < '2021-01-01'
) as cnt_messages
from users u
This query would take advantage of an index on messages(user_id, date).
You could also join and aggregate. If you want to allow users that have no messages, a left join is a appropriate:
select u.name, count(m.user_id) as cnt_messages
from users u
left join messages m
on m.user_id = u.user_id and m.date >= '2020-01-01' and m.date < '2021-01-01'
group by u.user_id, u.name
Note that it is more efficient to filter the date column against literal dates than applying a function on it (which precludes the use of an index).
You are missing a GROUP BY clause to group by user:
SELECT u.user_id, u.name, COUNT(*) AS counter
FROM users u JOIN messages m
ON m.user_id = u.user_id
WHERE substr(m.date,1,4)='2020'
GROUP BY u.user_id, u.name

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