Group messages by latest response in conversation threading - mysql

I need a simple internal messaging system between users.
My tables:
+--------------+ +---------------------+
| messages | | users |
+----+---------+ +---------------------+
| id | message | | id | username | ...
+----+---------+ +---------------------+
+------------------------------------------------------------------------------+
| users_messages |
+------------------------------------------------------------------------------+
| id | from_usr_id | to_usr_id | msg_id | thread_id | read | sent_at | read_at |
+------------------------------------------------------------------------------+
INT 'thread_id' represents the conversation thread, its used to group messages.
BOOLEAN 'read' represents if the user opened/viewed the message or not.
I want to group messages by 'thread_id', sorted by 'sent_at' so I can show the user his latest messages by thread. I want also to count the messages in each thread.
I want to get something like this for a specific user id:
+----------------------------------------------------------------------------
| last_messages_by_conversation
+----------------------------------------------------------------------------
| message | from_username | sent_at | count_thread_msgs | count_unread_msg |
+----------------------------------------------------------------------------
TEXT 'message' is the latest message in the specific 'thread_id'
VARCHAR 'from_username' and DATETIME 'sent_at' are related to the latest message.
INT 'count_thread_msgs' and INT 'count_unread_msg' are related to the thread, representing the total number of messages and the number of unread messages in the thread.
Each row represents a thread/conversation (group by 'thread_id'), showing the last message (sorted by 'sent_at') for that specific thread.

You are looking for the groupwise maximum, which can be found by first grouping the users_messages table by thread_id and selecting MAX(sent_at), then joining the result back onto the users_messages table to find the other fields of that maximum record.
I find that NATURAL JOIN is a very handy shortcut here:
SELECT messages.message,
users.username AS from_username,
t.sent_at,
t.count_thread_msgs,
t.count_unread_msg
FROM users_messages NATURAL JOIN (
SELECT thread_id,
to_usr_id,
MAX(sent_at) AS sent_at,
COUNT(*) AS count_thread_msgs,
SUM(NOT read) AS count_unread_msg
FROM users_messages
WHERE to_usr_id = ?
GROUP BY thread_id
) t JOIN messages ON messages.id = users_messages.msg_id
JOIN users ON users.id = users_messages.from_usr_id

SELECT
users.id,
users.username,
user_messages.thread_id,
user_messages.unread ,
messages.message
FROM users
LEFT JOIN (SELECT
from_usr_id ,
msg_id,
count(thread_id)) as thread_id,
count(read_at) as unread
FROM user_messages)as user_messages on user_messages.from_usr_id = users.id
LEFT JOIN messages on messages.id = user_messages.msg_id

You can try this solution:
SELECT c.message,
d.username AS from_username,
b.sent_at,
a.count_thread_msgs,
a.count_unread_msg
FROM (
SELECT MAX(id) AS maxid,
COUNT(*) AS count_thread_msgs,
COUNT(CASE WHEN `read` = 0 AND <uid> = to_usr_id THEN 1 END) AS count_unread_msg
FROM users_messages
WHERE <uid> IN (from_usr_id, to_usr_id)
GROUP BY thread_id
) a
JOIN users_messages b ON a.maxid = b.id
JOIN messages c ON b.msg_id = c.id
JOIN users d ON b.from_usr_id = d.id
ORDER BY b.sent_at DESC
This gets the latest message in each thread that the user <uid> started or is a part of.
The latest message is based on the highest id of each thread_id.
This solution makes the following assumptions:
The id in users_messages is a unique auto-incrementing int for each new row.
Each thread contains correspondence between never more than two users.
If the thread can contain more than two users, then the query will need to be slightly adjusted so as to derive an accurate count aggregation.

Try this and let me know, change $$ for your user ID..
select u.username,msg.message,m.sent_at,
(select count(*) from user_message where read=0 and to_usr_id=$$) as count_thread_msgs,
(select count(*) from user_message where to_usr_id= $$) as count_unread_msg
from users as u join user_messages as m
on u.id=m.id where u.id=$$
join messages as msg on msg.id=m.id
group by u.id;`

Try this query -
SELECT
m.message,
u.username from_username,
um1.sent_at,
um2.count_thread_msgs,
um2.count_unread_msg
FROM users_messages um1
JOIN (
SELECT
thread_id,
MAX(sent_at) sent_at,
COUNT(*) count_thread_msgs,
COUNT(IF(`read` = 1, `read`, NULL)) count_unread_msg
FROM users_messages GROUP BY thread_id) um2
ON um1.thread_id = um2.thread_id AND um1.sent_at = um2.sent_at
JOIN messages m
ON m.id = um1.msg_id
JOIN users u
ON u.id = um1.from_usr_id
-- WHERE u.id = 100 -- specify user id here
Answers on your questions:
About last datetime: I have changed query a little, just try new one.
About specific users: Add WHERE condition to filter users - ...WHERE u.id = 100.
About many records: because you join another tables (messages and users), and there can be more then one record with the same thread_id. To avoid this you should group result set by thread_id field and use aggregate function to get single result, e.g. using GROUP_CONCAT function.

Related

Selecting a count of rows having a max value

Working example: http://sqlfiddle.com/#!9/80995/20
I have three tables, a user table, a user_group table, and a link table.
The link table contains the dates that users were added to user groups. I need a query that returns the count of users currently in each group. The most recent date determines the group that the user is currently in.
SELECT
user_groups.name,
COUNT(l.name) AS ct,
GROUP_CONCAT(l.`name` separator ", ") AS members
FROM user_groups
LEFT JOIN
(SELECT MAX(added), group_id, name FROM link LEFT JOIN users ON users.id = link.user_id GROUP BY user_id) l
ON l.group_id = user_groups.id
GROUP BY user_groups.id
My question is if the query I have written could be optimized, or written better.
Thanks!
Ben
You actual query is not giving you the answer you want; at least, as far as I understand your question. John actually joined group 2 on 2017-01-05, yet it appears on group 1 (that he joined on 2017-01-01) on your results. Note also you're missing one Group 4.
Using standard SQL, I think the next query is what you're looking for. The comments in the query should clarify what each part is doing:
SELECT
user_groups.name AS group_name,
COUNT(u.name) AS member_count,
group_concat(u.name separator ', ') AS members
FROM
user_groups
LEFT JOIN
(
SELECT * FROM
(-- For each user, find most recent date s/he got into a group
SELECT
user_id AS the_user_id, MAX(added) AS last_added
FROM
link
GROUP BY
the_user_id
) AS u_a
-- Join back to the link table, so that the `group_id` can be retrieved
JOIN link l2 ON l2.user_id = u_a.the_user_id AND l2.added = u_a.last_added
) AS most_recent_group ON most_recent_group.group_id = user_groups.id
-- And get the users...
LEFT JOIN users u ON u.id = most_recent_group.the_user_id
GROUP BY
user_groups.id, user_groups.name
ORDER BY
user_groups.name ;
This can be written in a more compact way in MySQL (abusing the fact that, in older versions of MySQL, it doesn't follow the SQL standard for the GROUP BY restrictions).
That's what you'll get:
group_name | member_count | members
:--------- | -----------: | :-------------
Group 1 | 2 | Mikie, Dominic
Group 2 | 2 | John, Paddy
Group 3 | 0 | null
Group 4 | 1 | Nellie
dbfiddle here
Note that this query can be simplified if you use a database with window functions (such as MariaDB 10.2). Then, you can use:
SELECT
user_groups.name AS group_name,
COUNT(u.name) AS member_count,
group_concat(u.name separator ', ') AS members
FROM
user_groups
LEFT JOIN
(
SELECT
user_id AS the_user_id,
last_value(group_id) OVER (PARTITION BY user_id ORDER BY added) AS group_id
FROM
link
GROUP BY
user_id
) AS most_recent_group ON most_recent_group.group_id = user_groups.id
-- And get the users...
LEFT JOIN users u ON u.id = most_recent_group.the_user_id
GROUP BY
user_groups.id, user_groups.name
ORDER BY
user_groups.name ;
dbfiddle here

MySQL: Joining 4 tables with correct results

I am have four tables in my DB for a simple forum that I am coding.
Topics:
topic_ID | name | description
Threads:
thread_ID | topic_ID | name | description
Messages:
message_ID | thread_ID | title | message | date | user_ID
Users:
user_ID | name | email | username
I want to run one query to do the following:
Display the available topics, the number of threads associated with each topic, the number of messages associated with each topic, the date latest message posted and the user who posted it.
So one row of the result would say something like:
Topic: Admin
Threads: 4
Posts: 50
Newest message: 2016/05/18 by pixelled
I started with this (which worked):
SELECT topics.topic_id, topics.name, count(threads.topic_id) AS 'totals'
FROM topics
LEFT JOIN threads
ON topics.topic_id = threads.topic_id
GROUP BY threads.topic_id
I then added the messages table:
SELECT topics.topic_id, topics.name, count(threads.topic_id) AS 'totals', MAX(messages.date) AS 'Newest'
FROM topics
LEFT JOIN threads
ON topics.topic_id = threads.topic_id
LEFT JOIN messages
ON messages.thread_id = threads.thread_id
GROUP BY threads.topic_id
But the results of this query show the wrong values for the totals column.
Adding the users table works:
SELECT topics.topic_id, topics.name, count(threads.topic_id) AS 'totals', MAX(messages.date) AS 'Newest', users.username
FROM topics
LEFT JOIN threads
ON topics.topic_id = threads.topic_id
LEFT JOIN messages
ON messages.thread_id = threads.thread_id
LEFT JOIN users
ON users.user_ID = messages.user_ID
GROUP BY threads.topic_id
Please help me to complete this query so that the correct value shows in the totals column.
Here is the fiddle:
http://sqlfiddle.com/#!9/0f926
Try
count (distinct threads.thread_id)
This was the query that gave the correct results:
SELECT topics.topic_id, topics.name, topics.description, count(threads.topic_id) AS 'totals', MAX(m.date) AS 'Newest', users.username
FROM topics
LEFT JOIN threads
ON topics.topic_id = threads.topic_id
LEFT JOIN
(
SELECT thread_id, date, user_id
FROM messages
GROUP BY thread_id
) m
ON m.thread_id = threads.thread_id
LEFT JOIN users
ON users.id = m.user_id
GROUP BY threads.topic_id
It was duplicating the threads table because it was joined twice.

MySQL database: Query table and join with "valid from" date column of other table

I cannot come up with a valid SQL Query for my problem, which I will describe in the scenario of a Blog. I have three tables: User, Blog – for blog entries, and UserStatus which holds the users' status he is assigned.
The tables look like that:
User
ID | Name
Blog
ID | User_ID | Date | Text
UserStatus
ID | User_ID | Valid_From_Date | Status
I guess you can imagine what entries of User and Blog would look like. Here is how UserStatus could look for one user:
ID | User_ID | Valid_From_Date | Status
34 |  7 | 2012-01-01 | Basic
35 |  7 | 2013-04-01 | Premium
36 |  7 | 2014-08-01 | Gold
The user's valid status at a certain date is the most recent one which satisfies Valid_From_Date<=Date. So on '2014-03-30' the valid status of this user is 'Premium'.
Now, what I want is to get all blog entries together with the users' names and valid status.
I have this approach:
SELECT User.Name, UserStatus.Status, Blog.Date, Blog.Text
FROM Blog
JOIN User ON User.ID = Blog.User_ID
JOIN UserStatus ON User.ID = UserStatus.User_ID
JOIN (Select User_ID, max(Valid_From_Date) AS date_for_most_recent_status FROM UserStatus
WHERE date_for_most_recent_status <= ??? GROUP BY User_ID) AS recent_user_status
ON recent_user_status.User_ID = UserStatus.User_ID
AND date_for_most_recent_status = UserStatus.Valid_From_Date
??? -> Can I relate to the particular Blog.Date of current entry when joining?
And that approach:
SELECT User.Name, UserStatus.Status, Blog.Date, Blog.Text, max(Valid_From)
FROM Blog
JOIN User ON User.ID = Blog.User_ID
JOIN UserStatus ON User.ID = UserStatus.User_ID
WHERE UserStatus.Valid_From_Date <= Blog.Date
GROUP BY Blog.Date, User.Name, Blog.Text
Here the good thing is that I can relate to the actual Blog.Date since it is just on Select-Statement. However, I don't know how to handle UserStatus.Status, which should be in the GROUP BY expression but cannot be, since I just want the most recent one.
Can anyone help me out here, please?
Correlated subquery taking the value of a column in a table and mapping it to a value in a sub table. In this case we know we want the max valid_from_Date for each user so we use the userID from an table outside the subequery and the userID on the table inside the subquery and return just the max and using that as the criteria to determine which user status record to limit by on the join.
SELECT User.Name, UserStatus.Status, Blog.Date, Blog.Text
FROM Blog
JOIN User
ON User.ID = Blog.User_ID
JOIN UserStatus
ON User.ID = UserStatus.User_ID
and Valid_from_date = (Select max(Valid_From_Date)
FROM UserStatus
where user_ID = User.ID
and UserStatus.Valid_from_Date <= Blog.Date)
If you want data user wise then use below:
SELECT User.Name, a.Status, Blog.Date, Blog.Text, a.valid_from_date
FROM Blog
JOIN USER ON User.ID = Blog.User_ID
JOIN
(SELECT user_id,`status`,valid_from_date FROM userstatus ORDER BY valid_from_date DESC) a
ON a.user_id=User.ID
WHERE a.Valid_From_Date <= Blog.Date
GROUP BY a.user_id;
If you want blog date, user, text wise then use:
SELECT User.Name, a.Status, Blog.Date, Blog.Text, a.valid_from_date
FROM Blog
JOIN USER ON User.ID = Blog.User_ID
JOIN
(SELECT user_id,`status`,valid_from_date FROM userstatus ORDER BY valid_from_date DESC) a
ON a.user_id=User.ID
WHERE a.Valid_From_Date <= Blog.Date
GROUP BY Blog.Date, a.user_id, Blog.Text;

select two logins from one record

I'm creating chat ( just for two people to chat, not "global" ) and my tables look like this:
MESSAGES:
id | author | receiver | content | sent
where author and receiver are id's from USERS:
id | login | avatar
and i want to get login and avatar both from receiver and author of the message. I was trying with something like this:
SELECT * FROM
((SELECT messages.*,users.login as starter,users.avatar FROM messages
LEFT JOIN users ON messages.author = users.id
WHERE messages.receiver = 1)
UNION
(SELECT messages.*,users.login as test,users.avatar FROM messages
LEFT JOIN users ON messages.receiver = users.id
WHERE messages.author = 1)
ORDER BY id DESC LIMIT 5) tmp
ORDER BY id ASC
and few other queries but i could only get one login ( either receiver or author ). Is there any way to do that?
Try this:
select * from messages m
join users a on (m.author=a.id)
join users r on (m.reciever=r.id)
Of course, you can add a where clause to filter it for a specific message, author or whatever.

How to write inner query that returns latest message for a given user?

I have some tables like this:
USERS TABLE:
| id | created | active | fname | lname |
MESSAGES TABLE:
| id | userId| active | date | content |
I am trying to return some user information, along with the most recently added message for a given user.
Below is the structure of the results that I am rying to achieve:
| userId | userCreated | latestMessageDate| latestMessageContent |
The following query returns the user information:
SELECT
user.id,
user.created
FROM user
WHERE user.active = 1
... But how do I now attach the date of the latest message, along with the actual latest message?
I believe using an inner query is one such approach, but how do you write such a query??
SELECT u.fname, u.lname, m.id, m.userID, m.datem, m.content
FROM USERS AS u
LEFT JOIN ( SELECT id, userID, date, content
FROM MESSAGES
WHERE active
ORDER BY date DESC) AS m
ON u.id = m.userId
WHERE u.active
# AND u.id = {$int_user_id}
GROUP BY u.id
Maybe something like this:
SELECT
Users.id AS userId,
Users.created AS userCreated,
LatestMessage.LatestMessageDate,
MESSAGES.content AS latestMessageContent
FROM
Users
LEFT JOIN
(
SELECT
MAX(date) AS LatestMessageDate,
MESSAGES.userId
FROM
MESSAGES
GROUP BY
MESSAGES.userId
) AS LatestMessage
ON Users.id=LatestMessage.userId
LEFT JOIN MESSAGES
ON LatestMessage.LatestMessageDate=MESSAGES.date
AND LatestMessage.userId=MESSAGES.userId