2

We have a game where players run through a map, and are scored for it. We need to find out which attempts had the high score on each map, and set their isHighScore flag to true. Only one attempt can have the high score for each map - if two attempts have the same score, only the attempt which came first chronologically should have its isHighScore flag set.

The SQL we currently have looks like this:

UPDATE attempts AS A1
SET isHighScore = 1
WHERE A1.ID =
(
    SELECT A2.ID
    FROM (SELECT ID, score, mapID, `date` FROM attempts) AS A2
    WHERE A2.mapID = A1.mapID
    ORDER BY A2.score DESC, A2.date ASC
    LIMIT 1
)

(The (SELECT ... FROM attempts) subquery is due to this issue)

The above takes longer than the timeout to run on a table with around 75k entries (and yes, there is an index on mapID, score, date). I assumed it was because the innermost query copies the entire attempts table to a temp-table, but moving the WHERE mapID = A1.mapID conditional into that query gives a syntax error, so I'm not sure what to do about that. Also, the inner-query runs once for every row - maybe there's a way to fix that?

Does anyone know of a better way to write this query in MySQL?

Community
  • 1
  • 1
BlueRaja - Danny Pflughoeft
  • 84,206
  • 33
  • 197
  • 283

4 Answers4

1

You could try using an update-join.

UPDATE attempts
RIGHT JOIN
(
    SELECT id FROM attempts a1
    WHERE NOT EXISTS
    (
        SELECT 0 FROM attempts a2
        WHERE a2.mapID = a1.mapID AND 
            (a2.score > a1.score OR (a2.score = a1.score AND a2.date < a1.date))
    )
) tmp ON tmp.id = attempts.id
SET attempts.isHighestScore = 1;

If you put an index on the mapID column and score column this should be fairly fast.

BlueRaja - Danny Pflughoeft
  • 84,206
  • 33
  • 197
  • 283
fuzic
  • 2,492
  • 16
  • 22
  • This will not work due to the following: *"if two attempts have the same score, only the attempt which came first chronologically should have its `isHighScore` flag set"* – BlueRaja - Danny Pflughoeft Mar 13 '13 at 19:00
  • Yes, very sorry I glossed over that key part. I revised my answer. – fuzic Mar 13 '13 at 19:20
  • Not sure what you're trying to get at, but this new answer won't even compile *(`SELECT * FROM ... GROUP BY mapID`)* – BlueRaja - Danny Pflughoeft Mar 13 '13 at 19:30
  • I don't understand, I set up your environment locally and it runs for me. Does it throw an error? – fuzic Mar 13 '13 at 19:33
  • 1
    Ah, I didn't actually run it, I just assumed it wouldn't compile since, according to the standard, you cannot select columns that are not in the `GROUP BY`. But apparently [MySQL allows it anyways](http://goo.gl/tZblK). However, this solution *still* won't work; you're assuming the first row will always be selected, but according to that link: *"The server is free to choose any value from each group, so unless they are the same, the values chosen are indeterminate. Furthermore, the selection of values from each group cannot be influenced by adding an ORDER BY clause."* Close, but no cigar. – BlueRaja - Danny Pflughoeft Mar 13 '13 at 19:57
  • Interesting. I hadn't realized group by worked like that. This is the fastest I could find. It is @matts query to find the best rows, but updated with a right join instead of using `in`. – fuzic Mar 13 '13 at 20:41
  • With the exception of the MySQL subquery issue mentioned in the question, this looks like it should work. I'll try both your and @matts' solutions tonight, and see which is faster. – BlueRaja - Danny Pflughoeft Mar 13 '13 at 20:44
0

You could try using a correlated subquery to only match rows for the same map where there is no other row with a greater score, or if there is one with an equal score, no other row with an older date:

UPDATE attempts
   SET isHighScore = 1
 WHERE ID IN (
           SELECT ID FROM (
               SELECT ID
                 FROM attempts a1
                WHERE NOT EXISTS (
                          SELECT 0
                            FROM attempts a2
                           WHERE a2.mapID = a1.mapID
                             AND (a2.score > a1.score OR
                                 (a2.score = a1.score AND a2.date < a1.date))
                      )
           ) a0
       )

A SQLFiddle.

Don't forget to set the old highscores to 0.

matts
  • 6,738
  • 1
  • 33
  • 50
  • I'll give this a shot, but given that the copy-to-a-temp-table and the runs-once-for-every-row problems still exist, I doubt this will be any faster. – BlueRaja - Danny Pflughoeft Mar 13 '13 at 19:02
  • According to the execution plan, your original update doesn't use the `mapId`-`score`-`date` index. – matts Mar 13 '13 at 19:09
  • Yours does not either *(you are missing the innermost query, the `SELECT 0 FROM attempts a2` is actually not valid in MySQL; see the link in my question)* – BlueRaja - Danny Pflughoeft Mar 13 '13 at 19:13
  • @BlueRaja-DannyPflughoeft I tried again! This one gets all the IDs for the highscore attempts and puts that in the derived table. I made sure the update actually works in my Fiddle this time, and the execution plan indicates it's using the index, so hopefully it's faster. I don't have 75k rows to test with though... – matts Mar 13 '13 at 19:53
  • Looks like this one actually uses the index; I'll give it a shot tonight *(FYI I did not downvote you)* – BlueRaja - Danny Pflughoeft Mar 13 '13 at 20:07
0

This works but I'm not sure about the performance, give a try. This is fiddle.

UPDATE attempts SET isHighScore = 1 WHERE ID IN
(
  SELECT ID FROM
  (
    SELECT a5.* FROM attempts a5
    INNER JOIN
    (
      SELECT mapID, max(score) as max_score, min(date) as min_date FROM
      (
         SELECT a1.id, a1.mapID, a1.score, a1.date 
         FROM attempts a1 
         INNER JOIN
        (
          SELECT mapID, max(score) as max_score 
          FROM attempts
          GROUP BY mapID
         ) a2
         ON
            a1.mapID = a2.mapID and a1.score = a2.max_score
      ) a3
      GROUP BY mapID
    ) a4
    ON a5.mapID = a4.mapID and a5.score = a4.max_score and a5.date = a4.min_date
   )a6
);

Explanation:

1- inner most group by gives the max_score and mapID for each mapID

2- this is joined with the original table to find the ids and dates of those attempts but may contain more than one row per mapID when there is a tie on max_score

3- once again group by mapID to get the min(date)

4- once again join with original table to get the ids for update

jurgenreza
  • 5,856
  • 2
  • 25
  • 37
  • This will not work due to the following: *"if two attempts have the same score, only the attempt which came first chronologically should have its isHighScore flag set"* – BlueRaja - Danny Pflughoeft Mar 13 '13 at 20:09
  • @BlueRaja-DannyPflughoeft It does consider that, see the order by on the last line. also see fiddle, in the sample data i created two high scores of 100 and only the one that is first is set to 1. – jurgenreza Mar 13 '13 at 20:15
  • @BlueRaja-DannyPflughoeft I also explained it in part 3 of Explanation. – jurgenreza Mar 13 '13 at 20:16
  • Sorry, but that is just a fluke; this query does not actually work. See my large comment to [this](http://stackoverflow.com/a/15394086/238419) similar answer for an explanation why. – BlueRaja - Danny Pflughoeft Mar 13 '13 at 20:17
  • @BlueRaja-DannyPflughoeft I understand your point about group by fields in mysql. I updated the answer and solved that problem. also included a fiddle and it is working fine. it must be fast. give it a try. and plz remove the downvote. – jurgenreza Mar 13 '13 at 21:02
-1

For anything like this, I always recommend a Temp table. In this case, the temp table could be having columns - (id, highest_score, map) -

create temporary table temp1
select min(id) as id, highest_score, map 
from attempts 
where <<attempts of today>>
group by map;

Now directly join attempts with the temp table and flag them.

update temp1 as t inner join attempts as a on a.id = t.id
set a.isHighestScore = 1;

Drop temporary table temp1;

(If replication is enabled, either use permanent tables or a transaction for being on safer side)

georgecj11
  • 1,600
  • 15
  • 22
  • -1 I am already using a temp-table implicitly; this answer is completely useless. – BlueRaja - Danny Pflughoeft Mar 13 '13 at 20:02
  • @BlueRaja-DannyPflughoeft However, if written correctly, this explicit temporary table will be much smaller (only one row per map instead of the entire `attempts` table), and the index will be used when populating it, so it should be faster. – matts Mar 13 '13 at 21:27