Getting the latest message between all conversations with other users - php

I'm trying to pull out of the db a list of each last message the current user had sent to/received from another, like the facebook current messaging box (contains a list of "conversations")
Example data:
msg_id sender_id target_id date_sent content ...
1 2 4 20 bla ...
2 2 5 21 bla ...
3 2 6 22 bla ...
4 4 2 25 bla ...
5 5 6 26 bla ...
6 4 2 50 bla ...
If the current user is 2, then I want to get only the last message 2 had with anyone else (sent or received)
Wished data would be:
msg_id sender_id target_id date_sent content ...
6 4 2 50 bla ...
2 2 5 21 bla ...
3 2 6 22 bla ...
msg_id 6 is there because in all the messages 2 and 4 had (regardless of who is the sender/receiver) it has the greatest date (50)
msg_id 2 and 3 are there because that's the latest msg 2 had in conversation with users 5 and 6 (one msg to each, sent)
Couldn't find the way to pull this off, should involve a group_by on some uniquely generated field containing both sender and receiver IDs? I don't know, help please
UPDATE:
I liked the ideas, eventually I created a view of that table with another new field, that contains a concatenation of the two Id's, in order (bigger first), seperated by an underscore. This way that field is unique to each conversation. Then group by it :)

Here's an untested example. There are other ways to do it, this is a pretty common question if you do a search.
SELECT t1.*
FROM
msg_table AS t1
LEFT JOIN msg_table AS t2
ON ((t1.sender_id = t2.sender_id AND t1.target_id = t2.target_id)
OR (t1.sender_id = t2.target_id AND t1.target_id = t2.sender_id))
AND t1.msg_id < t2.msg_id
WHERE (t1.sender_id = ? OR t1.target_id = ?) AND t2.msg_id IS NULL

You could probably do something where you combine sender_id and target_id into a computed value and then do a "LIKE" query on that.
But, to be honest, I suspect your performance will be better just running two simpler queries and then merging the results in your code. Your code will be easier to maintain, as well.

That could be a bit messy.
Had a quick play and something like this will do the job. However this will get the latest message sent to a sender AND the latest sent by a sender (rather than the latest one of those which it seems you want), along with the latest message to each recipient from that sender.
Not tested so please excuse any typos.
SELECT a.msg_id, a.sender_id, a.target_id, a.date_sent, a.content
FROM someTable a
INNER JOIN (SELECT sender_id As SomeBod, MAX(date_sent) AS date_sent
FROM someTable
WHERE sender_id = 2
GROUP BY sender_id) sub1
ON a.sender_id = sub1.SomeBod
AND a.date_sent = sub1.date_sent
UNION
SELECT b.msg_id, b.sender_id, b.target_id, b.date_sent, b.content
FROM someTable b
INNER JOIN (SELECT target_id As SomeBod, MAX(date_sent) AS date_sent
FROM someTable
WHERE target_id = 2
GROUP BY target_id) sub2
ON b.target_id = sub2.SomeBod
AND b.date_sent = sub2.date_sent
UNION
SELECT c.msg_id, c.sender_id, c.target_id, c.date_sent, c.content
FROM someTable c
INNER JOIN (SELECT sender_id, target_id, MAX(date_sent) AS date_sent
WHERE sender_id = 2
GROUP BY sender_id, target_id) sub3
ON a.sender_id = sub3.sender_id
AND b.target_id = sub3.target_id
AND b.date_sent = sub3.date_sent

I did something similar to what you're shooting for here. But to make it easier, i added another column to the table called convoID. This is not a unique id, but an id that is reused every time the user sends or receives a message from a user they have allready communicated with. then get all the convoIDs from the table(but not duplicates)
SELECT DISTINCT convoID FROM table WHERE sender='$user' OR recipient='$user'
This way every message between the same 2 people is easy to access as a conversation.From there you can get the newest message from a conversation like this:
SELECT message FROM table WHERE convoID='$thisConvoID' ORDER BY time DESC LIMIT 0,1;
For me this was the least confusing way to do it, and worked great- if you need clarification on anything just ask.

Related

Group by only showing the first record

I'm trying to grab some conversation records in my database however only show the latest result. I've tried the below however it groups by the first record found is there a way I can make it group by the last record on the dateline colum
SELECT DISTINCT*
FROM table_messages
WHERE fromid=4 OR toid=4
GROUP BY convoid
ORDER BY dateline desc
My table is like the below
pmid - fromid - toid - convoid - dateline
1 4 15 3 1461079193
2 4 15 3 1461079200
3 15 4 3 1461079220
4 15 4 3 1461079230
5 4 15 3 1461079270
To get a list of all the conversations for a specific user, and the last message sent (and it's details) for each conversation, in a single query:
SELECT
convo.convoid,
message.pmid,
message.dateline,
CASE WHEN fromid = 4 then "SENT" ELSE "RECEIVED" as direction
FROM
(SELECT convoid, max(dateline) as maxdateline FROM table_messages GROUP BY convoid) convo
INNER JOIN table_messages message
ON convo.convoid = message.convoid AND
convo.maxdateline = message.dateline
WHERE
fromid = 4 or toid = 4
Here we are working with two sets again. The first (from the FROM) is a list of all convoid's and their max dateline. Then we INNER JOIN those with the same table on the relationship of convoid and dateline. This will give us a record set of every conversation and it's most recent message.
Then we restrict in the WHERE clause to get just the conversations where either the fromid or the toid is the user you are interested in.
For the fun, I added the CASE statement, just to bring in more information about that last message sent (whether it was sent by the user or received by the user).
It MIGHT be quicker to do:
SELECT
convo.convoid,
message.pmid,
message.dateline,
CASE WHEN fromid = 4 then "SENT" ELSE "RECEIVED" as direction
FROM
(SELECT convoid, max(dateline) as maxdateline FROM table_messages GROUP BY convoid WHERE fromid = 4 or toid = 4) convo
INNER JOIN table_messages message
ON convo.convoid = message.convoid AND
convo.maxdateline = message.dateline
Depending on how MySQL optimizes the first query. If it tries to get every conversation and then the last message, then restricts it for the user then you can force the optimizer to instead, with this version, only get conversations for the user, then get the latest message for those conversations. My bet though is that it's a wash and that MySQL will properly optimize the first query to do what is explicitly stated in the second. But... I figured just in case, I would put it here.
For readability sake, I like the first query better since one can very quickly see the conditions in the main query's WHERE clause. Otherwise you have to hunt in the subquery.

Get 5 random rows from MySQL DB [duplicate]

This question already has answers here:
MySQL select 10 random rows from 600K rows fast
(28 answers)
Closed 8 years ago.
I have searched all over for an answer and although people say not to use the ORDER BY RAND() clause, I think for my purposes it is ok as this is for a competition which barely has more than a few hundred records at a time PER competition.
So basically i need to retrieve 5 random records from a competition entries table. However any loyalty customers will received an additional EXTRA entry so example:
compEntryid | firstName | lastName | compID |
1 | bob | smith | 100
2 | bob | smith | 100
3 | jane | doe | 100
4 | sam | citizen | 100
etc
So we are giving the loyalty members a better chance at winning a prize. However im a little worried that the returned result from a usual ORDER BY RAND() can include 2 entries of the SAME person ? What is an optimised method to ensure that we truly have 5 random records but at the same time giving those extra entrants a better or (weighted) chance ? Happy to use multiple queries, sub-queries or even a mix of MySQL and PHP ? Any advice is deeply appreciated thank you !
Bass
EDIT:
These 2 queries both work!
query1
SELECT concat(firstName, " ", lastName) name,id, email
FROM t WHERE
RAND()<(SELECT ((5/COUNT(id))*10) FROM t)
group by email ORDER BY RAND() limit 5;
query2
select distinct
email, id, firstName, lastName from
(
select id ,
email, firstName , lastName , compID, rand()/(select count(*) from t where
email=t1.email
) as rank
from t t1
where compID = 100
order by rank) t2 limit 5;
http://sqlfiddle.com/#!2/73470c/2
If you have a few hundred record, I think that order by rand() solution should be fine:
subquery will order weighting number of entries, but duplicates remains. Parent SELECT will take the first 5 distinct rows.
SELECT DISTINCT firstName ,
lastName ,
compID
FROM
( SELECT compEntryid ,firstName , lastName , compID, rand()/(select count(*)
FROM t
WHERE firstName=t1.firstName AND
lastName = t1.lastName) AS rank
FROM t t1
WHERE compID = 100
ORDER BY rank) t2
LIMIT 5
Fiddle
I think you will need to use a sub query if you want to return a compEntryid.
SELECT t.firstName, t.lastName, t.compID, MIN(compEntryid)
FROM t
INNER JOIN
(
SELECT DISTINCT firstName, lastName, compID
FROM t
ORDER by rand()
LIMIT 5
) t2
ON t.firstName = t2.firstName
AND t.lastName = t2.lastName
AND t.compID = t2.compID
GROUP BY t.firstName, t.lastName, t.compID;
This uses a sub query to get 5 random firstName / lastName / compID. Then joins against the table to get the MIN compEntryId.
However not certain about this. Think it will eliminate the duplicates in the sub query before performing the order / limit, which would prevent someone with more entries having more chances.
EDIT
More of a play and I think I have found a solution. Although efficiency is not one of its strong points.
SELECT MIN(compEntryid), firstName, lastName, compID
FROM
(
SELECT firstName, lastName, compID, compEntryid, #seq:=#seq+1 AS seq
FROM
(
SELECT firstName, lastName, compID, compEntryid
FROM t
ORDER by rand()
) sub0
CROSS JOIN (SELECT #seq:=0) sub1
) sub2
GROUP BY sub2.firstName, sub2.lastName, sub2.compID
ORDER BY MIN(seq)
LIMIT 5
This has an inner sub query that gets all the records in a random order. Around that another sub query adds a sequence number to the records. The outer query groups by the name, etc, and orders by the min sequence number for that name. The compEntryId is just grabbed as the MIN for the name / competition (I am assuming you don't care too much about this).
This way if someone had 5 entries the inner sub query would mix them up in the list. the next sub query would add a sequence number. At this stage those 5 entries could be sequence numbers 1 to 5. The outer one would order by the lowest sequence number for the name and ignore the others, so of those 5 only sequence number 1 would be used and 2 to 5 ignored, with the next selected person being the one with sequence number 6.
This way the more entries they have the more likely they are to be a winner, but can't be 2 of the 5 winners.
With thanks to kiks73 for setting up some sqlfiddle data:-
http://sqlfiddle.com/#!2/cd777/1
EDIT
A solution based on that above by #kiks73. Tweaked to use a non correlated sub query for the counts, and eliminates a few uncertainties. For example with his solution I am not quite sure whether MySQL will chose to do the DISTINCT by implicitly doing a GROUP BY, which would also implicitly do an orderering of the results prior to doing the limit (it doesn't seem to, but I am not sure this behaviour is defined).
SELECT t.firstName ,
t.lastName ,
t.compID,
MIN(rand() / t1.entry_count) AS rank
FROM
(
SELECT firstName, lastName, compID, COUNT(*) AS entry_count
FROM t
GROUP BY firstName, lastName, compID
) t1
INNER JOIN t
ON t.firstName=t1.firstName
AND t.lastName = t1.lastName
AND t.compID = t1.compID
GROUP BY t.firstName, t.lastName, t.compID
ORDER BY rank
LIMIT 5

Selecting N rows from each group in MYSQL

I need to search for the updates sent by the friends of a giving user.
There is a table called friendship. It has a column called profile1 and another one called profile2. It represents the friendship between two users in this websystem, and a friendship is the presence of two giving ids, no matter in what position. So the profile with id 1 may have 2 friends, profile with id 2 and with id 3 as following:
friendship
profile1 profile2
1 2 <--
3 1 <--
2 5
...
Now I want to search for the updates sent by some user's friends. There is this table update
update
id content time profile
1 A text ... 2
2 A text ... 2
3 A text ... 3
4 A text ... 2
5 A text ... 3
6 A text ... 2
7 A text ... 10
8 A text ... 11
If my profile/user is identified by the id 1, and it has only 2 friends (the profiles identified by id 2 and 3) and also I need my search to return only 2 results by each user, my SELECT has to return updates 1,2,3 and 5.
Preferably updates should be grouped by its author and it would be great if I could set the number of different profiles to be considered in this search (for example, if profile 1 had 10 friends and I wanted only updates from 3 profiles, the most recent must appear first).
Do you know how can I achieve this??
thank you very much!
#EDIT
This returns all updates sent by friends of profile 1. But i'm not sure whether or not i'm in the right direction
SELECT u.*
FROM `update` u
INNER JOIN friendship f1 ON f1.profile1 = u.author
WHERE f1.profile2 =1
UNION
SELECT u.*
FROM `update` u
INNER JOIN friendship f2 ON f2.profile2 = u.author
WHERE f2.profile1 =1
If you are willing to do it in two queries, you can do it like this. First, get three profiles who have most recently posted based on your constraints:
-- Get the three latest updated profiles from here.
-- (we can't use a CTE because MySQL doesn't support
-- them yet).
SELECT DISTINCT p.profile FROM
(
SELECT ui.profile, ui.time FROM
(
SELECT u.profile, u.time
FROM `update` u
INNER JOIN `friendship` f ON f.profile2 = u.profile
WHERE f.profile1 = 1
UNION ALL
SELECT u.profile, u.time
FROM `update` u
INNER JOIN `friendship` f ON f.profile1 = u.profile
WHERE f.profile2 = 1
) ui ORDER BY ui.time DESC
) p LIMIT 0, 3;
From that query, get the three profile IDs out and put them in place of <id1>, <id2> and <id3> in the following query
-- Use a union to get the result set back
(SELECT a.content, a.time, a.profile FROM `update` a
WHERE a.profile = <id1>
ORDER BY a.time DESC
LIMIT 0, 2)
UNION ALL
(SELECT a.content, a.time, a.profile FROM `update` a
WHERE a.profile = <id2>
ORDER BY a.time DESC
LIMIT 0, 2)
UNION ALL
(SELECT a.content, a.time, a.profile FROM `update` a
WHERE a.profile = <id3>
ORDER BY a.time DESC
LIMIT 0, 2);
If you get less than three profiles back, either remove parts of the query in your PHP code, or set the WHERE clause to something like 0 so it always evaluates to fault (assuming you don't have a profile ID of zero)
The 2 in the limit clauses above can be changed if you want more or fewer results per profile.
Sample SQL fiddle: http://sqlfiddle.com/#!2/22e57/1 (updated fiddle to make the content more meaningful and to use times)
I would suggest doing a series of queries for each author within one transaction, that way there would not be a need for grouping - you could simply append results together outside of your SQL.
SELECT * FROM `update` WHERE
profile IN (SELECT profile2 FROM `friendship` WHERE profile1=1) OR
profile IN (SELECT profile1 FROM `friendship` WHERE profile2=1);
try this sqlFiddle
SELECT T1.profile,T1.content,T1.time
FROM
(SELECT UPD.profile,UPD.content,UPD.time,
IF (#prevProfile != UPD.profile,#timeRank:=1,#timeRank:=#timeRank+1) as timeRank,
#prevProfile := UPD.profile
FROM
(SELECT UP.profile,UP.content,UP.time
FROM
(SELECT profile,max(time) as latestUpdateTime
FROM friendship F INNER JOIN updates U
ON (F.profile1 = 1 AND U.profile = profile2) /* <-- specify profile on this line */
OR(F.profile2 = 1 AND U.profile = profile1) /* <-- specify profile on this line */
GROUP BY profile
ORDER BY latestUpdateTime DESC
LIMIT 3 /* limit to 3 friends profiles that have the most recent updates */
)as LU
INNER JOIN updates UP
ON (UP.profile = LU.profile)
ORDER BY profile,time DESC
)as UPD,(SELECT #prevProfile:=0,#timeRank:=0)variables
)T1
WHERE T1.timeRank BETWEEN 1 AND 2 /* grab 2 lastest updates for each profile */
ORDER BY T1.time DESC
in my example, profile id 1 has more than 3 friends, but i am only grabbing 3 friends that made the most recent updates.
explanation of above query.
LU grabs 3 profiles that are friends with profile id 1 that made the latest updates.
UPD grabs all contents that belong to these 3 friends.
T1 returns the contents along with a timeRank number for each content in order from 1 counting upward order by time DESCENDING for each profile
and finally the WHERE we only grab 2 content updates for each profile
then we finally ORDER these updates based on TIME starting from most recent.

Select records from table1 depending on table2

I am trying to build a project where you can like other people's pictures, and when the other person likes your picture too, you have a match. Like the Tinder app if you know.
Now, I fetch 1 photo like so:
SELECT id, picture_path, profile_picture, username
FROM tusers
WHERE profile_picture IS NOT NULL
AND settings LIKE '1,%'
AND sex = :sex
AND last_visit BETWEEN CURDATE() - INTERVAL 21 DAY AND CURDATE()
AND dob BETWEEN :dob - INTERVAL 5 YEAR AND :dob2 + INTERVAL 5 YEAR
LIMIT 1
However, if you've already LIKED or PASSED someone's photo, I don't want to show it to you again. I am not sure how to do this part yet (right now, I have alreadyLiked() and alreadyPassed() functions and I am only doing a header("Location") redirect if they return true, but that will fail when you have liked/passed all the photos).
I have another table with these columns: id, user1_id, user2_id, liked, passed, matched
When you like or pass a picture, a 1 is inserted in the corresponding column.
user1_id is your ID. user2_id is the other person's ID.
Knowing the above information, what kind of query (or logic) would you use to make sure that you only show the right people (that you haven't liked or passed already) ?
suppose you have 2 tables
usr
id username
1 a
2 b
3 c
4 d
liked
id user1 user2 liked
1 1 4 1
2 1 3 1
assuming your id is 1 , from table liked it seems you have liked c,d . since 1(a) is your own id you need only b as output, your query goes as below
SELECT *
FROM usr
WHERE id NOT
IN (
SELECT user2
FROM liked
WHERE user1 =1
)
and id!=1
assuming 1 will come from session
You can use mysql join to get data from one table based on the another
In the upper case you can have join on the first tables id with the other tables user1_id, user2_id putting where clause on the liked, passed, matched
To know more about mysql joins
Try this as join is much better than a inner query suggested by ersumit $loggedINuser_id='2';//take from session $sql="SELECT tu.id, tu.picture_path, tu.profile_picture, tu.username FROM tusers tu LEFT JOIN secondtable st ON tu.id=st.user2_id WHERE tu.profile_picture IS NOT NULL AND tu.settings LIKE '1,%' AND tu.sex = :sex AND tu.last_visit BETWEEN CURDATE() - INTERVAL 21 DAY AND CURDATE() AND tu.dob BETWEEN :dob - INTERVAL 5 YEAR AND :dob2 + INTERVAL 5 YEAR AND st.user1_id != '".$loggedINuser_id."' LIMIT 1";

Mysql facebook style message join 3 tables

//USER TABLE
user_id name
1 ben
2 alex
3 john
//CONVERSION TABLE
c_id user_one user_2
1 2(alex) 1(ben)
2 2(alex) 3(john)
3 1(ben) 3(john)
//MESSAGE TABLE
m_id c_id send receive message
1 1 2(alex) 1(ben) hi ben
2 1 2(alex) 1(ben) ben, u there?
3 2 1(ben) 3(john) whatever...
//QUERY 1
SELECT * FROM conversion WHERE user_one=1(ben)
OR user_two=1(ben)
So now i know ben have 2 conversations (one with alex another with john)
my question is
how to join 3 tables and fetch out like this
conversation_1 - Alex(id=2) - Last message in cv_1(ben, u there?)
conversationi_3 - John(id=3) - Last message in cv_3(whatever...)
like facebook message
The main idea is that you have to use joins. Standard JOIN syntax will be of no help here because you cannot have OR statement in JOIN .. ON. But something like this will do the trick
SELECT c.c_id, u.user_id, u.name, MAX(m_id), message FROM message m, conversation c, user u
WHERE m.c_id = c.c_id
AND
(
c.user_one = u.user_id
OR
c.user_2 = u.user_id
)
GROUP BY c.c_id
Here we join 3 tables together, getting maximum message ID (I assume ID is auto incremental so it is safe to assume that the higher ID the older the message) and group by conversation id. This is how we will have the oldest message and conversation details of messages where Ben (logged in user for instance) was involved
A nice article I saw somewhere on stack overflow before is http://www.khankennels.com/blog/index.php/archives/2007/04/20/getting-joins/. The current approach is INNER JOIN.
SQL Fiddle - http://sqlfiddle.com/#!2/ae6e6/14

Categories