Querying conversations from messages table - mysql

I have the messages table formatted as follows:
+------------------------------------------------------------------+
| id|sender_id| recipient_id | message_text |created_at |
+------------------------------------------------------------------+
| 1 | 2 | 10 | "test1" |"2017-04-10 09:05:45" |
| 2 | 10 | 2 | "test2" |"2017-04-10 09:05:47" |
| 3 | 2 | 4 | "test3" |"2017-04-10 09:05:49" |
| 4 | 10 | 4 | "test4" |"2017-04-10 09:05:51" |
| 5 | 4 | 2 | "test5" |"2017-04-10 09:05:53" |
| 6 | 2 | 10 | "test6" |"2017-04-10 09:05:58" |
+------------------------------------------------------------------+
What I'm trying to do is get all the "conversations" of a logged in user (say user with id 2), along with the last messages for that conversation.
I managed to pull out the id's of users user2 has messages with using this query:
select distinct users.id
from messages, users where
(recipient_id = 2 and users.id = sender_id)
or
(sender_id = 2 and users.id = recipient_id);
What this yields is
4
10
as user2 has either sent and/or received messages from these two people (test1, test2, test6 for 10, and test3, test5 for 4).
What I can't do is modify this query so it also yields the last message sent to or received by the yielded id - for example
4 | test5
10 | test6

If I understand your requirement correctly, you want to obtain the most recent message date for every conversation involving a certain user. In this case, we can aggregate over conversations for a given user and retain the most recent message date.
SELECT m1.*
FROM messages m1
INNER JOIN
(
SELECT LEAST(sender_id, recipient_id) AS first,
GREATEST(sender_id, recipient_id) AS second,
MAX(created_at) AS recent_date
FROM messages
WHERE sender_id = 2 OR recipient_id = 2
GROUP BY LEAST(sender_id, recipient_id),
GREATEST(sender_id, recipient_id)
) m2
ON LEAST(m1.sender_id, m1.recipient_id) = m2.first AND
GREATEST(m1.sender_id, m1.recipient_id) = m2.second AND
m1.created_at = m2.recent_date
Output:
Explanation:
The challenge in this query is to find a way to group conversations between two users together. I used the LEAST/GREATEST trick, which is a way that we can treat a 2 -> 4 and 4 -> 2 conversation as being logically the same thing. Then, using GROUP BY, we can identify the most recent conversation date for that pair of conversing users. So the subquery in my answer above finds, for each pair of users, without regard of any order, that pair along with its most recent conversation date. We then join this result back to the messages table to bring in the actual latest message text.
Demo here:
Rextester

Use an order_by created_at desc statement at the end of your query to get the most recent messages.

Related

Using parent SQL column in subquery

Good morning. I'm trying to pull the username of the user from the column in to_id. Is there. It'd be simple if I was just filtering on to_id, but I also need records from another column from_id. I've attempted doing a UNION to get around this issue, but it only pulls records from user.id 3 of course.
Does anyone happen to know a way around this? I'm somewhat new to writing complex SQL queries. Haven't been able to figure much out from similar questions.
SELECT
users.username, -- Placeholder until username from to_id can be pulled
payment.id,
to_id,
amount,
state,
type,
timedate
FROM
payment
LEFT JOIN users ON users.id = payment.to_id AND users.id = payment.from_id
WHERE to_id = 3 OR from_id = 3
The result of that would be along the lines of:
+----------+----+-------+--------+----------+------+---------------------+
| username | id | to_id | amount | state | type | timedate |
+----------+----+-------+--------+----------+------+---------------------+
| NULL | 1 | 1 | 12.56 | COMPLETE | u2u | 2021-11-12 06:09:21 |
| NULL | 2 | 1 | 43.00 | COMPLETE | u2u | 2021-11-12 06:17:10 |
| NULL | 3 | 3 | 2.25 | COMPLETE | u2u | 2021-11-12 06:22:53 |
+----------+----+-------+--------+----------+------+---------------------+
Username is null due to the two Joins being AND. If it's OR, the username will show up, but the rows will be there twice. Once with the to_id username, once with the from_id username.
So you have one users table for all payers and payees accounts and one transaction table with two ID columns (payer and payee)? You need to join the users table to the transaction table twice, once to get the payer info, once to get the payee info.
select
payment.from_id,
from_user.username,
payment.to_id,
to_user.username,
payment.id,
amount,
state,
type,
timedate
from payment
left join users as from_user
on from_user.id = payment.from_id
left join users as to_user
on to_user.id = payment.to_id
where payment.to_id = 3 OR payment.from_id = 3

Creating a dynamic column on grouped mysql query

I have the following table in mysql
msg_id | thread_id | read_status
--------------------------------
1 | 1 | read
2 | 1 | read
3 | 2 | unread
4 | 2 | read
5 | 2 | read
6 | 3 | read
7 | 3 | unread
I want a query that will show me all messages grouped by thread_id and if they contain any unread messages I want the a column called read_status to show unread
so the query result would look like this:
thread_id | read_status
-----------------------
1 | read
2 | unread
3 | unread
So far i have:
SELECT
thread_id,
IF(user_read_status = 'U',"unread","read") as message_status FROM messages
GROUP BY thread_id
but this populates the "read_status" with the result of whatever the first message is, rather than if any of the messages are unread...
I have no idea about how to do this, can anyone help?
Thanks in advance
You need aggregation, such as:
select thread_id,
(case when sum(user_read_status = 'U') > 0 then 'unread' else 'read' end) as thread_status
from messages
group by thread_id;
If the message statuses are really "read" and "unread" (as in the sample data), you can take this shortcut:
select thread_id, max(message_status) as thread_status
from messages
group by thread_id;
This only works because you have two statuses and the alphabetical ordering has a meaning.

Get users who replied the first message of the conversation within 24hours

I need to find the users who replied the first message of the conversation (one to one conversation) within 24hours. I have a messages table where all data are stored.
Table: messages
id | sender_id | recipient_id | content | Created_at
1 | 1001 | 256 | Hi | 2017-03-20 22:37:30
2 | 256 | 1001 | Hello | 2017-03-21 20:29:10
3 | 1001 | 256 | XYZ | 2017-03-21 22:02:00
4 | 256 | 1001 | ??? | 2017-03-21 23:01:01
5 | 1002 | 500 | Hi there | 2017-03-22 10:10:10
6 | 1002 | 500 | Can you meet?| 2017-03-22 10:15:32
7 | 500 | 1002 | Yes | 2017-03-22 10:20:30
8 | 1003 | 600 | Hello world | 2017-03-23 01:00:00
9 | 1004 | 700 | Hi | 2017-03-23 08:10:10
10 | 700 | 1004 | hello | 2017-03-26 22:00:00
Expected result:
users
256
500
Example: Conversation between user 1001 and 256.
id | sender_id | recipient_id | content | Created_at
1 | 1001 | 256 | Hi | 2017-03-20 22:37:30
2 | 256 | 1001 | Hello | 2017-03-21 20:29:10
3 | 1001 | 256 | XYZ | 2017-03-21 22:02:00
4 | 256 | 1001 | ??? | 2017-03-21 23:01:01
Here 2 | 256 | 1001 | Hello | 2017-03-21 20:29:10 is the first replied message of the conversation and its replied within 24 hours.
I've tested this out and it works. It's much the same as the other answers though.
select messages.sender_id as users from (
select t.id1, t.id2, t.start, messages.sender_id as initiator,
messages.recipient_id as replier from (
select greatest(sender_id, recipient_id) as id1,
least(sender_id, recipient_id) as id2, min(Created_at) as start
from messages group by id1, id2
) as t left join messages on messages.Created_at = t.start
and ((messages.sender_id = t.id1 and messages.receiver_id = t.id2)
or (messages.sender_id = t.id2 and messages.receiver_id = t.id1))
) as t inner join messages on messages.sender_id = t.replier
and messages.recipient_id = t.initiator
and messages.Created_at < date_add(t.start, interval 1 day)
group by users;
The innermost query finds conversations by grouping messages by the two users involved, and finds the start of that conversation by taking the minimum Created_at.
The middle query finds the initiator and replier by looking up the first message in the conversation.
The outside query finds messages from the replier to the initiator (which are therefore in that conversation) within one day of the start of it, and groups by users so that they each appear only once (even if involved in multiple conversations).
Alright.
First, we need to define what a conversation is: a pair of (sender_id, recipient_id) exchanging messages. Determining the first message in a conversation is a bit tricky. We could do this:
SELECT sender_id, recipient_id, min(created_at) FROM messages
GROUP BY sender_id, recipient_id
However, this will give us the first two messages of each conversation instead. We still don't know who started it and who replied without looking at the date, but the data we get is all we need to answer the question. And it is likely to be fast, since I will assume an index on (sender_id, recipient_id, created_at).
Now, I see two ways to solve this. First one:
SELECT least(sender_id,recipient_id),
greatest(sender_id,recipient_id),
max(created_at) <= DATE_ADD( min(created_at), INTERVAL 1 DAY )
FROM (
SELECT sender_id, recipient_id, min(created_at) FROM messages
GROUP BY sender_id, recipient_id
) foo
GROUP BY least(sender_id,recipient_id),
greatest(sender_id,recipient_id)
HAVING count(*)=2;
least() and greatest() allow to create one id for each conversation from the sender and receiver ids. max() and min() will return the first message and its reply, since we have only 2 rows per conversation. And the having will remove messages without reply.
We could also use a temp table:
CREATE TEMPORARY TABLE foo (
sender_id INT NOT NULL,
recipient_id INT NOT NULL,
createdèat DATETIME NOT NULL
);
INSERT INTO foo
SELECT sender_id, recipient_id, min(created_at) FROM messages
GROUP BY sender_id, recipient_id
ALTER TABLE foo ADD PRIMARY KEY (sender_id,recipient_id);
SELECT ... substract a.created_at and b.created_at to get your 24h limit
FROM foo a
JOIN foo b ON ( a.sender_id=b.recipient_id
AND a.recipient_id=b.sender_id
AND a.created_at < b.created_at)
By joining the temp table to itself, we put together the first message and its reply in a single query, and we can compare their dates.
Taking a swing without testing, as I think the desired result is still unclear.
First, find the "first messages" of a conversation:
select m1.id
,m.sender_id
,m.recipient_id
,m.Created_at
from messages m1
inner join (
select m.sender_id
,m.recipient_id
,Min(m.Created_at) as first_message
from messages m
group by m.sender_id
,m.recipient_id
) m2
on m1.sender_id = m2.sender_id
and m1.m.recipient_id = m2m.recipient_id
and m1.Created_at = m2.first_message
If these are properly "first messages", then find any replies in 24 hours
select distinct m3.sender_id
from messages m3
inner join (
<the above first message select statement>
) fm
on m3.sender_id = fm.recipient_id
and m3.recipient_id = fm.sender_id
and m3.Created_at < DATEADD (HH , 24 , fm.Created_at)
where m3.Created_at > fm.Created_at
this return the last message in 24 hours between to users
select
cnv.id ,
cnv.sender_id,
cnv.recipient_id,
cnv.content,
cnv.Created_at
from
(
-- first create a table with costum id of conversaton
select
-- ex: 1001-256
concat(greatest(sender_id, recipient_id),'-',least(sender_id, recipient_id) ) as 'cnv_id', -- costum column for joining
id ,
sender_id,
recipient_id,
content,
Created_at
from message
) cnv
INNER JOIN
(
-- second extract the last date of every one to one coversation conversation
-- result ex : 1001-256 | 2017-03-21 23:01:01
SELECT
concat(greatest(sender_id, recipient_id),'-',least(sender_id, recipient_id) ) as 'cnv_id', -- costum column for joining
max(Created_at) 'max_date'
group by cnv_id
) max_cnv ON cnv.cnv_id = max_cnv.cnv_id -- join the two result by the custom cnv_id
WHERE
-- here we extract only the record that there Created_at is > 24 hours from the max date
-- you can make this condition as you want but i think this will work
(max_cnv.max_date - cnv.Created_at)/1000/60 >= 24;

MySQL SELECT Multiple DISTINCT COUNT

Here is what I'm trying to do. I have a table with user assessments which may contain duplicate rows. I'm looking to only get DISTINCT values for each user.
In the example of the table below. If only user_id 1 and 50 belongs to the specific location, then only the unique video_id's for each user should be returned as the COUNT. User 1 passed video 1, 2, and 1. So that should only be 2 records, and user 50 passed video 2. So the total for this location would be 3. I think I need to have two DISTINCT's in the query, but am not sure how to do this.
+-----+----------+----------+
| id | video_id | user_id |
+-----+----------+----------+
| 1 | 1 | 1 |
| 2 | 2 | 50 |
| 3 | 1 | 115 |
| 4 | 2 | 25 |
| 5 | 2 | 1 |
| 6 | 6 | 98 |
| 7 | 1 | 1 |
+-----+----------+----------+
This is what my current query looks like.
$stmt2 = $dbConn->prepare("SELECT COUNT(DISTINCT user_assessment.id)
FROM user_assessment
LEFT JOIN user ON user_assessment.user_id = user.id
WHERE user.location = '$location'");
$stmt2->execute();
$stmt2->bind_result($video_count);
$stmt2->fetch();
$stmt2->close();
So my query returns all of the count for that specific location, but it doesn't omit the non-unique results from each specific user.
Hope this makes sense, thanks for the help.
SELECT COUNT(DISTINCT ua.video_id, ua.user_id)
FROM user_assessment ua
INNER JOIN user ON ua.user_id = user.id
WHERE user.location = '$location'
You can write a lot of things inside a COUNT so don't hesitate to put what you exactly want in it. This will give the number of different couple (video_id, user_id), which is what you wanted if I understood correctly.
The query below joins a sub-query that fetches the distinct videos per user. Then, the main query does a sum on those numbers to get the total of videos for the location.
SELECT
SUM(video_count)
FROM
user u
INNER JOIN
( SELECT
ua.user_id,
COUNT(DISTINCT video_id) as video_count
FROM
user_assessment ua
GROUP BY
ua.user_id) uav on uav.user_id = u.user_id
WHERE
u.location = '$location'
Note, that since you already use bindings, you can also pass $location in a bind parameter. I leave this to you, since it's not part of the question. ;-)

How to make a query to GROUP BY x DESC

The following SELECT statement
select *
from messages
where receiverID = '5'
group BY senderID
order by id DESC
database:
id | senderID | receiverID | message
1 | 245 | 5 | test 1
2 | 89 | 5 | test 2
3 | 79 | 5 | test 3
4 | 245 | 5 | test 4
5 | 245 | 5 | test 5
For senderID=245 I expected to return the row with id=5 , but it dosent it returns row with id=1, but i want the last row. How to achieve that ?
returns:
id | senderID | receiverID | message
1 | 245 | 5 | test 1
2 | 89 | 5 | test 2
3 | 79 | 5 | test 3
Ohh I made it :D
so this is the code that worked,for anyone with similar question
SELECT * FROM ( SELECT * FROM messages WHERE
receiverID = '5' ORDER BY id DESC) AS m GROUP BY senderID ORDER BY id DESC
This is not possible. You have to do something like:
[...] WHERE `id` = (SELECT MAX(`id`) FROM `messages` WHERE `receiverID` = '5')
Personally I'd consider a subquery, something along the lines of this should do the job for you
SELECT messagesOrdered.*
FROM (
SELECT *
FROM messages
WHERE receiverID = '5'
ORDER BY id DESC
) AS messagesOrdered
GROUP BY senderID
You may wish to check what keys you have set up depending on how large the table is.
The problem with using MAX is that if you use MAX on the id field then it will get the number you are looking for, however using MAX on another field does not get the data that matches that id. Using the subquery method, the inner query is doing the sorting and then the GROUP on the outside will group based on the order of rows in the inner query.
SELECT * FROM messages m
JOIN
( SELECT senderID, MAX(id) AS last
FROM messages
WHERE receiverID = '5'
GROUP BY senderID ) mg
ON m.id = mg.last
Not sure I understand your question completely, but it sounds to me like you want:
select max(id),
senderId,
max(receiverId),
max(message)
from messages
where receiverID = '5'
group BY senderID
order by id DESC
Note that you need to include message into your aggregate as well, otherwise you'll get unpredicatable results (other DBMS wouldn't allow leaving out the max(message) but MySQL will simply return a random row from the group).
Here it goes mine :)
select m1.* from messages m1
left join messages m2
on m1.senderid = m2.senderid and m1.id < m2.id
where m2.id is null and receiverID = '5'
Given your example this would return:
+----+----------+------------+---------+
| ID | SENDERID | RECEIVERID | MESSAGE |
+----+----------+------------+---------+
| 2 | 89 | 5 | test 2 |
| 3 | 79 | 5 | test 3 |
| 5 | 245 | 5 | test 5 |
+----+----------+------------+---------+