SQL query for most recent messages - mysql

I am trying to implement a message system quite similar to facebook . The message table is :
+--------+----------+--------+-----+----------+
| msg_id | msg_from | msg_to | msg | msg_time |
+--------+----------+--------+-----+----------+
Here msg_from and msg_to contain user ids and the msg_time contains the timestamp of the message . A user's user id can appear in both the to and from column and multiple times for another user . How should I write a SQL query which selects the most recent sent message between two users ? (The message can come from either one) 1 to 2 or 2 to 1 .

Since John Woo clarified that it is not directional, here's my new answer:
select *
from msgsList
where (least(msg_from, msg_to), greatest(msg_from, msg_to), msg_time)
in
(
select
least(msg_from, msg_to) as x, greatest(msg_from, msg_to) as y,
max(msg_time) as msg_time
from msgsList
group by x, y
);
Output:
| MSG_ID | MSG_FROM | MSG_TO | MSG | MSG_TIME |
------------------------------------------------------------------------
| 1 | 1 | 2 | hello | January, 23 2010 17:00:00-0800 |
| 5 | 1 | 3 | me too | January, 23 2012 00:15:00-0800 |
| 6 | 3 | 2 | hello | January, 23 2012 01:12:12-0800 |
For this input:
create table msgsList
(
msg_id int,
msg_from int,
msg_to int,
msg varchar(10),
msg_time datetime
);
insert into msgslist VALUES
(1, 1, 2, 'hello', '2010-01-23 17:00:00'), -- shown
(2, 2, 1, 'world', '2010-01-23 16:00:00'),
(3, 3, 1, 'i am alive', '2011-01-23 00:00:00'),
(4, 3, 1, 'really', '2011-01-22 23:15:00'),
(5, 1, 3, 'me too', '2012-01-23 00:15:00'), -- shown
(6, 3, 2, 'hello', '2012-01-23 01:12:12'); -- shown
SQLFiddle Demo
If ANSI SQL is your cup of tea, here's the way to do it: http://sqlfiddle.com/#!2/0a575/19
select *
from msgsList z
where exists
(
select null
from msgsList
where
least(z.msg_from, z.msg_to) = least(msg_from, msg_to)
and greatest(z.msg_from, z.msg_to) = greatest(msg_from, msg_to)
group by least(msg_from, msg_to), greatest(msg_from, msg_to)
having max(msg_time) = z.msg_time
) ;

Could it be this simple? http://www.sqlfiddle.com/#!2/50f9f/1
set #User1 := 'John';
set #User2 := 'Paul';
select *
from
(
select *
from messages
where msg_from = #User1 and msg_to = #User2
order by msg_time desc
limit 1
) as x
union
select *
from
(
select *
from messages
where msg_from = #User2 and msg_to = #User1
order by msg_time desc
limit 1
) as x
order by msg_time desc
Output:
| MSG_ID | MSG_FROM | MSG_TO | MSG | MSG_TIME |
----------------------------------------------------------------------------
| 2 | Paul | John | Hey Johnny! | August, 20 2012 00:00:00-0700 |
| 1 | John | Paul | Hey Paulie! | August, 19 2012 00:00:00-0700 |
Could be a lot simpler if only MySQL supported windowing function: http://www.sqlfiddle.com/#!1/e4781/8
with recent_message as
(
select *, rank() over(partition by msg_from, msg_to order by msg_time desc) as r
from messages
)
select *
from recent_message
where r = 1
and
(
(msg_from = 'John' and msg_to = 'Paul')
or
(msg_from = 'Paul' and msg_to = 'John')
)
order by msg_time desc;

For any complex query like this, use TDQD — Test-Driven Query Design. Devise the answer step-by-step, with the size of the steps controlled by your experience and how well you understand the problem.
Step 1 — Find the time of the most recent message between the given users
Throughout this, I assume that the user IDs are integers; I'm using the values 1000 and 2000.
SELECT MAX(msg_time) AS msg_time
FROM message
WHERE ((msg_to = 1000 AND msg_from = 2000) OR
(msg_to = 2000 AND msg_from = 1000)
)
Step 2 — Find the record corresponding to the most recent message
SELECT m.*
FROM message AS m
JOIN (SELECT MAX(msg_time) AS msg_time
FROM message
WHERE ((msg_to = 1000 AND msg_from = 2000) OR
(msg_to = 2000 AND msg_from = 1000)
)
) AS t
ON t.msg_time = m.msg_time
WHERE ((m.msg_to = 1000 AND m.msg_from = 2000) OR
(m.msg_to = 2000 AND m.msg_from = 1000)
)
If there happen to be two (or more) messages between these characters with the same latest timestamp, then they'll all be selected; there is at present no basis for choosing between the collisions. If you think that's a problem, you can arrange to find the MAX(msg_id) using the query above (as a sub-query):
SELECT m2.*
FROM message AS m2
JOIN (SELECT MAX(m.msg_id) AS msg_id
FROM message AS m
JOIN (SELECT MAX(msg_time) AS msg_time
FROM message
WHERE ((msg_to = 1000 AND msg_from = 2000) OR
(msg_to = 2000 AND msg_from = 1000)
)
) AS t
ON t.msg_time = m.msg_time
WHERE ((m.msg_to = 1000 AND m.msg_from = 2000) OR
(m.msg_to = 2000 AND m.msg_from = 1000)
)
) AS i
ON i.msg_id = m2.msg_id
Warning: Code not formally tested with any DBMS.

After giving it some thought, I came up with this:
SELECT min_user AS min(msg_from, msg_to), max_user AS max(msg_from, msg_to),
max(msg_date) FROM msg GROUP BY min_user, max_user
I'm still not quite sure how to get the additional data from the message, but I'll give it some thought.

Related

How to limit a query by column value

Following query...
SELECT event_id, user_id FROM EventUser WHERE user_id IN (1, 2)
...gives me the following result:
+----------+---------+
| event_id | user_id |
+----------+---------+
| 3 | 1 |
| 2 | 1 |
| 1 | 1 |
| 5 | 1 |
| 4 | 1 |
| 6 | 1 |
| 4 | 2 |
| 2 | 2 |
| 1 | 2 |
| 5 | 2 |
+----------+---------+
Now, I want to modify the above query so that I only get for example two rows for each user_id, eg:
+----------+---------+
| event_id | user_id |
+----------+---------+
| 3 | 1 |
| 2 | 1 |
| 4 | 2 |
| 5 | 2 |
+----------+---------+
I am thinking about something like this, which of course does not work:
SELECT event_id, user_id FROM EventUser WHERE user_id IN (1, 2) LIMIT 2 by user_id
Ideally, this should work with offsets as well because I want to use it for paginations.
For performance reasons it is essential to use the WHERE user_id IN (1, 2) part of the query.
One method -- assuming you have at least two rows for each user -- would be:
(select min(event_id) as event_id, user_id
from t
where user in (1, 2)
group by user_id
) union all
(select max(event_id) as event_id, user_id
from t
where user in (1, 2)
group by user_id
);
Admittedly, this is not a "general" solution, but it might be the simplest solution for what you want.
If you want the two biggest or smallest, then an alternative also works:
select t.*
from t
where t.user_id in (1, 2) and
t.event_id >= (select t2.event_id
from t t2
where t2.user_id = t.user_id
order by t2.event_id desc
limit 1, 1
);
Here is a dynamic example for such problems, Please note that this example is working in SQL Server, could not try on mysql for now. Please let me know how it works.
CREATE TABLE mytable
(
number INT,
score INT
)
INSERT INTO mytable VALUES ( 1, 100)
INSERT INTO mytable VALUES ( 2, 100)
INSERT INTO mytable VALUES ( 2, 120)
INSERT INTO mytable VALUES ( 2, 110)
INSERT INTO mytable VALUES ( 3, 120)
INSERT INTO mytable VALUES ( 3, 150)
SELECT *
FROM mytable m
WHERE
(
SELECT COUNT(*)
FROM mytable m2
WHERE m2.number = m.number AND
m2.score >= m.score
) <= 2
How about this?
SELECT event_id, user_id
FROM (
SELECT event_id, user_id, row_number() OVER (PARTITION BY user_id) AS row_num
FROM EventUser WHERE user_id in (1,2)) WHERE row_num <= n;
And n can be whatever
Later but help uses a derived table and the cross join.
For the example in this post the query will be this:
SELECT
#row_number:=CASE
WHEN #user_no = user_id
THEN
#row_number + 1
ELSE
1
END AS num,
#user_no:=user_id userid, event_id
FROM
EventUser,
(SELECT #user_no:=0,#row_number:=0) as t
group by user_id,event_id
having num < 3;
More information in this link.

Number of unread messages sum

I want to retrieve messages and number of unread message (0) for a sender and dest in a conversation.
+---------------------------------------------------------------+
| messages |
+---------------------------------------------------------------+
| message_id | id_sender | id_dest | subject | message | read |
+---------------------------------------------------------------+
| 1 | 25 | 50 | Hi | message | 0 |
| 2 | 25 | 50 | Hi2 |message2 | 1 |
| 3 | 25 | 50 | Hi3 |message3 | 0 |
+---------------------------------------------------------------+
In this case the result must be 2. I try with
SELECT *
FROM
(SELECT message,sum(read = 0) as nm_messages
FROM messages
WHERE ( id_sender = id1 AND id_dest = id2 ) or
( id_dest = id1 AND id_sender = id2 )
ORDER BY message_id DESC
LIMIT 10) AS ttbl
ORDER BY message_id ASC
The messages part is ok but when
I add
sum(read = 0) as nm_messages
return only the firsth message if possible for both mysql postgresql
Thanks!
I have used PostgreSQL 9.4.11, compiled by Visual C++ build 1800, 64-bit.
With distinct on you can eliminate same rows with their unique ids. in this case i have used id_sender.
SELECT DISTINCT ON ( expression [, ...] ) keeps only the first row of each set of rows where the given expressions evaluate to equal
more information look at this link:
distinct on
Below sql query will return only the first message and the number of unread messages (0):
SELECT distinct on (id_Sender)
message,
count(case when read=0 then 1 end) over() as nm_messages
FROM messages
group by id_Sender,message,message_id
order by id_Sender,message_id
message | nm_message
message | 2
You should use the sum with if condition. Should be like this:
SELECT *
FROM
(SELECT GROUP_CONCAT(message),sum(IF(read = 0,1,0)) as nm_messages
FROM messages
WHERE ( id_sender = id1 AND id_dest = id2 ) or
( id_dest = id1 AND id_sender = id2 )
GROUP BY id_sender, id_dest
LIMIT 10) AS ttbl
ORDER BY message_id ASC
When the condition is true (read = 1), then it will sum up 1, otherwise 0.
I just saw that there was no grouping in the query. I added that. Also if you use an aggregate function, it doesnt make sense to do that only for one field (read), and not for others (message). So i put group_concact around message. That makes more sense to me?!

sql - relation is unknown inside a subquery

I have a relation that is built from 2 integers photo_id , user_id and a string -info, (this is the tag) ,
primary key is (user_id, photo_id, info)
photo_id | user_id | info
---------------------------
5 | 3 | aa
7 | 6 | aa
2 | 2 | bb
1 | 2 | cc
1 | 9 | aa
2 | 8 | cc
1 | 4 | cc
9 | 9 | cc
I'm trying to find the k most common tags in my relation.
(secondary sort is by tags).
in this example i would like to get:
k=2 : aa , cc
k=1 : cc
By using this sql query :
SELECT info,tagCount
FROM (SELECT info, COUNT(photo_id) as tagCount
FROM Tags
GROUP BY info
ORDER BY tagCount DESC, info ASC) T
WHERE (SELECT count(info) FROM T T1
WHERE ((T1.tagCount > T.tagCount) OR
(T1.tagCount = T.tagCount AND T1.info < T.info))) < 'k';
But I get the error:
SQL error:
ERROR: relation "t" does not exist
Where is my mistake?
While I still remain unclear on what you are trying to achieve, and assuming the query is for MySQL (not "sql server") then the following may also help. Please note that the cause of the error message is that alias T refers to a resultset, but you cannot reuse that entire resultset in the where clause (the subquery T1 assume that you can reuse T). Regrettablly MySQL (at the time of writing) does not support common table expressions which would allow referencing T like this:
/* T as a common table expression (CTE) */
with T as (
SELECT info, COUNT(photo_id) as tagCount
FROM Tags
GROUP BY info
)
SELECT info,tagCount
, (SELECT count(info) FROM T T1
WHERE (T1.tagCount > T.tagCount) OR
(T1.tagCount = T.tagCount AND T1.info < T.info)
) as k
FROM T
ORDER BY tagCount DESC, info ASC
;
So, in the absence of a CTE capability, you have to repeat the initial subquery, like this:
SELECT
info
, tagCount
, (
SELECT
COUNT(info)
FROM (
SELECT
info
, COUNT(photo_id) AS tagCount
FROM Tags
GROUP BY
info
) T1
WHERE (T1.tagCount > T.tagCount)
OR (T1.tagCount = T.tagCount
AND T1.info < T.info)
)
AS k
FROM (
SELECT
info
, COUNT(photo_id) AS tagCount
FROM Tags
GROUP BY
info
) T
ORDER BY
tagCount DESC
, info ASC
;
and the result of that query (from the sample data) is as follows:
| info | tagCount | k |
|------|----------|---|
| cc | 4 | 0 |
| aa | 3 | 1 |
| bb | 1 | 2 |
Now, exactly how you derive the "expected result" shown in the question (where tag "bb" is not included) I remain unclear.
By the way. Another issue in your original query is that the where clause predicate is comparing an integer to 'k'
where (select count(info) ....) < 'k'
count(info) is an integer, 'k' is a string, so it will fail.
This may only be a step toward your solution as I don't completely understand the question. I think you need to count(distinct column) then use a much simpler where clause.
SQL Fiddle
MySQL 5.6 Schema Setup:
CREATE TABLE Tags
(`photo_id` int, `user_id` int, `info` varchar(2))
;
INSERT INTO Tags
(`photo_id`, `user_id`, `info`)
VALUES
(5, 3, 'aa'),
(7, 6, 'aa'),
(2, 2, 'bb'),
(1, 2, 'cc'),
(1, 9, 'aa'),
(2, 8, 'cc'),
(1, 4, 'cc'),
(9, 9, 'cc')
;
Query 1:
SELECT
info
, COUNT(distinct photo_id) AS photoCount
, COUNT(distinct user_id) AS userCount
FROM Tags
GROUP BY
info
ORDER BY
photoCount DESC
, userCount DESC
, info ASC
Results:
| info | photoCount | userCount |
|------|------------|-----------|
| cc | 3 | 4 |
| aa | 3 | 3 |
| bb | 1 | 1 |

Selecting latest row of record with distinct parameter

I want to select a list of non-duplicate records, that invlove a certain user (either in user_to or user_from). I want to retrieve the other user and also the latest content in that record. The list must not have duplications in the other user selected.
For example, I have the following set of records
id user_to user_from content time
1 1 2 ABC 2013-11-05
2 4 2 BBC 2013-11-06
3 3 1 CBC 2013-11-07
4 5 1 ABC 2013-11-08
5 1 2 AAC 2013-11-09
6 5 1 ABB 2013-11-10
7 3 4 CBC 2013-11-11
8 1 2 ACC 2013-11-12
In this case, If the parameter to provide is 1, I want to select record 3,6,8 , the others are not selected because either they are duplicated and older or they do not involve 1.
I have looked into this post and tried something like this:
SELECT u, content, date FROM(
(SELECT
user_from AS u,
MAX(time) AS date,
content
FROM t1
WHERE user_to = :user
)
UNION
(SELECT
user_to AS u,
MAX(time) AS date,
content
FROM t1
WHERE user_from = :user
)
) t2
WHERE date IN (SELECT MAX(date) FROM t2 GROUP BY u)
But no, can't get it done.
Any idea how to write the query? Thanks!
Your query should be this:
select m.* from
message m,
( select user_to,
user_from,
max(dtime) mxdate
from message
where user_from = 1 or user_to = 1
group by user_to, user_from) m2
where m.dtime = m2.mxdate
and (m.user_from = 1 or m.user_to = 1)
See it here at fiddle: http://sqlfiddle.com/#!2/13d4e/4
As you ask in comments: ok. but as I only want the user_id of the other user, is there a way to select only user_to when user_from=1 and user_from when user_to=1 ?
select if(m.user_to=1,m.user_from,m.user_to) as user,
m.content,
m.dtime
from
message m,
( select user_to,
user_from,
max(dtime) mxdate
from message
where user_from = 1 or user_to = 1
group by user_to, user_from) m2
where m.dtime = m2.mxdate
and (m.user_from = 1 or m.user_to = 1)
see it here: http://sqlfiddle.com/#!2/13d4e/5
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
,user_to INT NOT NULL
,user_from INT NOT NULL
,content CHAR(3) NOT NULL
,dt DATE NOT NULL
);
INSERT INTO my_table VALUES
(1,1,2,'ABC','2013-11-05'),
(2,4,2,'BBC','2013-11-06'),
(3,3,1,'CBC','2013-11-07'),
(4,5,1,'ABC','2013-11-08'),
(5,1,2,'AAC','2013-11-09'),
(6,5,1,'ABB','2013-11-10'),
(7,3,4,'CBC','2013-11-11'),
(8,1,2,'ACC','2013-11-12');
SELECT x.*
FROM my_table x
JOIN
( SELECT LEAST(user_to,user_from) l
, GREATEST(user_to,user_from) g
, MAX(dt) max_dt FROM my_table
GROUP
BY LEAST(user_to,user_from)
, GREATEST(user_to,user_from)
) y
ON y.l = LEAST(x.user_to,x.user_from)
AND y.g = GREATEST(x.user_to,x.user_from)
AND y.max_dt = x.dt
WHERE 1 IN (x.user_to,x.user_from);
+----+---------+-----------+---------+------------+
| id | user_to | user_from | content | dt |
+----+---------+-----------+---------+------------+
| 3 | 3 | 1 | CBC | 2013-11-07 |
| 6 | 5 | 1 | ABB | 2013-11-10 |
| 8 | 1 | 2 | ACC | 2013-11-12 |
+----+---------+-----------+---------+------------+
if filtering is "1" add this to the query of #Jorge Campos
where user_from = 1 OR user_to=1

Get column values based on last entry (not null)

I have a table in MySQL which holds conversations. These conversation are composed of messages. A single conversation looks like the table below.
Importance and eId's are only sometimes set. What I am trying to get from the table is the last message in the conversation (messageId = 4) but with the last set importance and last set eId.
So, from this table
+----------------+-----------+----------+----------+------------+-------+---------+
| conversationId | messageId | time | status | importance | eId | message |
+----------------+-----------+----------+----------+------------+-------+---------+
| 25 | 4 | 11:00:00 | feedback | NULL | NULL | d.. |
+----------------+-----------+----------+----------+------------+-------+---------+
| 25 | 3 | 10:00:00 | open | MEDIUM | NULL | c.. |
+----------------+-----------+----------+----------+------------+-------+---------+
| 25 | 2 | 09:00:00 | feedback | NULL | 123 | b... |
+----------------+-----------+----------+----------+------------+-------+---------+
| 25 | 1 | 08:00:00 | open | HIGH | NULL | a... |
+----------------+-----------+----------+----------+------------+-------+---------+
I need to get this result
+----------------+-----------+----------+----------+------------+-------+---------+
| conversationId | messageId | time | status | importance | eId | message |
+----------------+-----------+----------+----------+------------+-------+---------+
| 25 | 4 | 11:00:00 | feedback | MEDIUM | 123 | d.. |
+----------------+-----------+----------+----------+------------+-------+---------+
I can't get the query to work.
Any help would be appriciated. Thanks.
If there is more than one conversationId in the table, and you want to get the desired result for all of the conversationIds at the same time, then I think you need to join 3 subqueries within subqueries. Something like the 3 below:
SELECT messages.conversationId, messages.messageId, messages.time, messages.status, messages.message
FROM messages
JOIN (
SELECT conversationId, MAX(messageId) as messageId
FROM messages
GROUP BY conversationId) as m2
ON (messages.messageId = m2.messageId);
SELECT messages.conversationId, messages.importance
FROM messages
JOIN (
SELECT conversationId, MAX(messageId) as messageId
FROM messages
WHERE importance IS NOT NULL
GROUP BY conversationId) as m3
ON (messages.messageId = m3.messageId);
SELECT messages.conversationId, messages.eId
FROM messages
JOIN (
SELECT conversationId, MAX(messageId) as messageId
FROM messages
WHERE eId IS NOT NULL
GROUP BY conversationId) as m4
ON (messages.messageId = m4.messageId);
The JOIN would look like this:
SELECT
main.conversationId,
main.messageId,
main.time,
main.status,
importance.importance,
eId.eId,
main.message
FROM (
SELECT messages.conversationId, messages.messageId, messages.time, messages.status, messages.message
FROM messages
JOIN (
SELECT conversationId, MAX(messageId) AS messageId
FROM messages
GROUP BY conversationId) AS m2
ON (messages.messageId = m2.messageId)
) AS main
JOIN (
SELECT messages.conversationId, messages.importance
FROM messages
JOIN (
SELECT conversationId, MAX(messageId) AS messageId
FROM messages
WHERE importance IS NOT NULL
GROUP BY conversationId) AS m3
ON (messages.messageId = m3.messageId)
) AS importance
JOIN (
SELECT messages.conversationId, messages.eId
FROM messages
JOIN (
SELECT conversationId, MAX(messageId) AS messageId
FROM messages
WHERE eId IS NOT NULL
GROUP BY conversationId) AS m4
ON (messages.messageId = m4.messageId)
) AS eId
ON (
main.conversationId = importance.conversationId
AND main.conversationId = eId.conversationId
);
Here's an sqlfiddle: http://sqlfiddle.com/#!2/857aa/38. This assumes that messageId is unique. If it is not unique, then you need to join on messageId AND conversationId.
Maybe a bit oldschool but should do the trick :
SELECT c.conversationId as convId, max(c.messageId), t.time, s.status, i.importance, e.eId, c.message
FROM convs c,
(SELECT conversationId, max(time) AS time FROM convs) t,
(SELECT conversationId, status FROM convs WHERE status IS NOT NULL ORDER BY messageId DESC) s,
(SELECT conversationId, importance FROM convs WHERE importance IS NOT NULL ORDER BY messageId DESC) i,
(SELECT conversationId, eId FROM convs WHERE eId IS NOT NULL ORDER BY messageId DESC) e
WHERE 1=1
AND t.conversationId = c.conversationId
AND s.conversationId = c.conversationId
AND i.conversationId = c.conversationId
AND e.conversationId = c.conversationId
GROUP BY c.conversationId
SQL Fiddle demo
This one is not necessarily the best solution as I had to create this in my local SQL Server (SQL fiddle was down when tried and I have no MySQL installed), also a "quick and dirty" query (means: there might be a better solution for your problem), but as I am keen to help I post it anyway.
If you happen to wait for another, possibly better solution from the community, I will not feel offended :) That is what I wanted to say :)
SELECT TOP 1 conversationId,messageId,RecTime,ConvStatus,
(
SELECT TOP 1 importance
FROM Conversations
WHERE importance IS NOT NULL
ORDER BY messageId DESC
) AS importance,
(
SELECT TOP 1 eId
FROM Conversations
WHERE eId IS NOT NULL
ORDER BY messageId DESC
) AS eId,
UsrMessage
FROM Conversations
ORDER BY messageId DESC
GO
(do not forget that you might have to change "formatting items" to make it work in MySQL)