Need feedback on an SQL Request (MySQL, GROUP BY behavior) - mysql

I'm programming a private conversation system and need advices on my most complex request.
My (simplified) tables, under MySQL 5.5 :
user:
id (int)
username (string)
conversation:
id (int)
subject (string)
conversation_user:
conversation_id (int)
user_id (int)
message:
id (int)
conversation_id (int)
user_id (int)
content (text)
created_at (datetime)
So, I want to display to the user the list of the conversations in which he is participating : for each conversation, I need the conversation id, the conversation subject, the list of participating users, the last message id, the last message author id and username, and the last message date.
I've written this request, which looks pretty bad :
SELECT
cu.conversation_id,
c.subject,
u.username,
m.id,
m.user_id,
m.created_at
FROM conversation_user cu
JOIN conversation c ON c.id = cu.conversation_id
JOIN conversation_user cu2 ON cu2.conversation_id = c.conversation_id
JOIN user u ON u.id = cu2.user_id
JOIN message m ON m.conversation_id = cu.conversation_id
WHERE cu.user_id = :current_user_id # the session user_id
AND m.id = (SELECT MAX(id) FROM message WHERE conversation_id = cu.conversation_id)
So, I would like to know if you guys see a better way to get the same result ?
I've already read GROUP BY behavior when no aggregate functions are present in the SELECT clause, that's why I didn't put a GROUP BY clause and wrote the last line instead :
AND m.id = (SELECT MAX(id) FROM message WHERE conversation_id = cu.conversation_id)
Thanks !
Edit
I did an EXPLAIN on this request, and there is no KEY used for the JOIN user u ON u.id = cu2.user_id line : why ?
(the first question is still relevant)

There are alternatives, such as I'm about to put forward for you. But, honestly, that's how I'd do it :)
AND m.id = (SELECT id FROM message WHERE conversation_id = cu.conversation_id ORDER BY id DESC LIMIT 1)
The above allows you to order by DATE or somet other field, and still pick just one message id.
JOIN
message m
ON m.conversation_id = cu.conversation_id
JOIN
(SELECT conversation_id, MAX(id) AS id FROM message GROUP BY conversation_id) AS filter
ON filter.conversation_id = cu.conversation_id
AND filter.id = m.id
The above avoids a correlated sub-query, and so can (but not always) be faster.
In terms of there be no key being used for JOIN user u ON u.id = cu2.user_id, do both of the two tables in question have indexes on the relevant fields?

Related

SELECT the last message of conversation - MySQL

I have query which looks like:
SELECT * FROM
( SELECT DISTINCT CASE
WHEN user1_id = 1
THEN user2_id
ELSE user1_id
END userID,conversationId
FROM conversations
WHERE 1 IN (user2_id,user1_id))dt
INNER JOIN users on dt.userID = users.id
It returns conversationId and information about user from users table. I would like to also add the last message (the one with biggest messageId) from message table on base of conversationId. The last thing would be to sort all the results by messageId
I tried to use another INNER JOIN which looked like :
INNER JOIN message on dt.conversationId = message.conversationId
Its adding messages to the result but I would like to get only the last one (the one with highest messageId as mentioned). I guess I would have to implement MAX somehow but I dont have idea how. The same thing with sorting all result by messageId so results with the biggest messageId would be first.
Thanks for all suggestions.
You can get the highest messageId for the conversation in a corelated subquery and use it for your join condition:
INNER JOIN message m
on m.conversationId = dt.conversationId
and m.messageId = (
SELECT MAX(m1.messageId)
FROM message m1
WHERE m1.conversationId = dt.conversationId
)
So the solution for eveything was following query
SELECT * FROM
( SELECT DISTINCT
CASE
WHEN user1_id = 1
THEN user2_id
ELSE user1_id
END userID,conversationId
FROM conversations
WHERE 1 IN (user2_id,user1_id))dt
INNER JOIN users on dt.userID = users.id
INNER JOIN message m on m.conversationId = dt.conversationId and m.messageId = (SELECT MAX(m1.messageId)
FROM message m1 WHERE m1.conversationId = dt.conversationId)
ORDER by m.messageId DESC

conversation type inbox messages between two users for a website

I want to achieve a conversation type inbox messages between two users in which the conversations will be group between two users and displayed on the inbox page and when clicked on the conversation the overall chat history will be displayed between two users ....
I have a database schema but failed to group the conversations between two users.I need a query for acheiving this Thank you
here are my tables.
1.Users:
id
username
password
2.conversations:
conversation_id
from_user
to_user
3.messages:
message_id
conversation_id
user_id
message_text
message_date
seen
so if someone know how to achieve this query then help me also changes to my schema will be appreciated thank you
SELECT conversation FROM
conversations
WHERE (conversations.from_user = <user parameter>
OR conversations.to_user = <user parameter>)
AND conversations.from_user <> conversations.to_user
You can get all conversations of current user with last message by conversation using query that looks like the following:
SELECT users.id,
users.name,
messages.conversation_id,
messages.message_id,
messages.message_text,
messages.message_date,
messages.seen
FROM
(SELECT conversations.conversation_id,
IF(conversations.from_user = <current_user_id>, conversations.to_user, conversations.from_user) AS interlocutor_id,
max(messages.message_id) AS last_message_id
FROM conversations
LEFT JOIN messages ON conversations.conversation_id = messages.conversation_id
WHERE convesations.from_id = <current_user_id>
OR conversations.to_id = <current_user_id>
GROUP BY conversations.conversation_id) AS t
LEFT JOIN users ON users.id = t.intercolutor_id
LEFT JOIN messages ON messages.message_id = t.last_message_id
If you need get all conversations of current user with last indox messages using:
SELECT users.id,
users.name,
messages.conversation_id,
messages.message_id,
messages.message_text,
messages.message_date,
messages.seen
FROM
(SELECT conversations.conversation_id,
IF(conversations.from_user = <current_user_id>, conversations.to_user, conversations.from_user) AS interlocutor_id,
max(messages.message_id) AS last_message_id
FROM conversations
LEFT JOIN messages ON conversations.conversation_id = messages.conversation_id
WHERE (convesations.from_id = <current_user_id>
OR conversations.to_id = <current_user_id>)
AND messages.user_id <> <current_user_id>
GROUP BY conversations.conversation_id) AS t
LEFT JOIN users ON users.id = t.intercolutor_id
LEFT JOIN messages ON messages.message_id = t.last_message_id
But I think than better way if you will rewrite your database scheme and you will add id of last message in 'conversations' table. It will make the query more optimal
UPDATED:
If you need get list of all users that have conversation with current user:
SELECT t.conversation_id,
users.id,
users.name
FROM
(SELECT conversation_id,
IF(from_user = <current_user_id>, to_user, from_user) AS interlocutor_id
FROM conversations
WHERE from_id = <current_user_id>
OR to_id = <current_user_id>) AS t
LEFT JOIN users ON users.id = t.intercolutor_id
P.S. You must replace <current_user_id> to actual indentifier of current user

Getting a message thread from a MySQL database based on USER and MESSAGE tables

I may be asking my SQL query to do a bit much in one hit. I'm writing a messaging script and want to display the summary list of conversation threads just like text messages work on phones; so the person the message thread is with, the date/time of the last message and the number of unread messages per user.
So consider this query, assuming the user id we're checking is 1:
SELECT u.id,
u.name,
u.image,
m.message,
m.sender,
m.sent,
sum(if(m.sender<>1,m.unread,0)) as unread
FROM messages m
INNER JOIN members u ON u.id = m.sender OR u.id = m.receiver
WHERE (m.sender = 1 OR m.receiver = 1) AND u.id <> 1
GROUP BY IF(m.sender = 1, m.receiver, m.sender)
ORDER BY m.sent DESC
In this the field types should be reasonably obvious. 'sender' is the id of the person who sent the message; 'receiver' the other user id; 'sent' the date/time the message was sent; and 'unread' a tinyint flag which is either 0 or 1.
For the most part this works - it gives me a list of unique conversation threads plus the correct number of unread messages, but, the message it's picking up as the last one sent always seems to be the first one, not the last one as if ORDER BY m.sent DESC is being overridden by the GROUP or JOIN. I'm obviously doing something dumb, but I just can't see it. Any pointers?
Ok, I managed to solve this after much trial and error by adding another join to the messages table which simply grabbed the last unique message ID for each grouped conversation, i.e.:
SELECT u.id,
u.name,
u.image,
m.message,
m.sender,
m.sent,
sum(if(m.sender<>1,m.unread,0)) as unread
FROM messages m
INNER JOIN members u ON u.id = m.sender OR u.id = m.receiver
JOIN (SELECT max(id) id FROM messages GROUP BY IF(sender=1, receiver, sender)) t ON m.id = t.id
WHERE (m.sender = 1 OR m.receiver = 1) AND u.id <> 1
GROUP BY IF(m.sender = 1, m.receiver, m.sender)
ORDER BY m.sent DESC
I'm still a little confused as to why the original query wasn't picking up the last message as per the ORDER BY sent, but hey - a fix is a fix.

Select distinct and count other table

In a small chat service, two users have a common chat.
This SQL selects a list of the users that you have chatted with.
SELECT DISTINCT user.user_id, user.username
FROM user
INNER JOIN message
ON user.user_id = message.owner_user_id OR user.user_id = message.to_user_id
WHERE message.owner_user_id = :activeUserId
OR message.to_user_id = :activeUserId
ORDER BY message.date_time DESC
I also need to get the number of messages that has been sent between two users. The output is a list of "folders", one for each user, the active user has chatted with. Each folder contains the username of the user and the number of messages in the chat (the sum of the active user's number of messages to the specific user, and the specific user's number of messages to the active user).
row1: (active user and JohnSmith have 33 messsages in their common chat.)
user_id = 1;
username = 'JohnSmith';
message_count = 33;
row2: (active user and Johnny have 43 messsages in their common chat.)
user_id = 2;
username = 'Johnny';
message_count = 43;
How could this be done in one SQL-statement?
Right now you're "flattening" the results using DISTINCT. You can flatten and count using the COUNT(*) function and GROUP BY:
SELECT DISTINCT user.user_id, user.username, COUNT(*)
FROM user
INNER JOIN message
ON user.user_id = message.owner_user_id OR user.user_id = message.to_user_id
WHERE message.owner_user_id = :activeUserId OR message.to_user_id = :activeUserId
GROUP BY user.user_id, user.username
ORDER BY message.date_time DESC
The only changes to your query are the additional column (COUNT(*)) and the GROUP BY line.
Your question seems a bit ambiguous. It seems like you want to count the messages from one user to each other user they communicate with -- regardless of whether they are the "from" or "to" roles on the message.
When dealing with messages, one approach is to normalize them with the smaller user id first and then the larger user id. The result from such a query would be one row for your users, that looks like:
1 'JohnSmith' 2 'Johnny' 33
rather than two separate rows. You can get the two separate rows as well, but that seems less useful to me.
The query that produces that style of output is:
select least(m.owner_user_id, m.to_user_id) as uid1,
greatest(m.owner_user_id, m.to_user_id) as uid2,
u1.username, u2.username, count(*) as nummessages
from message m join
user u1
on u1.user_id = least(m.owner_user_id, m.to_user_id) join
user u2
on u2.user_id = greatest(m.owner_user_id, m.to_user_id)
WHERE m.owner_user_id = :activeUserId or
m.to_user_id = :activeUserId
group by least(m.owner_user_id, m.to_user_id) as uid1,
greatest(m.owner_user_id, m.to_user_id);
Try:
SELECT DISTINCT(user.user_id), user.username, COUNT(c.to_user_id) AS message_count
FROM user
INNER JOIN message
ON user.user_id = message.owner_user_id
OR user.user_id = message.to_user_id
INNER JOIN message m1
ON user.user_id = m1.owner_user_id
OR user.user_id = m1.to_user_id
WHERE message.owner_user_id = :activeUserId
OR message.to_user_id = :activeUserId
ORDER BY message.date_time DESC

Facebook Style Messages: Efficient Query

users tbl:
id
username
first_name
last_name
email_address
messages tbl:
id
date_sent
title
content
status
message_relation tbl:
id
message_id
sender_id
receiver_id
What would be the most efficient way to query a message TO a selected user given these tables? In other words, I want to list all messages that are in userA's "inbox"
Secondly, how would you recommend handling global messages that need to be sent to everyone from the admin?
I think this is what you're looking for..
SELECT [WHATEVER] FROM
messages
INNER JOIN message_relation
ON (messages.id = message_relation.message_id AND message_relation.receiver_id = $id)
This does what you asked for efficiently. If you also want to select fields on either of the users, you can JOIN Receiver_ID to User_ID to to get that.
Suggested tables setup if you want Global/Admin messages:
Messages (Message_ID, [Fields common to all messages, e.g. Message_Content, Message_Timestamp, etc])
Global_Messages (Global_Message_ID, Message_ID, [any fields specific to Global_Messages])
User_Messages (User_Message_ID, Message_ID, [any fields specific to User_Messages])
User_Message_Relations (User_Message_Relations_ID, User_Message_ID, Sender_ID, Receiver_ID)
Then, to query an Inbox, something like:
SELECT [WHATEVER] FROM
Messages
LEFT JOIN (Global_Messages)
ON (Messages.Message_ID = Global_Messages.Message_ID)
LEFT JOIN (User_Messages)
ON (Messages.Message_ID = User_Messages.Message_ID)
LEFT JOIN (User_Message_Relations)
ON (User_Messages.User_Message_ID = User_Message_Relations.User_Message_ID AND User_Message_Relations.Receiver_ID = $uid)
That will give you a result you can loop through to get a user's entire Inbox.
SELECT
m.id, m.date_sent,m.title,m.content,m.status
FROM
messages m
INNER JOIN
message_relation mr on (mr.message_id = m.id)
INNER JOIN
users u on (u.id = mr.receiver_id);
WHERE u.id = ...your current user id ...
This is a simple way of doing it, and it will be fast and almost no load on your database