Group by only showing the first record - php

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.

Related

Mysql - Select & Group rows with A column, and B column where A.value and B.value = x and y or y and x

I have a table which has 2 numeric (int) values (IDs) as sender_id and receiver_id.
What I'm trying to do is group these rows as "conversations". the conversation between :
sender_id = 1, and receiver_id = 5,
is the same conversation where sender_id = 5 and receiver_id = 1
So when I fetch the database rows, I want to have only 1 row for each conversation.
I will fetch these conversation for a specific user. the user ID (which I already know and will refer to as userId) can be in sender_id and receiver_id.
So my algorithm would be something like:
Select All rows
from Database,
where sender_id = userId OR receiver_id = userId,
group by sender_id+user_id
the result rows have to include the other column value (not the one equal to userId), it can include both though and I will have to detect the other one by comparing them both to the userId I have.
To be honest I have always been uncomfortable with SQL manual. For some reason I'm failing to find what I need from there. Forgive me..
I'm not sure if I should make a temp table or use the SQL SUM function, or if I can use something like GROUP BY sender_id+receiver_id
More info (not sure if relevant):
I'm using php
The table has few more columns like record_id, create_date and read_date
Your help and replies are greatly appreciated.
Update:
The solution to my question is marked below. However, I encountered another problem that I thought of sharing, as it can maybe potentially help someone viewing this question later.
The problem I faced is, when I show the conversations, I wanna show the last message row. Now when grouping is done, the grouping stacks on the first row found. and I want the opposite. I want the rows returned to contain the last message row per conversation.
I could do that with the following query. Please note that this is only one way of doing it and I'm not yet sure of the performance of this query. I will test it later and update this if needed.
the $this->id is the id of the user I wanna display the conversations for.
"SELECT t1.* FROM table t1
INNER JOIN (
SELECT sender_id, receiver_id, max(create_date) lastCreateDate
FROM table
WHERE receiver_id = ".$this->id." OR sender_Id = ".$this->id."
GROUP BY GREATEST(sender_id ,receiver_id), LEAST(sender_id,receiver_id)
) t2
ON ((t1.sender_id = t2.sender_id
OR t1.sender_id = t2.receiver_id)
AND t1.create_date = t2.lastCreateDate)
ORDER BY create_date DESC LIMIT 0,15"
LEAST returns the lowest and GREATEST the greatest value:
GROUP BY LEAST(sender_id, receiver_id), GREATEST(sender_id, receiver_id)
GROUP BY max(sender_id, receiver_id), min(sender_id, receiver_id)

Getting the latest message between all conversations with other users

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.

messages between users: show conversations

I am coding a message system for user communication. In the inbox I do not want to show the user the messages he/she received. I just want to show the conversations. So as an example, if one user send or receive more than one message then in the inbox there should only be the conversation (which includes the newest message, either written or received) with the user and when the user clicks on the conversation he/she can see all past messages.
The table structure (simplified) of 'messages' is as followed:
message_id
user_id_sender
user_id_recipient
message
Now the problem is that the messages are saved in a database where each row is one message, so I have to group these messages in a certain way.
The select statement I came up with is the following:
SELECT * FROM messages
WHERE user_id_sender = 1 OR user_id_recipient = 1
GROUP BY user_id_sender
But now I obviously get two messages because one which has been written by user '1' and one that he has received..
Does anybody have an idea how to solve this?
I've solved this problem some month ago. I suppose you have also a date field. This query give you a well structured results with date of last message and last message.
$qry = 'SELECT
CONCAT(GREATEST(user_id_sender,user_id_recipient)," ",LEAST(user_id_sender,user_id_recipient)) AS thread,
MAX(CONCAT(date,"|",message)) as date_message,
MAX(date) AS last_message,
messages.*
FROM messages
WHERE user_id_sender= ? || user_id_recipient=? GROUP BY thread ORDER BY last_message DESC';
$rows = $db->fetchAll($qry, Array($current_user_id,$current_user_id));
Assuming message_id is ascending (i.e. higher ids are for later messages). #user_id is just a placeholder for the user_id of the inbox you are looking at. I've used Andrea's trick for getting the other_recipient_id concisely.
Select
mm.other_recipient_id,
m.*
From (
Select
user_id_sender + user_id_recipient - #user_id as other_recipient_id,
Max(message_id) as message_id
From
messages
Where
user_id_sender = #user_id Or
user_id_recipient = #user_id
Group By
user_id_sender + user_id_recipient - #user_id
) mm
Inner Join
messages m
On
mm.message_id = m.message_id
Example fiddle http://sqlfiddle.com/#!2/05d191/3/0
You get only one message, as long as the messages (message_id) are unique. You will get multiple messages, of course, if there is a longer communication going on.
N.B.: You don't need group by here, because this is used only for aggregating columns, e.g.:
SELECT user_id_sender, sum(*) FROM messages
GROUP BY user_id_sender;
where you get the sum of messages each user has sent.
So, if you want to see the communication between two users:
SELECT * FROM messages
WHERE user_id_sender = 1 OR user_id_recipient = 1;
You can restrict this further, if you store the timestamps as well, message_time for example or limit the number of messages displayed:
select * from ... limit 10;
Assuming we want to see conversations of user with certain _ ID _, this query can be useful:
SELECT (user_id_sender + user_id_recipient) - _ID_ AS correspondent, COUNT(*) AS total
FROM messages
WHERE user_id_sender = _ID_ OR user_id_recipient = _ID_
GROUP BY (user_id_sender + user_id_recipient)
The resulting query returns the ID of the other user of the conversation ("correspondent") and the number of messages between the two users ("total").
Regards

A MySQL query, some normalized tables, issues w/ counts from the normalized table rows (from PHP)

Update: Added Schema to the bottom...
I have a table of contracts: tbl_contract
And a table of users associated with the contract: tbl_contract2user
There can be any number of entries in tbl_contract2user, in which an entry existing means that the relationship exists (along with a pending column where 1 = pending and 0 = approved).
My goal here is to select all contracts where there is 1 (or more) active users within the time frame specified (see below).
The problem I'm having is the ability to sort out these contracts properly. The date range is working fine... For some reason I'm having trouble understanding when the number of users is 1 or more...(vs. 0) and yes - I'll be working with that data set (After the query).
See below for the start of the query...
$result = mysql_query("SELECT tbl_contract.id
FROM tbl_contract
LEFT JOIN tbl_contract2user ON tbl_contract.id = tbl_contract2user.contractID
WHERE tbl_contract2user.pending = 0
AND tbl_contract.startDate <= {$billing['start_time']}
AND tbl_contract.endDate >= {$billing['end_time']}");
Schema:
tbl_contract: id, startDate, endDate, value, name, dateCreated
tbl_contract2user: id, contractID, userID, pending
What is the actual problem?
Do you get all records instead of only those with a related user? If yes, turn the LEFT JOIN into a INNER JOIN and all contracts without a relation are gone...
The real issue is that if I have 6 users in one contract, I get 6 rows
returned instead of ONE row for that contract
This is exactly what a JOIN does. It takes all records from the left side and joins them with the records on the right side by using a specific condition. If you only want to know how many users a contract has, you can you a GROUP BY clause and a COUNT(*):
SELECT tbl_contract.id, COUNT(*) AS userCount
FROM tbl_contract
LEFT JOIN tbl_contract2user ON tbl_contract.id = tbl_contract2user.contractID
WHERE tbl_contract2user.pending = 0
AND tbl_contract.startDate <= {$billing['start_time']}
AND tbl_contract.endDate >= {$billing['end_time']}
GROUP BY tbl_contract.id
If you need more information about the user, you really need all these 6 rows...

Get multiple GROUP BY results per group, or use separate concatenated table

I am working on an auction web application. Now i have a table with bids, and from this table i want to select the last 10 bids per auction.
Now I know I can get the last bid by using something like:
SELECT bids.id FROM bids WHERE * GROUP BY bids.id ORDER BY bids.created
Now I have read that setting an amount for the GROUP BY results is not an easy thing to do, actually I have found no easy solution, if there is i would like to hear that.
But i have come up with some solutions to tackle this problem, but I am not sure if i am doing this well.
Alternative
The first thing is creating a new table, calling this bids_history. In this table i store a string of the last items.
example:
bids_history
================================================================
auction_id bid_id bidders times
1 20,25,40 user1,user2,user1 time1,time2,time3
I have to store the names and the times too, because I have found no easy way of taking the string used in bid_id(20,25,40), and just using this in a join.
This way i can just just join on auction id, and i have the latest result.
Now when there is placed a new bid, these are the steps:
insert bid into bids get the lastinserteid
get the bids_history string for this
auction product
explode the string
insert new values
check if there are more than 3
implode the array, and insert the string again
This all seems to me not a very well solution.
I really don't know which way to go. Please keep in mind this is a website with a lot of bidding's, they can g up to 15.000 bidding's per auction item. Maybe because of this amount is GROUPING and ORDERING not a good way to go. Please correct me if I am wrong.
After the auction is over i do clean up the bids table, removing all the bids, and store them in a separate table.
Can someone please help me tackle this problem!
And if you have been, thanks for reading..
EDIT
The tables i use are:
bids
======================
id (prim_key)
aid (auction id)
uid (user id)
cbid (current bid)
created (time created)
======================
auction_products
====================
id (prim_key)
pid (product id)
closetime (time the auction closses)
What i want as the result of the query:
result
===============================================
auction_products.id bids.uid bids.created
2 6 time1
2 8 time2
2 10 time3
5 3 time1
5 4 time2
5 9 time3
7 3 time1
7 2 time2
7 1 time3
So that is per auction the latest bids, to choose by number, 3 or 10
Using user variable, and control flow, i end up with that (just replace the <=3 with <=10 if you want the ten auctions) :
SELECT a.*
FROM
(SELECT aid, uid, created FROM bids ORDER BY aid, created DESC) a,
(SELECT #prev:=-1, #count:=1) b
WHERE
CASE WHEN #prev<>a.aid THEN
CASE WHEN #prev:=a.aid THEN
#count:=1
END
ELSE
#count:=#count+1
END <= 3
Why do this in one query?
$sql = "SELECT id FROM auctions ORDER BY created DESC LIMIT 10";
$auctions = array();
while($row = mysql_fetch_assoc(mysql_query($sql)))
$auctions[] = $row['id'];
$auctions = implode(', ', $auctions);
$sql = "SELECT id FROM bids WHERE auction_id IN ($auctions) ORDER BY created LIMIT 10";
// ...
You should obviously handle the case where, e.g. $auctions is empty, but I think this should work.
EDIT: This is wrong :-)
You will need to use a subquery:
SELECT bids1.id
FROM ( SELECT *
FROM bids AS bids1 LEFT JOIN
bids AS bids2 ON bids1.created < bids2.created
AND bids1.AuctionId = bids2.AuctionId
WHERE bid2.id IS NULL)
ORDER BY bids.created DESC
LIMIT 10
So the subquery performs a left join from bids to itself, pairing each record with all records that have the same auctionId and and a created date that is after its own created date. For the most recent record, there will be no other record with a greater created date, and so that record would not be included in the join, but since we use a Left join, it will be included, with all the bids2 fields being null, hence the WHERE bid2.id IS NULL statement.
So the sub query has one row per auction, contianing the data from the most recent bid. Then simply select off the top ten using orderby and limit.
If your database engine doesn't support subqueries, you can use a view just as well.
Ok, this one should work:
SELECT bids1.id
FROM bids AS bids1 LEFT JOIN
bids AS bids2 ON bids1.created < bids2.created
AND bids1.AuctionId = bids2.AuctionId
GROUP BY bids1.auctionId, bids1.created
HAVING COUNT(bids2.created) < 9
So, like before, left join bids with itself so we can compare each bid with all the others. Then, group it first by auction (we want the last ten bids per auction) and then by created. Because the left join pairs each bid with all previous bids, we can then count the number of bids2.created per group, which will give us the number of bids occurring before that bid. If this count is < 9 (because the first will have count == 0, it is zero indexed) it is one of the ten most recent bids, and we want to select it.
To select last 10 bids for a given auction, just create a normalized bids table (1 record per bid) and issue this query:
SELECT bids.id
FROM bids
WHERE auction = ?
ORDER BY
bids.created DESC
LIMIT 10
To select last 10 bids per multiple auctions, use this:
SELECT bo.*
FROM (
SELECT a.id,
COALESCE(
(
SELECT bi.created
FROM bids bi
WHERE bi.auction = a.id
ORDER BY
bi.auction DESC, bi.created DESC, bi.id DESC
LIMIT 1 OFFSET 9
), '01.01.1900'
) AS mcreated
COALESCE(
(
SELECT bi.id
FROM bids bi
WHERE bi.auction = a.id
ORDER BY
bi.auction DESC, bi.created DESC, bi.id DESC
LIMIT 1 OFFSET 9
), 0)
AS mid
FROM auctions a
) q
JOIN bids bo
ON bo.auction >= q.auction
AND bo.auction <= q.auction
AND (bo.created, bo.id) >= (q.mcreated, q.mid)
Create a composite index on bids (auction, created, id) for this to work fast.

Categories