Last message in correspondence - mysql

I have the following table of messages (sid = sending correspondent ID, rid = receiving correspondent ID, mdate = message date, mtext = message text) representing a correspondence among parties:
sid|rid| mdate | mtext
---+---+------------+----------
1 | 2 | 01-08-2014 | message1 <-- 1st m. in corresp. between id=1 and id=2
2 | 1 | 02-08-2014 | message2 <-- 2nd m. in corresp. between id=1 and id=2
1 | 2 | 04-08-2014 | message3 <-- last m. in corrensp. between id=1 and id=2
2 | 3 | 02-08-2014 | message4 <-- not id=1 correspondence at all
1 | 3 | 03-08-2014 | message5 <-- 1st m. in corrensp. between id=1 and id=3
3 | 1 | 04-08-2014 | message6 <-- 2nd m. in corrensp. between id=1 and id=3
3 | 1 | 05-08-2014 | message7 <-- last m. in corrensp. between id=1 and id=3
5 | 1 | 03-08-2014 | message8 <-- last m. in corrensp. between id=1 and id=5
requested MySQL query should return for one correspondent (being sender or receiver) only correspondence with last message (sent or received) with other parties. So from previous table of messages this query for correspondent with id=1 should return last correspondence messages (last sent or received):
sid|rid| mdate | mtext
---+---+------------+----------
1 | 2 | 04-08-2014 | message3
3 | 1 | 05-08-2014 | message7
5 | 1 | 03-08-2014 | message8
How to make such a query for MySQL?

group by sid if rid=1 or rid if sid=1 to find max date, then join:
select a.*
from messages a
join (
select if(sid=1, rid, sid) id, max(mdate) d
from messages
where sid = 1 or rid = 1
group by id) b on ((a.sid=1 and a.rid=b.id) or (a.sid=b.id and a.rid=1)) and a.mdate = b.d;
demo

Assuming that there are no messages with the exact same timestamp between two correspondents, you can use a filtering join:
select *
from messages m
join (
select case when sid > rid then sid else rid end r1
, case when sid <= rid then sid else rid end r2
, max(mdate) as max_mdate
from messages
where 1 in (sid, rid)
group by
r1
, r2
) as filter
on m.sid in (filter.r1, filter.r2)
and m.rid in (filter.r1, filter.r2)
and m.mdate = filter.max_mdate
Example on SQL Fiddle.

Does the following join work for you:
SELECT m.*
FROM messages m
INNER JOIN
(SELECT
MAX(
CASE
WHEN sid = 1 THEN mdate
ELSE NULL
END
) max_sdate,
MAX(
CASE
WHEN rid = 1 THEN mdate
ELSE NULL
END
) max_rdate
FROM messages
WHERE sid = 1 or rid = 1
) max_dates
ON (m.sid = 1 AND mdate = max_dates.max_sdate) OR (m.rid = 1 AND mdate = max_dates.max_rdate);
SQL Fiddle demo

Related

Limit query results based on value of multiple columns

I am using MySQL 5.6 and I have a table structure like below
| user_id | email_1 | email_2 | email_3 |
| 1 | abc#test.com | | |
| 2 | xyz#test.com | | joe#test.com |
| 3 | | test#test.com | bob#joh.com |
| 4 | | | x#y.com |
I want to fetch the first n email addresses from this table.
For example, if I want to fetch the first 5 then only the first 3 rows should return.
This makes certain assumptions about the uniqueness of data, that might not be true...
SELECT DISTINCT x.* FROM my_table x
JOIN
(SELECT user_id, 1 email_id,email_1 email FROM my_table WHERE email_1 IS NOT NULL
UNION ALL
SELECT user_id, 2 email_id,email_2 email FROM my_table WHERE email_2 IS NOT NULL
UNION ALL
SELECT user_id, 3 email_id,email_3 email FROM my_table WHERE email_3 IS NOT NULL
ORDER BY user_id, email_id LIMIT 5
) y
ON y.user_id = x.user_id
AND CASE WHEN y.email_id = 1 THEN y.email = x.email_1
WHEN y.email_id = 2 THEN y.email = x.email_2
WHEN y.email_id = 3 THEN y.email = x.email_3
END;
You want to return as many rows as necessary to get five emails. So you need a running total of the email count.
select user_id, email_1, email_2, email_3
from
(
select
user_id, email_1, email_2, email_3,
coalesce(
sum((email_1 is not null) + (email_2 is not null) + (email_3 is not null))
over (order by user_id rows between unbounded preceding and 1 preceding)
, 0) as cnt_prev
from mytable
) counted
where cnt_prev < 5 -- take the row if the previous row has not reached the count of 5
order by user_id;
You need a current MySQL version for SUM OVER to work.
The counting of the emails uses a MySQL feature: true equals 1 and false equals 0 in MySQL. Thus (email_1 is not null) + (email_2 is not null) + (email_3 is not null) counts the emails in the row.
Demo: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=ac415e71733699547196ae01cb1caf13

Select last message from all conversations (MySQL)

I need to select all the last messages for each conversation for user with given id.
In case of last message was send to given id, it have to be last message from sender.
Here is the test case without creationDate using messageID:
+-----------+------------+----------+------+
| messageID | fromUserID | toUserID | text |
+-----------+------------+----------+------+
| 1 | 1 | 2 | 'aa' |
| 2 | 1 | 3 | 'ab' |
| 3 | 2 | 1 | 'ac' |
| 4 | 2 | 1 | 'ad' |
| 5 | 3 | 2 | 'ae' |
+-----------+------------+----------+------+
The result for userID=1 have to be messages with text 'ab' and 'ad'.
For now I have this query with all of the last messages of every user to each other, but does not remove, according to my test case, message with id=1 (have to be only with id=2 and id=4).
SELECT
UM.messageID,
UM.fromUserID, UM.toUserID,
UM.text, UM.flags, UM.creationDate
FROM UserMessage AS UM
INNER JOIN
(
SELECT
MAX(messageID) AS maxMessageID
FROM UserMessage
GROUP BY fromUserID, toUserID
) IUM
ON UM.messageID = IUM.maxMessageID
WHERE UM.fromUserID = 1 OR UM.toUserID = 1
ORDER BY UM.messageID DESC
A simple method is
select um.*
from usermessage um
where um.messageid = (select min(um2.messageid)
from usermessage um2
where (um2.fromuserid, touserid) in ( (um.fromuserid, um.touserid), (um.touserid, um.fromuserid) )
);
Or, in MySQL 8+:
select um.*
from (select um.*,
row_number() over (partition by least(um.fromuserid, um.touserid), greatest(um.fromuserid, um.touserid) order by um.messageid desc) as seqnum
from usermessage um
) um
where seqnum = 1;

MySQL GROUP BY while keeping certain rows by column content

There is table with duplicated rows. See rows 1 and 2:
id full_name email status active
1 John Doe john#mail.com ok 1
2 John Doe john#mail.com null 1
3 Ricky Duke rick#mail.com null 1
4 Jane Doe jane#mail.com block 1
I need to select distinct rows, not randomly - one distinct row, but the one that has a 'status' NOT NULL.
My query is:
SELECT full_name, email
FROM `subscribers`
WHERE active = 1 AND (status = 'ok' OR status IS NULL)
GROUP BY email
That query selects distinct rows randomly, without prioritizing 'status' field.
How can i prioritize selection of distinct rows, that has a 'status' NOT NULL, and select ones with NULL only in case there is no rows with 'ok' status is present?
You can use row_number():
select s.*
from (select s.*,
row_number() over (partition by email order by (status is not null) desc) as seqnum
from subscribers s
where active = 1
) s
where seqnum = 1;
You could filter with a correlated subquery that does conditional ordering, and gives a lowest priority to null statuses:
select t.*
from mytable t
where t.id = (
select id
from mytable t1
where
t1.full_name = t.full_name
and t1.email = t.email
and t1.active = t.active
order by status is null, status
limit 1
)
This defines duplicats as records that have the same full_name, email and active. You might want to adapt that to your actual definition of duplicates.
Demo on DB Fiddle:
id | full_name | email | status | active
-: | :--------- | :------------ | :----- | :-----
1 | John Doe | john#mail.com | ok | 1
3 | Ricky Duke | rick#mail.com | null | 1
4 | Jane Doe | jane#mail.com | block | 1
(SELECT full_name, email
FROM `subscribers`
WHERE active = 1 AND status IS NOT NULL
GROUP BY email)
UNION ALL
(SELECT full_name, email
FROM `subscribers`
WHERE active = 1 AND status IS NULL AND
email not in (SELECT distinct email
FROM `subscribers`
WHERE active = 1 AND status IS NOT NULL)
GROUP BY email);

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

MySQL Join and Subqueries

I currently have the following tables:
Case_Workflows
case_id (PK) | work_id (PK) | date
1 | 1 | 2011-12-12
1 | 4 | 2011-12-13
2 | 6 | 2011-12-18
Workflows
work_id (PK) | status_id
1 | 1
2 | 1
3 | 1
4 | 2
5 | 2
6 | 3
Statuses
status_id (PK) | title
1 | abc
2 | def
3 | ghi
What I am attempting to do is pull a count of the total number of cases with a specific status such as 'abc'. The snag is that each case can have multiple workflows and I only want the single most recent one for each case.
The end result should be:
Status: abc - Count: 2
This is what I have so far:
SELECT COUNT(cases.case_id) as countNum
FROM $this->_caseTable
JOIN case_workflows
ON cases.case_id = cases_workflows.case_id
JOIN workflows
ON cases_workflows.workflow_id = workflows.workflow_id
JOIN statuses
ON workflow.status_id = statuses.status_id
WHERE cases.date > '2011-12-12'
AND cases.date <= '2011-12-18'
What I am unsure on is how to first select the latest work_id for each case, and then grabbing its status_id to match it to a WHERE clause such as WHERE statuses.title = 'abc'
SELECT COUNT(*) as countNum
FROM $this->_caseTable
JOIN workflows
ON workflows.workflow_id =
( SELECT workflow_id
FROM cases_workflows AS mcwf
WHERE mcwf.case_id = cases.case_id
ORDER BY date DESC
LIMIT 1
)
JOIN statuses
ON workflow.status_id = statuses.status_id
WHERE cases.date > '2011-12-12'
AND cases.date <= '2011-12-18'
AND statuses.title = 'abc'
From what I'm understanding here, you need to add statuses.title to your SELECT clause, and then add a GROUP BY clause:
SELECT statuses.title, COUNT(cases.case_id) as countNum
FROM $this->_caseTable
JOIN (SELECT case_id, work_id, max(date)
FROM case_workflows
GROUP BY work_id
WHERE case_id = cases.case_id) cw
ON cases.case_id = cw.case_id
JOIN workflows
ON cw.workflow_id = workflows.workflow_id
JOIN statuses
ON workflow.status_id = statuses.status_id
GROUP BY statuses.title
WHERE cases.date > '2011-12-12'
AND cases.date <= '2011-12-18'