MYSQL GROUP BY two columns in chat - mysql

I'm setting up a chat-service.
The table for my messages is created as followed:
CREATE TABLE message (
fromId int NOT NULL,
toId int NOT NULL,
message text NOT NULL,
timestamp timestamp NOT NULL,
toRead int NOT NULL,
FOREIGN KEY (fromId) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (toId) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
As a user I should get all other users I already chatted with. Therefore I create an sql-statement:
SELECT u.name, u.surname, u.id, m.toRead, m.timestamp, m.fromId, m.toId
FROM message m, user_informations u
WHERE (m.toId = '2' AND m.fromId = u.id) OR (m.fromId = '2' AND m.toId = u.id)
GROUP BY concat(m.fromId, m.toId OR m.toId, m.fromId)
ORDER BY m.timestamp LIMIT 10;
It works as long as one users only writes the other. If both have written each other I get two returned columns
'Peter','Tester','1','0','2015-07-27 16:10:11','1','2'
'Peter','Tester','1','0','2015-07-27 17:14:22','2','1'
because fromId in first result is other than in second and same with toId.
How can I get the statement to return only one row?

Related

How to get corresponding FIELD to MAX(DATE) in mySQL?

My two tables are as follows :
Table 1 : Transaction
TRANS_ID (primary key), TRANS_DATE, TRANS_STATUS, USER_ID (Foreign_Key)
The same user_id will be repeated when status change
Table 2 : Users
USER_ID (Primary_Key), USER_NAME, USER_DOB, OTHER_INFO
I want to get the user information along with last transaction status.
I am familiar with the following query.
SELECT MAX(Transaction.TRANS_DATE),Transaction.TRANS_STATUS, Users.USER_NAME, Users.USER_DOB
FROM Users
INNER_JOIN Transaction ON Transaction.USER_ID = Users.USER_ID
WHERE Transaction.USER_ID = #UserID
I pass the UserID with Parameter.AddWithValue. Unfortunately, this method does not return the TRANS_STATUS for the MAX(TRANS_DATE). It does return MAX(TRANS_DATE) but TRANS_STATUS is the first occurrence, not the corresponding STATUS to MAX(TRANS_DATE) record.
Please let me know how I could get the TRANS_STATUS for the MAX(TRANS_DATE). I prefer using INNER JOIN but recommendations are appreciated.
I could not still get to working.
Here are my table scripts.
CREATE TABLE `Transactions` (
`TRANS_ID` int(11) NOT NULL,
`TRANS_DATE` datetime NOT NULL,
`TRANS_STATUS` varchar(45) NOT NULL,
`USER_ID` int(11) NOT NULL,
PRIMARY KEY (`TRANS_ID`),
UNIQUE KEY `TRANS_ID_UNIQUE` (`TRANS_ID`),
KEY `USER_ID_idx` (`USER_ID`),
CONSTRAINT `USER_ID` FOREIGN KEY (`USER_ID`) REFERENCES `Users` (`USER_ID`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8
CREATE TABLE `Users` (
`USER_ID` int(11) NOT NULL AUTO_INCREMENT,
`USER_NAME` varchar(45) NOT NULL,
`USER_DOB` datetime NOT NULL,
`OTHER_INFO` varchar(45) NOT NULL,
PRIMARY KEY (`USER_ID`),
UNIQUE KEY `USER_ID_UNIQUE` (`USER_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
If I understood right this could work for you
SELECT A.USER_ID AS USER_ID, A.TRANS_DATE AS TRANS_DATE, TRANS_STATUS, USER_NAME, USER_DOB
FROM
(SELECT USER_ID, MAX(TRANS_DATE) AS TRANS_DATE FROM TRANSACTION
GROUP BY USER_ID) A
INNER JOIN
(SELECT USER_ID, TRANS_DATE, TRANS_STATUS FROM TRANSACTION) B
ON A.USER_ID = B.USER_ID
AND A.TRANS_DATE=B.TRANS_DATE
INNER JOIN USERS U
ON A.USER_ID=U.USER_ID;
SELECT Users.USER_NAME, Users.USER_DOB
FROM Users usr INNER JOIN(
SELECT Transaction.TRANS_STATUS, MAX(Transaction.TRANS_DATE)
FROM Transaction GROUP BY Transaction.TRANS_STATUS) trs ON trs.USER_ID=usr.USER_ID
You can use LAST_VALUE function
SELECT Top 1
u.User_ID
,u.user_name
,u.user_dob
,u.other_info
,LAST_VALUE(t.Trans_Date) OVER (PARTITION BY t.user_id ORDER BY
t.Trans_Date RANGE BETWEEN UNBOUNDED PRECEDING AND
UNBOUNDED FOLLOWING) AS Max_Tran_Date
,LAST_VALUE(t.trans_status) OVER (PARTITION BY t.user_id ORDER BY
t.Trans_Date RANGE BETWEEN UNBOUNDED PRECEDING AND
UNBOUNDED FOLLOWING) as Last_Status
FROM #Users as u
INNER JOIN #Transaction as t ON t.user_id = u.user_id
WHERE u.User_ID = #UserID
You could use a join on the subquery for max TRANS_DATE
select t.max_trans, t.USER_NAME, t.USER_DOB
from Transaction
INNER JOIN
(
SELECT MAX(Transaction.TRANS_DATE) max_trans,
Transaction.USER_ID, Users.USER_NAME, Users.USER_DOB
FROM Users
INNER_JOIN Transaction ON Transaction.USER_ID = Users.USER_ID
WHERE Transaction.USER_ID = #UserID
) t on Transaction.USER_ID = t.USER_ID and t.max_trans = Transaction.TRANS_DATE

How to order a subquery in MySQL?

I'm working on a program that allows a user to create events, and have other users RSVP to those events. When fetching these events, I would like to be able to sort the attendees by when they have RSVP'd (both those that have accepted, and those that have declined). I store the time that a user RSVP'd in the database. These are the relevant tables:
CREATE TABLE users (
username VARCHAR(50) NOT NULL,
server_id VARCHAR(40) NOT NULL,
role VARCHAR(10),
PRIMARY KEY (username, server_id),
FOREIGN KEY (server_id) REFERENCES servers(server_id)
ON DELETE CASCADE
);
CREATE TABLE events (
server_id VARCHAR(40) NOT NULL,
start_time DATETIME NOT NULL,
time_zone VARCHAR(5) NOT NULL,
title VARCHAR(256) NOT NULL,
description VARCHAR(1000),
PRIMARY KEY (server_id, title),
FOREIGN KEY (server_id) REFERENCES servers(server_id)
ON DELETE CASCADE
);
CREATE TABLE user_event (
username VARCHAR(50) NOT NULL,
server_id VARCHAR(40) NOT NULL,
title VARCHAR(256) NOT NULL,
attending BOOLEAN NOT NULL,
last_updated DATETIME NOT NULL,
PRIMARY KEY (username, server_id, title),
FOREIGN KEY (server_id, title) REFERENCES events(server_id, title)
ON DELETE CASCADE,
FOREIGN KEY (server_id, username) REFERENCES users(server_id, username)
ON DELETE CASCADE
);
Previous to wanting to sort by when a user RSVP'd, I was using this query and it was working great.
SELECT title, description, start_time, time_zone, (
SELECT GROUP_CONCAT(DISTINCT username)
FROM user_event
WHERE user_event.server_id = %s
AND user_event.title = %s
AND user_event.attending = 1)
AS accepted, (
SELECT GROUP_CONCAT(DISTINCT username)
FROM user_event
WHERE user_event.server_id = %s
AND user_event.title = %s
AND user_event.attending = 0)
AS declined
FROM events
WHERE server_id = %s
AND title = %s;
However, adding an ORDER BY last updated clause to the two nested SELECT statements does not seem to have any effect. Is this a limitation of MySQL, or is there still a way I can accomplish this?
You probably want ORDER BY inside GROUP_CONCAT:
SELECT title, description, start_time, time_zone, (
SELECT GROUP_CONCAT(DISTINCT username ORDER BY last updated)
FROM user_event
WHERE user_event.server_id = %s
AND user_event.title = %s
AND user_event.attending = 1)
AS accepted, (
SELECT GROUP_CONCAT(DISTINCT username ORDER BY last updated)
FROM user_event
WHERE user_event.server_id = %s
AND user_event.title = %s
AND user_event.attending = 0)
AS declined
FROM events
WHERE server_id = %s
AND title = %s;

Join tables when one table is empty

I have three tables which I need to join: User, Notification and UserNotification. The latter is just a cross reference table between User and Notificaion with columns UserID (fk to User) and LastReadNotificationID (fk to Notification). Table UserNotification should contain a reference to the last read Notification in a user-notification feature in a web app. So if we have two records in Notification with ID 1 and 2, and one record in UserNotification with a fk to Notification = 1, it means that the user has NOT read the last created notification which I want to display at the next login.
Now, I need to select all columns from the User table at login and add another column (Notify) to the resultset. Notify should be a boolean that should be false if:
Notification is empty
or Notification contains a record with eg ID = 10 AND UserNotification does have the corresponding foreign key.
Notify should be true if:
Notification contains a record AND UserNotification is empty.
or Notification contains a record with eg ID = 10 BUT UserNotification does NOT have the corresponding foreign key.
The problem is that I can't write a query that meets all the above requirements. The query I have at the moment works except when Notification is empty (and thus is UserNotification). In this case my query returns Notify = true;
If have tried many different ways to solve (left joins, right joins, if, case when, ifnull etc) this but I'm stuck. Please help.
The query I use now:
SELECT ID, FirstName, LastName, Email, Password, Roles, LastLoginTime, LoginCount, Active,
(SELECT IFNULL((SELECT 0 FROM UserNotification UN, User U
WHERE UN.UserId = U.ID AND U.Email = :email
AND UN.LastReadNotificationID <=> (SELECT MAX(ID) FROM Notification WHERE Display = 1)), 1)) AS Notify
FROM User WHERE Email = :email;
The 3 tables:
CREATE TABLE `User` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`FirstName` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`LastName` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`Email` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`Password` varchar(200) CHARACTER SET utf8 DEFAULT NULL,
`Roles` varchar(200) CHARACTER SET utf8 DEFAULT NULL,
`LastLoginTime` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`LoginCount` int(11) DEFAULT NULL,
`Active` bit(1) NOT NULL DEFAULT b'1',
PRIMARY KEY (`ID`),
UNIQUE KEY `Email` (`Email`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;
CREATE TABLE `UserNotification` (
`UserID` bigint(20) NOT NULL,
`LastReadNotificationID` int(11) NOT NULL,
UNIQUE KEY `UserID_UNIQUE` (`UserID`),
KEY `fk_UserNotification_Notification_ID` (`LastReadNotificationID`),
CONSTRAINT `fk_UserNotification_Notification_ID` FOREIGN KEY (`LastReadNotificationID`) REFERENCES `Notification` (`ID`) ON DELETE CASCADE,
CONSTRAINT `fk_UserNotification_User_ID` FOREIGN KEY (`UserID`) REFERENCES `User` (`ID`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `Notification` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`Text` text NOT NULL,
`Created` timestamp NOT NULL,
`UserID` bigint(20) NOT NULL,
`Display` bit(1) DEFAULT NULL,
PRIMARY KEY (`ID`),
KEY `fk_Notification_User_ID` (`UserID`),
CONSTRAINT `fk_Notification_User_ID` FOREIGN KEY (`UserID`) REFERENCES `User` (`ID`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;
The problem with your attempts probably boils down to one basic issue: you want to chain the three tables together with left joins (A -> B -> C) and yet you also want to be able to infer whether there are any other rows in the table that fall outside of the join logic and the pairs of IDs.
B and C can both be empty which is why it's natural to approach this with left joins. (B's "emptiness" is per user, of course.) The problem is that B sits in the middle and it can be empty even if C is not. But when B is empty you can't determine anything conclusive about C just from the results of the join.
select
u.ID as UserID, FirstName, LastName, Email, Password,
Roles, LastLoginTime, LoginCount, Active,
case
when max_n.IsEmpty = 1 or un.LastReadNotification = max_n.ID then 0
-- the following is equivalent and eliminates the IsEmpty flag entirely
-- when coalesce(max_n.ID, 0) = coalesce(un.LastReadNotification, 0) then 1
else 1 -- isn't it sufficient to just return 1 at this point?
-- when un.LastReadNotification is null then 1 -- notification wasn't empty btw
-- when un.LastReadNotification < agg_n.MaxID then 1 -- can't be greater, right?
end as Notify
from
User u
left outer join UserNotification un
on un.UserID = u.ID
cross join (
select
case when max(ID) is not null then 0 else 1 end as IsEmpty,
max(ID) as ID
from Notification
) max_n
You may prefer a subquery over the cross join:
select
u.ID as UserID, FirstName, LastName, Email, Password,
Roles, LastLoginTime, LoginCount, Active,
case
when coalesce((select max(ID) from Notification), 0)
= coalesce(LastReadNotification, 0)
else 1
end as Notify
from User u left outer join UserNotification un on un.UserID = u.ID
Ultimately all you really needed to know is whether there is a notification with ID greater than the ID last seen by each user. Grabbing just the highest notification ID is enough information to let you make that determination across the board.
Here's another thought though: would it possibly be easier to just have LastReadNotification in the user table with an initial value of 0 along with a dummy row with ID 0 in the "empty" notification table? Essentially you remove the need to know anything about emptiness at all. If you implemented the 0 row you'd just end up with something like this:
select
ID as UserID, FirstName, LastName, Email, Password,
Roles, LastLoginTime, LoginCount, Active,
case when exists (
select 1
from Notification n
where n.ID > u.LastReadNotification
) then 1 else 0 end as NotifyA,
(
select sign(count(*))
from Notification n
where n.ID > u.LastReadNotification
) as NotifyB
from User
Please expand on your desired results. Do you want the actual last entered notification that the user has not read only? Or do you just want to know if you should notify the user?
SELECT
u.Id
,u.FirstName
,u.LastName
,u.Email
,u.Password
,u.Roles
,u.LastLoginTime
,u.LoginCount
,u.Active
,CASE WHEN un.UserId IS NULL THEN 1 Notify
FROM
User u
CROSS JOIN Notification n
LEFT JOIN UserNotification un
ON u.ID = un.UserId
AND n.Id = un.LastReadNotificationId
WHERE
U.Email = :email
AND n.Display = 1
;
This query will give you every combination possible for Users to Notifications and then join the UserNotifications table if a record exists. It will specify to notify but for all Notifications that the user has not read. so user will be repeated by all Notifications.
You can then limit to notify and specify the latest Notification the user has not read by using a case statement and group by.
SELECT
u.Id
,u.FirstName
,u.LastName
,u.Email
,u.Password
,u.Roles
,u.LastLoginTime
,u.LoginCount
,u.Active
,CASE
WHEN
SUM(CASE
WHEN un.UserId IS NULL THEN 1
ELSE 0
END) > 0 THEN 1
ELSE 0
END AS Notify
,MAX(CASE WHEN un.UserId IS NULL THEN n.ID END) AS NextNotification
FROM
User u
CROSS JOIN Notification n
LEFT JOIN UserNotification un
ON u.ID = un.UserId
AND n.Id = un.LastReadNotificationId
WHERE
U.Email = :email
AND n.Display = 1
GROUP BY
u.Id
,u.FirstName
,u.LastName
,u.Email
,u.Password
,u.Roles
,u.LastLoginTime
,u.LoginCount
,u.Active
If the user has read all notification the NextNotifcation will be NULL as MAX ignores null values and the case statement doesn't specify and else therefore the value would be null.
If you just want to know if you should notify the user just ignore/remove the NextNotifcation column.
Another way to do this which would be similar to the way you were proceeding would be:
SELECT
u.Id
,u.FirstName
,u.LastName
,u.Email
,u.Password
,u.Roles
,u.LastLoginTime
,u.LoginCount
,u.Active
,CASE
WHEN EXISTS
(SELECT
*
FROM
Notifications n
LEFT JOIN UserNotifications un
ON n.Id = un.LastReadNotificationId
AND un.UserId = u.Id
WHERE
n.Display = 1
)
THEN 1
ELSE 0
END AS Notify
FROM
Users u
WHERE
u.Email = :email
;
Normally I don't like putting a sub select in a column definition but looking at your datatype of BIGINT for users it may actually be better performance to use EXISTS and not the cross join.

MySQL MAX and MIN

I am trying to execute the following query
SELECT `id`,
`name`,
`ownerid`,
`creationdata`,
`motd`,
(SELECT Count(*)
FROM guild_membership a,
players_online b
WHERE a.player_id = b.player_id
AND a.guild_id = id) AS `online`,
(SELECT Max(b.level)
FROM guild_membership a,
players b
WHERE a.player_id = b.id
AND a.guild_id = id) AS `toplevel`,
(SELECT Min(a.level)
FROM players a,
guild_membership b
WHERE a.id = b.player_id
AND b.guild_id = id) AS `lowlevel`
FROM `guilds`
WHERE `name` = 'Wideswing Poleaxe'
LIMIT 1;
The tables used in here are the followin
CREATE TABLE IF NOT EXISTS `players` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`group_id` int(11) NOT NULL DEFAULT '1',
`account_id` int(11) NOT NULL DEFAULT '0',
`level` int(11) NOT NULL DEFAULT '1',
...
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`),
FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE,
KEY `vocation` (`vocation`)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `guilds` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`ownerid` int(11) NOT NULL,
`creationdata` int(11) NOT NULL,
`motd` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY (`name`),
UNIQUE KEY (`ownerid`),
FOREIGN KEY (`ownerid`) REFERENCES `players`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `guild_membership` (
`player_id` int(11) NOT NULL,
`guild_id` int(11) NOT NULL,
`rank_id` int(11) NOT NULL,
`nick` varchar(15) NOT NULL DEFAULT '',
PRIMARY KEY (`player_id`),
FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (`rank_id`) REFERENCES `guild_ranks` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;
I am trying to get the MAX level and MIN level on the players table inside one guild
However I am always getting toplevel and lowlevel the same value and tis always the lowest level
I am not sure what is wrong
First thing I notice is: you are using LIMIT without ORDER BY. So from the guilds table you expect to find more than one entry for name = 'Wideswing Poleaxe', but only look at the first the DBMS happens to find. Is this desired?
Next thing I see is the out-dated join syntax. Where did you get this from? A twenty year old book? No, stop, twenty years ago this syntax was already made redundant, so it must be even older ;-) Use explicit joins instead (JOIN ... ON ...)
As to your subqueries: You are comparing with id without any qualifier, so the DBMS will take this to be guild_membership.id or players_online resp. players.id, where you really want it to be guild.id. This should explain that you get unexpected values.
As to how the query is built: You could join to the aggregated player data instead. And use alias names that match the tables.
select
guilds.id,
guilds.name,
guilds.ownerid,
guilds.creationdata,
guilds.motd,
players.online,
players.toplevel,
players.lowlevel
from guilds
left join
(
select
gms.guild_id,
max(p.level) as toplevel,
min(p.level) as lowlevel,
sum((select count(*) from players_online po where po.player_id = p.id)) as online
from guild_membership gms
join players p on p.id = gms.player_id
group by gms.guild_id
) players on players.guild_id = guilds.id
where guilds.name = 'Wideswing Poleaxe';
You can change the left outer join (left join) to an inner join (join), if you don't need to see guilds without any player.
I think the problem is here: a.guild_id = id
The id being used is from players, not guilds, as it is still part of the sub-query.
You shouldn't need all those subqueries, JOINs are almost always faster and should usually be first technique tried.
Try this...
SELECT `id`, `name`, `ownerid`, `creationdata`, `motd`
, COUNT(po.player_id) AS online
, MAX(p.level) AS toplevel
, MIN(p.level) AS lowlevel
FROM `guilds` AS g
LEFT JOIN guild_membership AS gm ON g.id = gm.guild_id
LEFT JOIN players AS p ON gm.player_id = p.player_id
LEFT JOIN players_online AS po ON gm.player_id = po.player_id
WHERE g.`name` = 'Wideswing Poleaxe'
;
COUNT only counts non-null values; similarly MAX, MIN, and most other aggregate functions ignore null values (only returning null if only null values were processed).
You should consider modifying your query like
SELECT g.`id`,
g.`name`,
g.`ownerid`,
g.`creationdata`,
g.`motd`,
(SELECT Count(*)
FROM guild_membership a,
players_online b
WHERE a.player_id = b.player_id
AND a.guild_id = id) AS `online`,
(SELECT Max(b.level)
FROM players b join guild_membership a on a.player_id = b.id
AND a.guild_id = g.id) AS `toplevel`,
(SELECT Min(a.level)
FROM players a join
guild_membership b on a.id = b.player_id
AND b.guild_id = g.id) AS `lowlevel`
FROM `guilds` g
WHERE g.`name` = 'Wideswing Poleaxe'
LIMIT 1;

Order query result by latest updated date

i am working on two tables,
CREATE TABLE IF NOT EXISTS `users` (
`userid` INT NOT NULL AUTO_INCREMENT ,
`fname` VARCHAR(45) NOT NULL ,
`lname` VARCHAR(45) NOT NULL ,
`usernick` VARCHAR(45) NOT NULL ,
PRIMARY KEY (`userid`) ,
UNIQUE INDEX `usernick_UNIQUE` (`usernick` ASC) )
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `messages` (
`messageid` INT NOT NULL AUTO_INCREMENT ,
`sendid` INT NOT NULL ,
`recid` INT NOT NULL ,
`message` VARCHAR(1000) NOT NULL ,
`date` TIMESTAMP NULL ,
PRIMARY KEY (`messageid`) ,
INDEX `sender_fk_idx` (`sendid` ASC) ,
INDEX `reciever_fk_idx` (`recid` ASC) ,
CONSTRAINT `sender_fk`
FOREIGN KEY (`sendid` )
REFERENCES `users` (`userid` )
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `reciever_fk`
FOREIGN KEY (`recid` )
REFERENCES `users` (`userid` )
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;
and want a list of user on basis if their last sent message. Eg.
select U.fname,U.lname,U.usernick from messages as M natural join users as U where M.sendid = U.userid and M.recid={$_SESSION['user_id']} group by U.usernick ORDER BY M.date DESC
EXAMPLE:
name msg sent to "RON" on
Alpha 17 aug
Beta 18 aug
Alpha 19 aug
Gamma 20 aug
SO i want a list like,
gamma (last msg on 20)
alpha (last msg on 18)
beta (last msg on 19)
SEE LINK http://sqlfiddle.com/#!2/9dca2/2/0
You almost have it. You just need max(m.date) in the order by clause. Otherwise, an arbitrary date is chosen:
select U.fname,U.lname,U.usernick
from messages as M join
users as U
on M.sendid = U.userid
where M.recid={$_SESSION['user_id']}
group by U.usernick
ORDER BY max(M.date) DESC
I also fixed the join syntax. It is best to avoid natural joins -- that is, be explicit about the join condition. You do this anyway, so I switched the condition to an on clause and the natural join to a join.