SQL get last messages from/to certain user - mysql

I'm trying to make SQL query from table,
- get last messages from all pairs of users with user 36 as sender or recipient, and join them with users table to get names. I've managed to create something like this, but still want to ask if there is more simple | efficient solution.
Mysql version - 5.5.31
table: messages
fields:
sender_user_id, recipient_user_id, date, text
query:
SELECT
*
FROM
(SELECT
(CASE sender_user_id > recipient_user_id
WHEN true THEN CONCAT(recipient_user_id, '|', sender_user_id)
WHEN false THEN CONCAT(sender_user_id, '|', recipient_user_id)
END) as hash,
sender_user_id,
recipient_user_id,
date,
text,
u.first_name
FROM
fed_messages
LEFT JOIN fed_users as u ON ((fed_messages.sender_user_id = 36 AND fed_messages.recipient_user_id = u.id) OR (fed_messages.recipient_user_id = 36 AND fed_messages.sender_user_id = u.id))
WHERE
sender_user_id = 36 OR recipient_user_id = 36
ORDER BY date DESC) as main
GROUP BY hash;
Thanks.
Upd. Data from sample messages table:
mysql> SELECT id, sender_user_id, recipient_user_id, text, date FROM federation.fed_messages WHERE id > 257;
-----+----------------+-------------------+-----------------+---------------------+
| id | sender_user_id | recipient_user_id | text | date |
+-----+----------------+-------------------+-----------------+---------------------+
| 258 | 40 | 36 | and one more | 2013-06-06 10:57:17 |
| 259 | 36 | 38 | another message | 2013-06-06 11:03:49 |
| 260 | 38 | 36 | some mes | 2013-06-06 12:29:33 |
| 261 | 38 | 36 | message | 2013-06-06 12:29:53 |
| 262 | 36 | 38 | message | 2013-06-06 12:47:26 |
| 263 | 36 | 40 | some message | 2013-06-10 16:22:46 |
The result should be with ids - 262, 263

I'm using SQL Server 2008, you don't say which database you are using.
From the information you have supplied your query seems overly complex for the output you require. Here's a simple query to get all the messages involving user 36:
SELECT
sender.msg_user_name AS sender_user_name
,recipient.msg_user_name AS recipient_user_name
,msg_date
,msg_text
FROM
dbo.Fed_Messages
INNER JOIN dbo.Fed_User AS sender
ON sender.msg_user_id = sender_user_id
INNER JOIN dbo.Fed_User AS recipient
ON recipient.msg_user_id = recipient_user_id
WHERE
sender_user_id = 36
OR recipient_user_id = 36
ORDER BY
msg_date DESC
I've had to change some field names as in SQL Server some of the names you have chosen are reserved words.
SQL Fiddle: http://sqlfiddle.com/#!3/b8e88/1
EDIT:
Now you've added some more information, and shown there is an id field on the message table, you could use something like this (note: I have SQL Server so you will probably have to change the query for MySQL):
SELECT sender.msg_user_name AS sender_user_name
,recipient.msg_user_name AS recipient_user_name
,msg_date
,msg_text
FROM dbo.Fed_Messages
INNER JOIN dbo.Fed_User AS sender ON sender.msg_user_id = sender_user_id
INNER JOIN dbo.Fed_User AS recipient ON recipient.msg_user_id = recipient_user_id
INNER JOIN ( SELECT MAX(id) AS most_recent_message_id
FROM dbo.Fed_Messages
GROUP BY CASE WHEN sender_user_id > recipient_user_id
THEN recipient_user_id
ELSE sender_user_id
END -- low_id
,CASE WHEN sender_user_id < recipient_user_id
THEN recipient_user_id
ELSE sender_user_id
END -- high_id
) T ON T.most_recent_message_id = dbo.Fed_Messages.id
WHERE sender_user_id = 36
OR recipient_user_id = 36
ORDER BY msg_date DESC
The SELECT in the FROM part of the query finds the most recent message (based on the id, I'm assuming it's an auto incrementing number) for each ordered pair of sender/recipient user id's. The result of that is rejoined to the Fed_Messages table to ensure we get the names for sender/receiver correct.
Updated SQL Fiddle: http://sqlfiddle.com/#!3/1f07a/2

Related

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;

Return latest entry result providing there is a review or close entry

I have a table that holds the answers to a question which is asked at entry to the system, at review periods and then at closure. The client can be opened and closed multiple times during their life on the system.
I am trying to get the latest 'entry' result from the table which also has either an associated 'review' or 'close' result.
This is my table (I have just included 1 user but the actual table has thousands of users):
row | user_id | answer | type | date_entered |
----+---------+--------+--------+--------------+
1 | 12 | 3 | entry | 2016-03-13 |
2 | 12 | 1 | review | 2016-03-14 |
3 | 12 | 7 | review | 2016-03-16 |
4 | 12 | 7 | close | 2016-03-17 |
5 | 12 | 8 | entry | 2016-03-20 |
6 | 12 | 2 | review | 2016-03-21 |
7 | 12 | 3 | close | 2016-03-22 |
8 | 12 | 1 | entry | 2016-03-28 |
So for this table the query would just return row 5 because the 'entry' on row 8 doesn't have any 'review' or 'closure' records after it.
Hopefully that makes sense.
SELECT a.*
FROM my_table a
JOIN
( SELECT x.user_id
, MAX(x.date_entered) date_entered
FROM my_table x
JOIN my_table y
ON y.user_id = x.user_id
AND y.date_entered > x.date_entered
AND y.type IN ('review','close')
WHERE x.type = 'entry'
GROUP
BY x.user_id
) b
ON b.user_id = a.user_id
AND b.date_entered = a.date_entered;
Basically you can seperate your query into two sub-queries. First query should get lastest record id (review and closure). Second query should have row_id > found_id.
SELECT *
FROM my_table
WHERE type = 'entry'
AND row_id > (SELECT Max(row_id)
FROM my_table
WHERE ( type = 'review'
OR type = 'close' ))
Please be careful about that; subquery may return zero-set.
I could think of several ways of doing it. But first a note: your date_entered field seems to be just a date. To tell which occurs "later" I'm going to use row because e.g. if both entry and review occurred on the same date, it's not possible to tell from the date_entered which one was later.
I just list a couple of solutions. The first one might be more efficient, but you should measure.
Here's a join against a subquery:
SELECT
m1.*
FROM
mytable m1
JOIN (SELECT
row, user_id
FROM
mytable
WHERE
type IN ('review', 'close') AND
user_id = 12
ORDER BY row DESC LIMIT 1) m2 ON m1.user_id = m2.user_id
WHERE
m1.user_id = 12 AND
m1.row < m2.row
ORDER BY
row DESC LIMIT 1
Here's a subquery for max:
SELECT
*
FROM
mytable
WHERE
row = (SELECT
MAX(m1.row)
FROM
mytable m1,
mytable m2
WHERE
m1.user_id = m2.user_id AND
m1.type = 'entry' AND
m2.type IN ('review', 'close') AND
m1.row < MAX(m2.row))

group by order by max()

I'm using MySQL.
The result I want, is to display the row with the highest 'time' where 'res' = 'hans', and group the 'frm'.
I am trying to fiddle around with GROUP BY, ORDER BY, MAX(time) - and I'm going no where.
My table: 'messages'
| frm | res | time | msg | opnd |
| poul | hans | 0916 | hi there | 1 |
| john | hans | 1033 | waz up | 1 |
| hans | john | 1140 | new text | 0 |
| poul | john | 1219 | message | 0 |
| poul | hans | 1405 | respond | 0 |
| john | hans | 1544 | write | 0 |
The result I want:
poul - hans - 1405 - respond - 0
john - hans - 1544 - write - 0
The result I get:
poul - hans - 1405 - hi there - 1
john - hans - 1544 - waz up - 1
I'm getting the correct 'time' but the wrong 'msg' and 'opnd'.
My code:
SELECT frm, res, MAX(time), msg, opnd
FROM messages
WHERE res = 'hans'
GROUP BY frm
ORDER BY time DESC
There are a couple ways to do this. One is to use a subquery and join back to the original table:
SELECT m.*
FROM messages m
JOIN (
SELECT frm, res, MAX(time) maxtime
FROM messages
WHERE res = 'hans'
GROUP BY frm, res) m2 on m.frm = m2.frm
and m.res = m2.res
and m.time = m2.maxtime
ORDER BY m.time DESC
Mysql allows you to omit fields from the group by clause that are not used in aggregation (a mistake imo -- most other databases do not allow this behavior). By allowing it, it just returns a random result though which is what you're experiencing.
Here'a another approach using an outer join, but I think the previous is easier to understand:
select m.*
from messages m
left join messages m2 on m.frm = m2.frm
and m.res = m2.res
and m2.time > m.time
where m2.frm is null
and m.res = 'hans'
order by m.time desc
Fiddle Demo With Both
your problem is that you are grouping by one column, but you select several columns with it. As a result, for the other non-grouped by columns you will get just one of the results, not nececarily the one which belongs to the max(time) value.
you need something like:
select a.frm, a.res, b.max_time, a.msg, a.opnd from
messages as a inner join
(SELECT frm, MAX(time) as max_time
FROM messages
WHERE res = 'hans'
GROUP BY frm) on a.frm = b.frm and a.time = b.max_time
ORDER BY time DESC

Return max join result only if ID matches

I'm attempting to build a query that will select all tenants of a rental property. Each property can have multiple tenants, and each tenant can be moved to a different property.
Here's what (part of) the user property table looks like:
User ID | Property ID | Move In Date | Move Out Date
----------------------------------------------------------
224 | 33A | 2015-11-01 | NULL
224 | 36B | 2015-11-15 | NULL
226 | 33A | 2015-11-01 | NULL
In the example above User 224 moved into property 33A on the 1st, then property 36B on the 15th. For this issue, and example, I'm assuming that the move_out_date column did not get properly populated with a date. In this instance I need a query that will bypass that issue.
Here's what I have for my query so far:
SELECT
u.user_email, p.user_fname, p.user_lname, r.move_in_date AS occupant_date
FROM
user u
LEFT JOIN
user_profile p ON p.user_id = u.user_id
LEFT JOIN
(
SELECT
user_id, property_id, move_in_date
FROM
user_property
WHERE
move_out_date IS NULL
ORDER BY
move_in_date DESC
) r ON r.user_id = u.user_id
WHERE
r.property_id = '33A'
GROUP BY
u.user_id
ORDER BY
r.move_in_date ASC, p.user_fname ASC
This query returns each occupants info, and move in date. For property 33A it is returning Users 224 and 226, but I only want to return User 226 because technically User 224 moved from that property.
Currently the output table looks like this:
User ID | Property ID | Move In Date | Move Out Date
----------------------------------------------------------
224 | 33A | 2015-11-01 | NULL
226 | 33A | 2015-11-01 | NULL
User ID | Email | First Name | Last Name | Occupant Date
---------------------------------------------------------------------
224 | user1#e.com | Kevin | Doe | 2015-11-01
226 | user2#e.com | Tom | Smith | 2015-11-01
But I'd only like the results for User 226 specifically for property ID 33A. Ultimately, this...
User ID | Email | First Name | Last Name | Occupant Date
---------------------------------------------------------------------
226 | user2#e.com | Tom | Smith | 2015-11-01
Is this possible to do in one query, or will I need to make a couple of them? I appreciate any help on this!
One way for getting the current tenant for a given property:
select p.user_id, p.property_id, p.move_in_date from
user_property p
join (
-- Get users most recent move in date.
select max(move_in_date) move_in_date, user_id
from user_property
group by user_id
) u_move_in on (p.user_id = u_move_in.user_id and p.move_in_date = u_move_in.move_in_date)
join (
-- Get the property's most recent move in date.
select max(move_in_date) move_in_date, property_id
from user_property
where property_id = '33A'
group by property_id
) p_move_in on (p.property_id = p_move_in.property_id and p.move_in_date = p_move_in.move_in_date);
Find the user(s) with most recent move in date = property most recent move in date.

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 |
+----+----------+------------+---------+