Building SQL query to join three tables to get messaging info - mysql

I have the following three tables for a messaging system:
`messaging_messagethread`
- id
- subject
- initiator_id # who creates the thread
- recipient_id
`messaging_message`
- id
- thread_id
- content
- timestamp
- sender_id
`messaging_messagestatus` # a status will be created for each recipient of a message
- id
- message_id
- recipient_id
- status
Given a user, I need to build a query to get the following:
Show Thread ID (distinct),
content and timestamp of the most recent message in that thread
Remove any threads with the most recent message status='deleted'.
Here is what I have so far:
SELECT DISTINCT thread.id as thread_id, timestamp.timestamp
FROM messaging_messagethread thread
INNER JOIN
(SELECT thread_id, MAX(timestamp) as timestamp
FROM messaging_message GROUP BY thread_id) timestamp
ON thread.id = timestamp.thread_id
WHERE initiator_id = 4 OR thread.recipient_id = 4 ORDER BY timestamp.timestamp DESC
This gives me distinct thread id's ordered by most recent timestamp. (My first of three points). How would I build the entire query?

You can use correlated subqueries to get the most recent message of a particular thread. Try this:
SELECT
a.id,
b.content,
b.timestamp
FROM
messaging_messagethread a
INNER JOIN
messaging_message b ON a.id = b.thread_id
WHERE
b.timestamp =
(
SELECT MAX(timestamp)
FROM messaging_message
WHERE thread_id = a.id
)
AND b.id NOT IN
(
SELECT message_id
FROM messaging_messagestatus
WHERE status = 'deleted'
)
AND 4 IN (a.initiator_id, a.recipient_id)
ORDER BY
b.timestamp DESC
If I understand you correctly, I believe this is what you want.

The problem is that you want the latest message from each thread,
unfortunately there's no way to retrieve the top member withing a group AFAIK
So what Im doing is basically getting all the messages from each thread_id
that the user is related to then simply returning the top id ... oh and checking
that message hasn't being deleted.
IM NOT SURE IF THIS IS CORRECT.
But please reconsider a different approach maybe using a programming language,
or multiple queries.
CAUSE THIS IS GOING TO BE SLOW...
SELECT thread_id, content, timestamp
FROM messaging_message
WHERE messaging_message.id IN
(
SELECT messaging_message.id
FROM messaging_message
INNER JOIN messaging_messagestatus ON
(messaging_messagestatus.message_id = messaging_message.id)
WHERE messaging_message.thread_id IN
(SELECT DISTINCT id
FROM messaging_messagethread
WHERE initiator_id = 4 OR recipient_id = 4) AND
messaging_messagestatus.status != 'deleted'
ORDER BY timestamp DESC
LIMIT 1
)
ORDER BY timestamp DESC
good luck.

Related

How to select single rows using MAX and GROUP BY on non-uniqe field in MySQL?

I came across this very simple case where I need to select a list of conversations from Conversations table along with latest message from Messages table - which has non-uniqe dateCreated field.
After long research I came up with this query:
SELECT
Conversations.id,
dateCreated,
`name`,
lastMessageId,
lastMessageDate,
lastMessagePayload
FROM Conversations
LEFT JOIN (
SELECT
id AS lastMessageId,
m1.conversationId,
payload AS lastMessagePayload,
m1.dateCreated AS lastMessageDate,
FROM Messages AS m1
INNER JOIN (
SELECT conversationId, MAX(dateCreated) AS mdate FROM Messages GROUP BY conversationId
) AS m2
ON m1.conversationId = m2.conversationId AND m1.dateCreated = m2.mdate
) AS msg2
ON msg2.conversationId = Conversations.id
ORDER BY dateCreated DESC
Query works well but if two latest messages in same conversation have exact same dateCreated field this query would then output two conversations with same id but different lastMessage... row of fields.
I just couldn't find a way to get around this problem as main problem is when you do GROUP BY a field and MAX on another non-uniqe field then you can't get out always only one row out.
Any idea how to get list of unique conversations with latest message (any message of the two if they have the same date)?
Use row_number()!
select c.*, m.* -- or whatever columns you want
from conversations c left join
(select m.*,
row_number() over (partition by m.conversationid order by m.dateCreated desc, m.id desc) as seqnum
from messages m
) m
on m.conversation_id = c.id and
m.seqnum = 1;
MySQL 5.x version...
Use a correlated sub-query to get the latest message id (for a given conversation), using ORDER BY and LIMIT 1
SELECT
Conversations.Conversations.id,
Conversations.dateCreated,
Conversations.`name`,
Messages.id AS lastMessageId,
Messages.payload AS lastMessagePayload,
Messages.dateCreated AS lastMessageDate,
FROM
Conversations
LEFT JOIN
Messages
ON Messages.id = (
SELECT lookup.id
FROM Messages AS lookup
WHERE lookup.conversationId = Conversations.id
ORDER BY lookup.dateCreated DESC
LIMIT 1
)
ORDER BY
Conversations.dateCreated DESC
In the event of two messages having the same date, the message you get is non-deterministic / arbitrary.
You could, if you wanted, therefore change it to get the highest id from the most recent date...
ORDER BY lookup.dateCreated DESC, lookup.id DESC
LIMIT 1

MySQL If record in subquery AND 1 field not set or NOT in subquery at all

I'm working through a process where I have to sync messages on multiple devices. If you read it or delete it on your phone, the next time you pick pick up your tablet, the message is marked as read or deleted there as well. Common and expected behavior.
Before the multiple device support, everything worked great. Now the problem is in the first subquery.
Process: If the message is read by a user, it gets logged by adding it to the log_table. If it gets deleted by a user, the deleted column in the log_table gets updated. If the message is UNREAD by that user it will not be in the log_table at all.
If there's an entry in the log_table for that user and that message, the query returns an empty result set. The problem is in the first subquery. If the message is read on one device there will be an entry in the log_table so NOT IN is producing a failure when the record has been read and this prohibits us from syncing that read message on the other device.
NOTE: Messages are not 1-to-1 but are 1-to-many.
DESIRED RESULT:
What I need to do, is include the message if it's unread (not in log_table) OR if it's in log_table and NOT deleted. Suggestions?
SELECT A.*, T.name FROM a_table AS A
RIGHT JOIN types_table AS T ON A.msg_type = T.id
WHERE A.id NOT IN (SELECT msg_id AS id FROM log_table WHERE userid='3' AND msg_id = A.id AND UNIX_TIMESTAMP(deleted) = 0)
AND 3 IN (SELECT userid FROM subscriber_table AS S WHERE S.userid='3') AND A.id > 01
Suggestions appreciated!
The solution is a conditional WHERE clause
SELECT A.*, T.name FROM a_table AS A
RIGHT JOIN types_table AS T ON A.msg_type = T.id
WHERE A.id NOT IN (SELECT msg_id AS id FROM log_table WHERE (userid='3' AND msg_id = A.id AND UNIX_TIMESTAMP(read) < 1) OR (userid='3' AND msg_id = A.id AND UNIX_TIMESTAMP(deleted) < 1) )
AND 3 IN (SELECT userid FROM subscriber_table AS S WHERE S.userid='3') AND A.id > 01

MySQL latest unique user chats not working

I'm trying to get latest created datetime for unique user_id. I've got below query but it does not seem to be working....It does not get the latest created time.
SELECT * FROM `chats`
WHERE receiver_id = 1
GROUP BY user_id
ORDER BY created DESC
Is there a reason why?
UPDATE:
Actually I found my answer myself. Please look below. I had to use INNER JOIN for nested searched and filtered result table then find using where clause of that result then left join on that table to get the data I needed!
SELECT c.*, users.username
FROM chats c
INNER JOIN(
SELECT MAX(created) AS Date, user_id, receiver_id, chat, type, id
FROM chats
WHERE receiver_id = 1
GROUP BY user_id ) cc ON cc.Date = c.created AND cc.user_id = c.user_id
LEFT JOIN users ON users.id = c.user_id
WHERE c.receiver_id = 1
You should use a MAX aggregate function -
SELECT user_id, MAX(created) latest_datetime FROM chats
GROUP BY user_id;
Try this query, it will show all users and latest created datetime.

Order by one-to-many relation

I have 2 tables:
Message:
id: INT
created_at: DATETIME
Comment:
id: INT
message_id: INT
created_at: DATETIME
One Message has many Comments.
I want to get all Messages ordered by the most recent activity:
If Message has Comments, then the most recent Comment's created_at is used as the Message activity indicator
If Message doesn't have Comments, it's created_at value is the activity indicator
So basically, I want to sort it like a classic e-mail or private messaging system.
Maybe I can INNER JOIN the Comments, but I don't think it's necessary to get all the Comments just for the sort.
Also, I thought about creating a column in Message to save the last activity date and update it whenever a Comment is created, but I'd like to see if you have better solutions..
I'm using Doctrine, so if you have a Doctrine-based solution I'd rather that,
Thanks!
What I would do in MySQL is something like:
SELECT *
FROM Messages AS m
ORDER BY GREATEST(
m.created_at,
(SELECT MAX( c.created_at ) FROM Comment AS c WHERE c.message_id = m.id )
) DESC
Try something like this:
select m.message_id, c.created_at, m.created_at
from message m
left join (
select message_id, MAX(created_at) as created_at
from comment
group by message_id
) c on m.id = c.message_id
order by
( case when c.created_at >= m.created_at then c.created_at
else m.created_at
end ) desc
The subquery will get most current comment date for each message, then message table will outer join the subquery because some message might not have any comments at all. Then order by greater value of comment created date and message created date.
You might need to change the case syntax for mysql.
If your comment table's id is an auto-incrment you can simply order by id desc and it will return the results in chronological order as long as you aren't doing deletes on the comment table which has a tendency to make inserts show up out of chronological order. Otherwise, you need to do something like:
SELECT a.id, a.created_at
FROM Message a, Comment b
WHERE b.created_at > a.created_at
ORDER BY b.created_at DESC

MySQL JOIN queries - Messaging system

I have the following tables for a messaging system and I was wondering how I would go about querying the DB for how many conversations have new messages.
My tables are as follows
Conversation
------------
id
subject
Messages
--------
id
conversation_id
user_id (sender)
message
timestamp (time sent)
Participants
------------
conversation_id
user_id
last_read (time stamp of last view user viewed conversation)
I'm trying to do the following query but it returns no results:
SELECT COUNT(m.conversation_id) AS count
FROM (messages_message m)
INNER JOIN messages_participants p ON p.conversation_id = m.conversation_id
WHERE `m`.`timestamp` > 'p.last_read'
AND `p`.`user_id` = '5'
GROUP BY m.conversation_id
LIMIT 1
Also, I probably will have to run this on every page load - any tips of making it as fast as possible?
Cheers
EDIT
I've got another somewhat related question if anybody would be so kind as to help out.
I'm trying to retrieve the subject, last message in conversation, timestamp of last convo and number of new messages. I believe I have a working query but it looks a bit badly put together. What sort of improvements can I do to this?
SELECT SQL_CALC_FOUND_ROWS c.*, last_msg.*, new_msgs.count as new_msgs_count
FROM ( messages_conversation c )
INNER JOIN messages_participants p ON p.user_id = '5'
INNER JOIN ( SELECT m.*
FROM (messages_message m)
ORDER BY m.timestamp DESC
LIMIT 1) last_msg
ON c.id = last_msg.conversation_id
LEFT JOIN ( SELECT COUNT(m.id) AS count, m.conversation_id, m.timestamp
FROM (messages_message m) ) new_msgs
ON c.id = new_msgs.conversation_id AND new_msgs.timestamp > p.last_read
LIMIT 0,10
Should I determine if the conversations is unread by doing an IF statement in MySQL or should I convert and compare timestamps on PHP?
Thanks again,
RS7
'p.last_read' as quoted above is a string constant - remove the quotes from this and see whether that changes anything, RS7. If user_id is an integer than remove the quotes from '5' as well.
As far as performance goes, ensure you have indexes on all the relevant columns. messages_participants.user_id and messages_message.timestamp being two important columns to index.
Yes, you have problem in your query.
Firstly, you should have noticed that you count the column you are grouping, so the count result will be 1.
Secondly, you are comparing the timestamp to a string : m.timestamp > 'p.last_read'.
Finally, avoid using LIMIT when you know your query will return one row (be self-confident :p).
Try:
SELECT
COUNT(m.conversation_id) AS count
FROM
messages_message m
INNER JOIN
messages_participants p ON p.conversation_id = m.conversation_id
WHERE
m.timestamp > p.last_read
AND p.user_id = 5
if you want to increase the query running time you can create a new index in message_participants (conversation_id, user_id) to index the conversations per users and then change your query with:
SELECT
COUNT(m.conversation_id) AS count
FROM
messages_message m
INNER JOIN
messages_participants p ON p.conversation_id = m.conversation_id AND p.user_id = 5
WHERE
m.timestamp > p.last_read
So that your DB engine can now filter the JOIN by simply looking at the index table. You could go deeper in this thought by indexing the timestampe too : (timestamp, conversation_id, user_id) and put the where condition in the join condition.
Whatever you choose, always put the most selective field first, to increase selectivity.
EDIT
First, let's comment your query:
SELECT
SQL_CALC_FOUND_ROWS c.*,
last_msg.*,
new_msgs.count as new_msgs_count
FROM
messages_conversation c
INNER JOIN
messages_participants p ON p.user_id = 5 -- Join with every conversations of user 5; if id is an integer, avoid writing '5' (string converted to an integer).
INNER JOIN
( -- Select every message : you could already select here messages from user 5
SELECT
*
FROM
messages_message m
ORDER BY -- this is not the goal of ORDER BY. Use MAX to obtain to latest timestamp.
m.timestamp DESC
LIMIT 1
) last_msg ON c.id = last_msg.conversation_id -- this query return one row and you want to have the latest timestamp for each conversation.
LEFT JOIN
(
SELECT
COUNT(m.id) AS count,
m.conversation_id,
m.timestamp
FROM
messages_message m
) new_msgs ON c.id = new_msgs.conversation_id AND new_msgs.timestamp > p.last_read
LIMIT 0,10
Let's rephrase your query:
select the number of new messages of a conversation subject, its last message and timestamp for user #id.
Do it step by step:
Selecting last message, timestamp in conversation for each user:
SELECT -- select the latest timestamp with its message
max(timestamp),
message
FROM
messages_message
GROUP BY
user_id
Aggregates functions (MAX, MIN, SUM, ...) work on the current group. Read this like "for each groups, calculate the aggregate functions, then select what I need where my conditions are true". So it will result in one row per group.
So this last query selects the last message and timestamp of every user in the messages_message table. As you can see, it is easy to select this value for a specific user adding the WHERE clause:
SELECT
MAX(timestamp),
message
FROM
messages_message
WHERE
user_id = #id
GROUP BY
user_id
Number of messages per conversation: for each conversation, count the number of messages
SELECT
COUNT(m.id) -- assuming id column is unique, otherwise count distinct value.
FROM
messages_conversation c
INNER JOIN -- The current user participated to the conversation
messages_participant p ON p.conversation_id = c.id AND p.user_id = #id
OUTER JOIN -- Messages of the conversation where the current user participated, newer than last read its time
messages_message m ON m.conversation_id = c.id AND m.timestamp > p.last_read = #id
GROUP BY
c.id -- for each conversation
INNER JOIN won't return rows for conversations where the current user did not participated.
Then OUTER JOIN will join with NULL columns if the condition is false, so that COUNT will return 0 - there is not new messages.
Putting it all together.
Select the last message and timestamp in conversation where the current user participated and the number of new messages in each conversation.
Which is a JOIN between the two last queries.
SELECT
last_msg.conversation_id,
last_msg.message,
last_msg.max_timestamp,
new_msgs.nb
FROM
(
SELECT
MAX(timestamp) AS max_timestamp,
message,
conversation_id
FROM
messages_message
WHERE
user_id = #id
GROUP BY
user_id
) last_msg
JOIN
(
SELECT
c.id AS conversation_id
COUNT(m.id) AS nb
FROM
messages_conversation c
INNER JOIN
messages_participant p ON p.conversation_id = c.id AND p.user_id = #id
OUTER JOIN
messages_message m ON m.conversation_id = c.id AND m.timestamp > p.last_read = #id
GROUP BY
C.id
) new_msgs ON new_msgs.conversation_id = last_msg.conversation_id
-- put here and only here a order by if necessary :)