How can I make a dynamic limit selecting? - mysql

I have a table like this:
// notifications
+----+--------------+------+---------+------------+
| id | event | seen | id_user | time_stamp |
+----+--------------+------+---------+------------+
| 1 | vote | 1 | 123 | 1464174617 |
| 2 | comment | 1 | 456 | 1464174664 |
| 3 | vote | 1 | 123 | 1464174725 |
| 4 | answer | 1 | 123 | 1464174813 |
| 5 | comment | NULL | 456 | 1464174928 |
| 6 | comment | 1 | 123 | 1464175114 |
| 7 | vote | NULL | 456 | 1464175317 |
| 8 | answer | NULL | 123 | 1464175279 |
| 9 | vote | NULL | 123 | 1464176618 |
+----+--------------+------+---------+------------+
I'm trying to create a recent inbox messages box. I want to fetch both read and unread message for each user based on this condition:
Note: Always there should be at least 2 messages in the end of list which are read (seen = 1) .
The result should be containing 15 rows by default. Unless:
there is more that 13 unread message (seen = NULL) (plus two read messages that have been read and always there).
all messages are less than 15 in total.
Here is some examples: (read message: seen = 1, unread message: seen = NULL)
Info: read message: 1, unread message: 1
Output: 2 rows. (containing one read message and one unread message)
Info: read message: 40, unread message: 1
Output: 15 rows. (containing 14 read messages and 1 unread message)
Info: read message: 12, unread message: 20
Output: 22 rows. (containing 2 read messages and 20 unread message)
Info: read message: 0, unread message: 16
Output: 16 rows. (containing 0 read messages and 16 unread message)
Info: read message: 25, unread message: 15
Output: 17 rows. (containing 2 read messages and 15 unread message)
Info: read message: 4, unread message: 8
Output: 12 rows. (containing 4 read messages and 8 unread message)
Info: read message: 745, unread message: 4
Output: 15 rows. (containing 11 read messages and 4 unread message)
As you see, I'm trying to fetch all unread messages plus at least two read messages. Also the number of rows should be 15 by default. How can I do that?
Here is my query:
SELECT id, event seen, time_stamp
FROM notifications
WHERE id_user = :id AND
LIMIT 15 OR IF (seen IS NULL > 15) THEN seen AND
SELECT id, event seen, time_stamp FROM notification
WHERE id_user = :id AND seen IS NOT NULL LIMIT 2
ENDIF

One method comes close:
SELECT id, event, seen, time_stamp
FROM notifications n
WHERE id_user = :id
ORDER BY (seen IS NULL) desc, time_stamp desc
LIMIT 15;
However, this does not get all unseen messages. So, here is sort of hack to do this:
(SELECT id, event, seen, time_stamp
FROM notifications n
WHERE id_user = :id AND seen IS NULL
) UNION
(SELECT id, event, seen, time_stamp
FROM notifications n
WHERE id_user = :id
ORDER BY (seen IS NULL) desc, time_stamp desc
LIMIT 15
)
ORDER BY (seen IS NULL) desc, time_stamp desc;
The first subquery gets all unseen messages. The second gets fifteen messages. The UNION removes duplicates, but no other limit is applied. I believe this does what you want.

There is no possibility to use varying values for LIMIT in MySQL.
The workaround would be to compute the value for the LIMIT and save it into a variable, and then use the prepared statements

Related

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.

Querying conversations from messages table

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.

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;

SQL get last messages from/to certain user

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

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