views:

42

answers:

3

I have a table in which users store scores and other information about said score (for example notes on score, or time taken etc). I want a mysql query that finds each users personal best score and it's associated notes and time etc.

What I have tried to use is something like this:

SELECT *, MAX(score) FROM table GROUP BY (user)

The problem with this is that whilst you can extra the users personal best from that query [MAX(score)], the returned notes and times etc are not associated with the maximum score, but a different score (specifically the one contained in *). Is there a way I can write a query that selects what I want? Or will I have to do it manually in PhP?

+1  A: 
SELECT *
FROM table t
ORDER BY t.score DESC
GROUP BY t.user
LIMIT 1

Side note: It is better to specify the fields than use SELECT *

Coronatus
I don't think this is what he is looking for. He wants a row for each user.
Mark Byers
A: 

You can join with a sub query, as in the following example:

SELECT   t.*,
         sub_t.max_score
FROM     table t
JOIN     (SELECT   MAX(score) as max_score,
                   user
          FROM     table
          GROUP BY user) sub_t ON (sub_t.user = t.user AND
                                   sub_t.max_score = t.score);

The above query can be explained as follows. It starts with:

SELECT t.* FROM table t;

... This by itself will obviously list all the contents of the table. The goal is to keep only the rows that represent a maximum score of a particular user. Therefore if we had the data below:

+------------------------+
| user | score |  notes  | 
+------+-------+---------+
|    1 |    10 |  note a |
|    1 |    15 |  note b |    
|    1 |    20 |  note c |
|    2 |     8 |  note d |
|    2 |    12 |  note e |
|    2 |     5 |  note f |
+------+-------+---------+

...We would have wanted to keep just the "note c" and "note e" rows.

To find the rows that we want to keep, we can simply use:

SELECT MAX(score), user FROM table GROUP BY user;

Note that we cannot get the notes attribute from the above query, because as you had already noticed, you would not get the expected results for fields not aggregated with an aggregate function, like MAX() or not part of the GROUP BY clause. For further reading on this topic, you may want to check:

Now we only need to keep the rows from the first query that match the second query. We can do this with an INNER JOIN:

...
JOIN     (SELECT   MAX(score) as max_score,
                   user
          FROM     table
          GROUP BY user) sub_t ON (sub_t.user = t.user AND
                                   sub_t.max_score = t.score);

The sub query is given the name sub_t. It is the set of all the users with the personal best score. The ON clause of the JOIN applies the restriction to the relevant fields. Remember that we only want to keep rows that are part of this subquery.

Daniel Vassallo
Thankyou very much, this does work. :)Is there any chance you could either explain what is going on with this sub query business, or point me towards a tutorial or something? I've never seen this before.If not, thankyou and I will seek to figure this out in the morning!
VolatileStorm
Glad it helped. Let me revise the answer with some points...
Daniel Vassallo
This works, but note that it can return multiple results if the same player scores the same score more than once.
Mark Byers
@Mark: You're right. However if this is not what the user requires, and there is some logic on which one of the personal best scores to choose (for example, choosing the latest in case of a tie), there are be a couple of solutions for this as well.
Daniel Vassallo
+1  A: 

I'm assuming that you only want one result per player, even if they have scored the same maximum score more than once. I am also assuming that you want each player's first time that they got their personal best in the case that there are repeats.

There's a few ways of doing this. Here's a way that is MySQL specific:

SELECT user, scoredate, score, notes FROM (
    SELECT *, @prev <> user AS is_best, @prev := user
    FROM table1, (SELECT @prev := -1) AS vars
    ORDER BY user, score DESC, scoredate
) AS T1
WHERE is_best

Here's a more general way that uses ordinary SQL:

SELECT T3.* FROM table1 AS T3
JOIN (
    SELECT T1.user, T1.score, MIN(scoredate) AS scoredate
    FROM table1 AS T1
    JOIN (SELECT user, MAX(score) AS score FROM table1 GROUP BY user) AS T2
    ON T1.user = T2.user AND T1.score = T2.score
    GROUP BY T1.user
) AS T4
ON T3.user = T4.user AND T3.score = T4.score AND T3.scoredate = T4.scoredate

Result:

1, '2010-01-01 17:00:00', 50, 'Much better'
2, '2010-01-01 14:00:00', 100, 'Perfect score'

Test data I used to test this:

CREATE TABLE table1 (user INT NOT NULL, scoredate DATETIME NOT NULL, score INT NOT NULL, notes NVARCHAR(100) NOT NULL);
INSERT INTO table1 (user, scoredate, score, notes) VALUES
(1, '2010-01-01 12:00:00', 10, 'First attempt'),
(1, '2010-01-01 17:00:00', 50, 'Much better'),
(1, '2010-01-01 22:00:00', 30, 'Time for bed'),
(2, '2010-01-01 14:00:00', 100, 'Perfect score'),
(2, '2010-01-01 16:00:00', 100, 'This is too easy');
Mark Byers