MySQL Complex Query, user conversation - mysql

I have this query to get the last message between two users.
SELECT f.*
FROM
(
SELECT *
FROM messages a
WHERE (LEAST(a.sender, a.receiver), GREATEST(a.sender, a.receiver), a.timestamp)
IN (
SELECT LEAST(b.sender, b.receiver) AS x,
GREATEST(b.sender, b.receiver) AS y,
MAX(b.timestamp) AS msg_time
FROM messages b
GROUP BY x, y
)
) f
WHERE :user_id IN (f.sender, f.receiver)
ORDER BY f.timestamp DESC
I got it here in SO, and it is a bit complex for me.
I need to modify it to get the last message only if :user_id has not deleted the conversation.
The table has 4 important fields: sender, receiver, sender_deleted, receiver_deleted.
When a user want to delete a conversation, i run a query to set the deleted fields to 1.
Example of table where user 4 has deleted the conversation:
sender receiver sender_deleted receiver_deleted message
4 17 1 0 user 4 to 17 message
17 4 0 1 user 17 to 4 message
I need to modify the above query so if :user_id = 4 it return empty, but if :user_id = 17 return the last message sent.
I hope i explained well what i want to do.

This ought to do what you're looking for:
SELECT f.*
FROM
(
SELECT *
FROM messages a
WHERE (LEAST(a.sender, a.receiver), GREATEST(a.sender, a.receiver), a.timestamp)
IN (
SELECT LEAST(b.sender, b.receiver) AS x,
GREATEST(b.sender, b.receiver) AS y,
MAX(b.timestamp) AS msg_time
FROM messages b
GROUP BY x, y
)
) f
WHERE (:user_id = f.sender AND f.sender_deleted != 1)
OR (:user_id = f.receiver AND f.receiver_deleted != 1)
ORDER BY f.timestamp DESC

Related

How to reorganise the values of a groupBy from the same table (MYSQL)?

I have a Chats table setup like this:
ID
CHAT_GROUP_ID
MESSAGE
READ
CREATED_AT
1
uu-34uu5-6662
hi1
1
2022-06-02 13:16:42
2
uu-34uu5-6662
hi2
1
2022-06-02 13:16:45
3
uu-34uu5-6663
hi3
0
2022-06-02 13:16:46
4
uu-34uu5-6663
hi4
0
2022-06-02 13:16:47
5
uu-34uu5-6664
hi5
0
2022-06-02 13:16:49
ID = int
CHAT_GROUP_ID = Varchat(some kind of UUID)
MESSAGE = String
What I am trying to achieve is:
GROUP ALL THE CHAT_GROUP_ID with their own respective IDs.
When all the CHAT_GROUP_ID are grouped, SUM all the READ AS SUM(IF(read = 0, 1, 0))
Finally (and where I am struggling), always show only 1 MESSAGE but always the latest one.
I have am struggling so much on this unbelievable! How can I do this?
If you need it to be done in MySQL v5.*, you can use the following query:
SELECT tab.ID,
tab.CHAT_GROUP_ID,
tab.MESSAGE,
aggregated.SUM_READ_,
aggregated.MAX_TIME
FROM tab
INNER JOIN (SELECT CHAT_GROUP_ID,
SUM(IF(READ_=0,1,0)) AS SUM_READ_,
MAX(CREATED_AT) AS MAX_TIME
FROM tab
GROUP BY CHAT_GROUP_ID ) aggregated
ON tab.CHAT_GROUP_ID = aggregated.CHAT_GROUP_ID
AND tab.CREATED_AT = aggregated.MAX_TIME
First you create a view containing aggregated value of READ and CREATED_AT with respect to CHAT_GROUP_ID, then use these information to retrieve the needed infos from the main table.
Try it here.
Assuming at least MySql 8:
SELECT ct.Chat_Group_ID, ct.Message,
( SELECT SUM(Case when read = 1 then 0 else 1 end)
FROM Chats ct2
WHERE ct2.Chat_Group_ID = ct.Chat_Group_ID
) as Unread
FROM (
SELECT Chat_Group_ID, Message
, row_number() over (partition by Chat_Group_ID order by create_at desc) rn
FROM Chats
) ct
WHERE ct.rn = 1

Nested queries and Join

As a beginner with SQL, I’m ok to do simple tasks but I’m struggling right now with multiple nested queries.
My problem is that I have 3 tables like this:
a Case table:
id nd date username
--------------------------------------------
1 596 2016-02-09 16:50:03 UserA
2 967 2015-10-09 21:12:23 UserB
3 967 2015-10-09 22:35:40 UserA
4 967 2015-10-09 23:50:31 UserB
5 580 2017-02-09 10:19:43 UserA
a Value table:
case_id labelValue_id Value Type
-------------------------------------------------
1 3633 2731858342 X
1 124 ["864","862"] X
1 8981 -2.103 X
1 27 443 X
... ... ... ...
2 7890 232478 X
2 765 0.2334 X
... ... ... ...
and a Label table:
id label
----------------------
3633 Value of W
124 Value of X
8981 Value of Y
27 Value of Z
Obviously, I want to join these tables. So I can do something like this:
SELECT *
from Case, Value, Label
where Case.id= Value.case_id
and Label.id = Value.labelValue_id
but I get pretty much everything whereas I would like to be more specific.
What I want is to do some filtering on the Case table and then use the resulting id's to join the two other tables. I'd like to:
Filter the Case.nd's such that if there is serveral instances of the same nd, take the oldest one,
Limit the number of nd's in the query. For example, I want to be able to join the tables for just 2, 3, 4 etc... different nd.
Use this query to make a join on the Value and Label table.
For example, the output of the queries 1 and 2 would be:
id nd date username
--------------------------------------------
1 596 2016-02-09 16:50:03 UserA
2 967 2015-10-09 21:12:23 UserB
if I ask for 2 different nd. The nd 967 appears several times but we take the oldest one.
In fact, I think I found out how to do all these things but I can't/don't know how to merge them.
To select the oldest nd, I can do someting like:
select min((date)), nd,id
from Case
group by nd
Then, to limit the number of nd in the output, I found this (based on this and that) :
select *,
#num := if(#type <> t.nd, #num + 1, 1) as row_number,
#type := t.nd as dummy
from(
select min((date)), nd,id
from Case
group by nd
) as t
group by t.nd
having row_number <= 2 -- number of output
It works but I feel it's getting slow.
Finally, when I try to make a join with this subquery and with the two other tables, the processing keeps going on for ever.
During my research, I could find answers for every part of the problem but I can't merge them. Also, for the "counting" problem, where I want to limit the number of nd, I feel it's kind of far-fetch.
I realize this is a long question but I think I miss something and I wanted to give details as much as possible.
to filter the case table to eliminate all but oldest nds,
select * from [case] c
where date = (Select min(date) from case
where nd = c.nd)
then just join this to the other tables:
select * from [case] c
join value v on v.Case_id = c.Id
join label l on l.Id = v.labelValue_id
where date = (Select min(date) from [case]
where nd = c.nd)
to limit it to a certain number of records, there is a mysql specific command, I think it called Limit
select * from [case] c
join value v on v.Case_id = c.Id
join label l on l.Id = v.labelValue_id
where date = (Select min(date) from [case]
where nd = c.nd)
Limit 4 -- <=== will limit return result set to 4 rows
if you only want records for the top N values of nd, then the Limit goes on a subquery restricting what values of nd to retrieve:
select * from [case] c
join value v on v.Case_id = c.Id
join label l on l.Id = v.labelValue_id
where date = (Select min(date) from [case]
where nd = c.nd)
and nd In (select distinct nd from [case]
order by nd desc Limit N)
So finally, here is what worked well for me:
select *
from (
select *
from Case
join (
select nd as T_ND, date as T_date
from Case
where nd in (select distinct nd from Case)
group by T_ND Limit 5 -- <========= Limit of nd's
) as t
on Case.nd = t.T_ND
where date = (select min(date)
from Case
where nd = t.T_ND)
) as subquery
join Value
on Value.context_id = subquery.id
join Label
on Label.id = Value.labelValue_id
Thank you #charlesbretana for leading me on the right track :).

Select most recent record based on two conditions

I have user1 who exchanged messages with user2 and user4 (these parameters are known). I now want to select the latest sent or received message for each conversation (i.e. LIMIT 1 for each conversation).
SQLFiddle
Currently my query returns all messages for all conversations:
SELECT *
FROM message
WHERE (toUserID IN (2,4) AND userID = 1)
OR (userID IN (2,4) AND toUserID = 1)
ORDER BY message.time DESC
The returned rows should be messageID 3 and 6.
Assuming that higher id values indicate more recent messages, you can do this:
Find all messages that involve user 1
Group the results by the other user id
Get the maximum message id per group
SELECT *
FROM message
WHERE messageID IN (
SELECT MAX(messageID)
FROM message
WHERE userID = 1 -- optionally filter by the other user
OR toUserID = 1 -- optionally filter by the other user
GROUP BY CASE WHEN userID = 1 THEN toUserID ELSE userID END
)
ORDER BY messageID DESC
Updated SQLFiddle
You can do this easily by separating it into two queries with ORDER BY and LIMIT then joining them with UNION:
(SELECT *
FROM message
WHERE (toUserID IN (2,4) AND userID = 1)
ORDER BY message.time DESC
LIMIT 1)
UNION
(SELECT *
FROM message
WHERE (userID IN (2,4) AND toUserID = 1)
ORDER BY message.time DESC
LIMIT 1)
The parenthesis are important here, and this returns messages 2 and 6, which seems correct, not 3 and 6.
It also seems like you could use UNION ALL for performance instead of UNION because there won't be duplicates between the two queries, but it's better if you decide that.
Here's your data:
MESSAGEID USERID TOUSERID MESSAGE TIME
1 1 2 nachricht 1 123
2 1 2 nachricht 2 124
3 2 1 nachricht 3 125
4 3 2 nachricht wrong 1263
5 2 4 nachricht wrong 1261
6 4 1 nachricht sandra 126
The below works as required:
SELECT m1.*
FROM Message m1
LEFT JOIN Message m2
ON LEAST(m1.toUserID, m1.userID) = LEAST(m2.toUserID, m2.userID)
AND GREATEST(m1.toUserID, m1.userID) = GREATEST(m2.toUserID, m2.userID)
AND m2.time > m1.Time
WHERE m2.MessageID IS NULL
AND ( (m1.toUserID IN (2,4) AND m1.userID = 1)
OR (m1.userID IN (2,4) AND m1.toUserID = 1)
);
To simplify how this works, imagine you just wanted the latest message sent by userid 1, rather than having to match the to/from tuples as this adds clutter to the query that doesn't help. To get this I would use:
SELECT m1.*
FROM Message AS m1
LEFT JOIN Message AS m2
ON m2.UserID = m1.UserID
AND m2.time > m1.time
WHERE m1.UserID = 1
AND m2.MessageID IS NULL;
So, we are joining similar messages, stipulating that the second message (m2) has a greater time than the first, where m2 is null it means there is not a similar message with a later time, therefore m2 is the latest message.
Exactly the principal has been applied in the solution, but we have a more complicated join to link conversations.
I have used LEAST and GREATEST in the join, the theory being that since you have 2 members in your tuple (UserID, ToUserID), then in any combination the greatest and the least will be the same, e.g.:
From/To | Greatest | Least |
--------+-----------+-------+
1, 2 | 2 | 1 |
2, 1 | 2 | 1 |
1, 4 | 4 | 1 |
4, 1 | 4 | 1 |
4, 2 | 4 | 2 |
2, 4 | 4 | 2 |
As you can see, in similar From/To the greatest and the least will be the same, so you can use this to join the table to itself.
There are two parts of your query in the following order:
You want the latest outgoing or incoming message for a conversation between two users
You want these latest messages for two different pairs of users, i.e. conversations.
So, lets get the latest message for a conversation between UserID a and UserID b:
SELECT *
FROM message
WHERE (toUserID, userID) IN ((a, b), (b, a))
ORDER BY message.time DESC
LIMIT 1
Then you want these to be combined for the two conversations between UserIDs 1 and 2 and UserIDs 1 and 4. This is where the union comes into play (we do not need to check for duplicates, thus we use UNION ALL, thanks to Marcus Adams, who brought that up first).
So a complete and straightforward solution would be:
(SELECT *
FROM message
WHERE (toUserID, userID) IN ((2, 1), (1, 2))
ORDER BY message.time DESC
LIMIT 1)
UNION ALL
(SELECT *
FROM message
WHERE (toUserID, userID) IN ((4, 1), (1, 4))
ORDER BY message.time DESC
LIMIT 1)
And as expected, you get message 3 and 6 in your SQLFiddle.

MYSQL: Find all posts between certain users

I have two tables:
table message (holds the creator of a message and the message)
id - creatorId - msg
and a table message_viewers (tells who can read the message msgId)
msgId - userId
If I create a message as user 1 and send it to user 2 and user 3, the tables will look like this:
tbl_message:
1 - 1 - 'message'
tbl_message_viewers:
1 - 2
1 - 3
What I want to do is to fetch the messages that are between the users x1...xN (any number of users) AND ONLY the messages between them.
(Example if users are 1, 2, and 3, I want the messages where the creator is 1, 2 or 3, and the users are 2,3 for creator = 1, 1 and 3 for creator = 2 and 1, 2 for creator = 3)
I am not interested by messages between 1 and 2, or 2 and 3, or 1 and 3, but only by messages between the 3 people.
I tried different approaches, such as joining the two tables on message id, selecting the messages where creatorId IN (X,Y) and then taking only the rows where userId IN (X, Y) as well. Maybe something about grouping and counting the rows, but I could not figure out a way of doing this that was working.
EDIT: SQL Fiddle here
http://sqlfiddle.com/#!2/963c0/1
I think this might do what you want:
SELECT m.*
FROM message m
INNER JOIN message_viewers mv ON m.id = mv.msgId
WHERE m.creatorId IN (1, 2, 3)
AND mv.userId IN (1, 2, 3)
AND NOT EXISTS (
SELECT 1
FROM message_viewers mv2
WHERE mv2.msgId = mv.msgId
AND mv2.userId NOT IN (1, 2, 3)
)
AND mv.userId != m.creatorId;
The IN's will give the users that created/can see, and the mv.userId != m.creatorId are for excluding the creator from the message_viewers table (like you showed in your requirements).
Edit:
With the requirement of only sending messages between those 3 id's, i came up with the following:
SELECT m.id,m.creatorId,m.message
FROM message m
INNER JOIN message_viewers mv ON m.id = mv.msgId
WHERE m.creatorId IN (1, 2, 3)
AND mv.userId IN (1, 2, 3)
AND mv.userId != m.creatorId
AND NOT EXISTS (
SELECT 1
FROM message_viewers mv2
WHERE mv2.msgId = mv.msgId
AND mv2.userId NOT IN (1, 2, 3)
)
GROUP BY 1,2,3
HAVING COUNT(*) = 2;
sqlfiddle demo
Try this with join and with IN() clause
SELECT * FROM
tbl_message m
JOIN tbl_message_viewers mv (m.id = mv.msgId )
WHERE m.creatorId IN(1,2,3) AND mv.userId IN(1,2,3)
Sounds like you might want the BETWEEN operator:
SELECT * FROM tablename WHERE fieldname BETWEEN 1 AND 10;
-- returns fieldname 1-10
In this case however, BETWEEN is inclusive, so you'll need to specify != those conditions as well:
SELECT * FROM tablename WHERE fieldname BETWEEN 1 AND 10 AND fieldname NOT IN (1, 10)
-- returns fieldname 2-9
http://www.w3schools.com/sql/sql_between.asp
this worked on oracle
first join gets row count for people we are not interested in
second join gets row count for people we are interested in
the in clauses will need to be generated by some sort of dynamic sql
and number_of_people also needs to be generated somehow.
select msgId, count_1, count_2
from message tm
join ( select ty.msgId as ty_msgId,
count(ty.msgId) as count_1
from message_viewers ty
where ty.userId not in (:a,:b,:c)
group by ty.msgId)
on msgId = ty_msgId
join (select tz.msgId as tz_msgId,
count(tz.msgId) as count_2
from message_viewers tz
where tz.userId in (:a,:b,:c)
group by tz.msgId)
on msgId = tz_msgId
where createrId in(:a,:b,:c)
and count_1 = 0
and count_2 = :number_of_people -1;
my sql prefers this
select msgId, count_1, count_2
from message tm
left join ( select ty.msgId as ty_msgId,
count(ty.msgId) as count_1
from message_viewers ty
where ty.userId not in (:a,:b,:c)
group by ty.msgId) as X
on msgId = ty_msgId
left join (select tz.msgId as tz_msgId,
count(tz.msgId) as count_2
from message_viewers tz
where tz.userId in (:a,:b,:c)
group by tz.msgId) as Y
on msgId = tz_msgId
where createrId in(:a,:b,:c)
and (count_1 = 0 or count_1 is null)
and count_2 = :number_of_people -1;

Displaying messsage in specific format in mysql php

I have created a messaging system for users which will allow users to send message to another.
For this I have created two tables.
conversation(conversation_id,user_id1,user_id2)
messages(message_id,conversation_id,sender_id,receiver_id,message,created_time)
If users are talking first time, a conversation_id will be created with user_id1(who initiate chat) and user_id2(to who user_id1 is sending message)
Messages table will contain all info related to message.
Now what I want is, to create a message summary page where logged in user can view his all conversation list between other users order by created_time, group by conversation_id.
Here are table data:
conversation_id | user_id1 | user_id2
1 100 103
2 101 103
3 103 102
message_id| conversation_id| sender_id| receiver_id| message | created_time
1 1 100 103 MSG A 2012-06-08 08:38:57
2 1 103 100 MSG B 2012-06-08 08:39:40
3 2 101 103 MSG C 2012-06-08 08:40:20
4 3 102 103 MSG D 2012-06-08 08:41:10
And here is what output what I am looking for: Lets say logged in user is with id: 103
conversation_id| conversation_with | last_message | created_time
3 102 MSG D 2012-06-08 08:41:10
2 101 MSG C 2012-06-08 08:40:20
1 100 MSG B 2012-06-08 08:39:40
So this output is ordering by created_time, Grouping by conversation_id and displaying id of user in conversation_with , with who userid 103 is having conversation.
Can somebody provide MySQL query I will need to get this output.
This was a fun one. So, you have three main things to solve.
Alternate the conversation_with column depending on where the desired user shows up amongst two columns.
greatest-n-per-group to find the most recent message.
least-n-per-group to find the earliest message created_time.
The first one I solved by querying twice, alternating the user_id column, unioning the results, and then ordering the unioned results by created_time. I think I can solve this with one query and no unions, but it works for now.
2 and 3 are a little more involved. Here's a SQL fiddle with the query: http://sqlfiddle.com/#!2/bf2b7/1
TL;DNR
select * from (
select c.conversation_id, c.user_id2 as conversation_with, m1.message as last_message, m3.created_time
from conversation as c
join messages as m1 on c.conversation_id = m1.conversation_id
left outer join messages as m2 on (c.conversation_id = m2.conversation_id
and (m1.created_time < m2.created_time OR m1.created_time = m2.created_time AND m1.message_id < m2.message_id))
join messages as m3 on c.conversation_id = m3.conversation_id
left outer join messages as m4 on (c.conversation_id = m4.conversation_id
and (m3.created_time > m4.created_time OR m3.created_time = m4.created_time AND m3.message_id > m4.message_id))
where user_id1 = 103 and m2.message_id is null and m4.message_id is null
union all
select c.conversation_id, c.user_id1 as conversation_with, m1.message as last_message, m3.created_time
from conversation as c
join messages as m1 on c.conversation_id = m1.conversation_id
left outer join messages as m2 on (c.conversation_id = m2.conversation_id
and (m1.created_time < m2.created_time OR m1.created_time = m2.created_time AND m1.message_id < m2.message_id))
join messages as m3 on c.conversation_id = m3.conversation_id
left outer join messages as m4 on (c.conversation_id = m4.conversation_id
and (m3.created_time > m4.created_time OR m3.created_time = m4.created_time AND m3.message_id > m4.message_id))
where user_id2 = 103 and m2.message_id is null and m4.message_id is null
) as conversations
order by created_time desc