SQL command to keep top 5 high scores only - mysql

I have a mySQL database with e.g. the following table,
world level player_id score
-----------------------------------------
1 1 1 100
1 1 2 123
1 1 3 130
1 1 4 200
1 1 5 90
1 2 8 234
.
.
.
For each unique (world, level, player_id) triple I want to record the top five scores only, as new scores come in.
My thoughts are to do the following: First insert the new score record, e.g.
REPLACE INTO Highscores (world, level, player_id, score) VALUES (1, 1, 6, 500)
Then keep only those records of the same (world, level) with the top 5 scores, e.g.
DELETE FROM Highscores WHERE world=1 AND level=1 AND score < (SELECT min(score) FROM (SELECT score FROM Highscores ORDER BY score DESC LIMIT 5) AS Highscores);
But I was wondering if there was some other way to do this, perhaps with a single line of SQL, which might be more efficient?
On tied scores:
I assume that the last record in the table was added last, so in the case of ties, I want to keep the last record and remove the earlier record.
world level player_id score
-----------------------------------------
1 1 1 100
1 1 2 200
1 1 3 100
1 1 4 100
1 1 5 100
1 1 8 200
Here, e.g. the row with player_id=8 would be kept, but the row with player_id=2 would be removed. player_id=1, 3, 4, 5 would be kept too.
Update
In the end, by introducing an AUTO_INCREMENT unique tableid as primary key, I settled for the following approach:
REPLACE INTO Highscores (world, level, player_id, score) VALUES (1, 1, 6, 500)
DELETE FROM Highscores
WHERE world=1 AND level=1 AND tableid NOT IN
(SELECT tableid FROM (SELECT tableid FROM Highscores WHERE world=1 AND level=1
AND score >=
(SELECT min(score) FROM
(SELECT score FROM Highscores WHERE world=1 AND level=1 ORDER BY score DESC LIMIT 5)
AS d)) AS c)

This may not be quite what you are looking for, but it should work. If you create the database with 5 'dummy' values, such as a negative player_id and a 0 score, this query will select the lowest, and replace it with the new score:
UPDATE Highscores
INNER JOIN
(SELECT player_id
FROM Highscores
ORDER BY score ASC, player_id ASC LIMIT 1) rpid
ON Highscores.player_id = rpid.player_id
SET Highscores.player_id = 6, Highscores.score = 300
Just replace the last line with the values that you need. Here is the fiddle.
Note that this solution is based on the assumption that player_id is always set to increment, and only used once.

Related

Trouble using group by to get a max value across two tables

I have been trying to solve a problem for a very long time- days- and I am not making any progress. Basically, I have two tables, players and matches. Each player in players has a unique player_id, as well as a group_id that identifies which group he/she belongs to. Each match in matches has the player_ids of two players in it, first_player and second_player, who are always from the same group. first_score corresponds to the score that first_player scores and second_score corresponds to the score that second_player scores. A match is won by who ever scores more. Here are the two tables:
create table players (
player_id integer not null unique,
group_id integer not null
);
create table matches (
match_id integer not null unique,
first_player integer not null,
second_player integer not null,
first_score integer not null,
second_score integer not null
);
Now what I am trying to do is to get the players with the most wins from each group, their group ID as well as the number of wins. So, for example, if there are three groups, the result would be something like:
Group Player Wins
1 24 23
2 13 25
3 34 20
Here's what I have right now
SELECT p1.group_id AS Group, p1.player_id AS Player, COUNT(*) AS Wins
FROM players p1, matches m1
WHERE (m1.first_player = p1.player_id AND m1.first_score > m1.second_score)
OR (m1.second_player = p1.player_id AND m1.second_score > m1.first_score)
GROUP BY p1.group_id
HAVING COUNT(*) >= (
SELECT COUNT(*)
FROM players p2, matches m2
WHERE p2.group_id = p1.group_id AND
((m2.first_player = p2.player_id AND m2.first_score > m2.second_score)
OR (m2.second_player = p2.player_id AND m2.second_score > m2.first_score))
)
My idea is to only select players whose wins are greater than, or equal to, the wins of all other players in his group. There is some syntactic problem with my query. I think I am using GROUP BY incorrectly as well.
There is also the issue of a tie in the number of wins, where I should just get the player with the least player_id. But I haven't even gotten to that point yet. I would really appreciate your help, thanks!
EDIT 1
I have a few sample data that I am running my query against.
SELECT * FROM players gives me this:
Player_ID Group_ID
100 1
200 1
300 1
400 2
500 2
600 3
700 3
SELECT * FROM matches gives me this:
match_id first_player second_player first_score second_score
1 100 200 10 20
2 200 300 30 20
3 400 500 30 10
4 500 400 20 20
5 600 700 20 10
So, the query should return:
Group Player Wins
1 200 2
2 400 1
3 600 1
Running the query as is returns the following error:
ERROR: column "p1.player_id" must appear in the GROUP BY clause or be used in an aggregate function
Now I understand that I have to specify player_id in the GROUP BY clause if I want to use it in the SELECT (or HAVING) statement, but I do not wish to group by player ID, only by the group ID.
Even if I do add p1.player_id to GROUP BY in my outer query, I get...the correct answer actually. But I am a bit confused. Doesn't Group By aggregate the table according to that column? Logically speaking, I only want to group by p1.group_id.
Also, if I were to have multiple players in a group with the highest number of wins, how can I just keep the one with the lowest player_id?
Edit 2
If I change the matches table to such that for Group 1, there are two players with 1 win each, the query result omits Group 1 from the result altogether.
So, if my matches table is:
match_id first_player second_player first_score second_score
1 100 200 10 20
2 200 300 10* 20
3 400 500 30 10
4 500 400 20 20
5 600 700 20 10
I would expect the result to be
Group Player Wins
1 200 1
1 300 1
2 400 1
3 600 1
However, I get the following:
Group Player Wins
2 400 1
3 600 1
Note that the desired result is
Group Player Wins
1 200 1
2 400 1
3 600 1
Since I wish to only take the player with the least player_id in the case of a draw.
WITH first_players AS (
SELECT group_id,player_id,SUM(first_score) AS scores FROM players p LEFT JOIN matches m ON p.player_id=m.first_player GROUP BY group_id,player_id
),
second_players AS (
SELECT group_id,player_id,SUM(second_score) AS scores FROM players p LEFT JOIN matches m ON p.player_id=m.second_player GROUP BY group_id,player_id
),
all_players AS (
WITH al AS (
SELECT group_id, player_id, scores FROM first_players
UNION ALL
SELECT group_id, player_id, scores FROM second_players
)
SELECT group_id, player_id,COALESCE(SUM(scores),0) AS scores FROM al GROUP BY group_id, player_id
),
players_rank AS (
SELECT *,
ROW_NUMBER() OVER(PARTITION BY group_id ORDER BY scores DESC, player_id ASC) AS score_rank,
ROW_NUMBER() OVER(PARTITION BY scores ORDER BY player_id ASC) AS id_rank FROM all_players ORDER BY group_id
)
SELECT group_id, player_id AS winner_id FROM players_rank WHERE score_rank=1 AND id_rank=1
Results
group_id winner_id
1 45
2 20
3 40
Try it Out
try like below
with cte as
(
select p.Group_ID,t1.winplayer,t1.numberofwin
row_number()over(partition by p.Group_ID order by t1.numberofwin desc,t1.winplayer) rn from players p join
(
SELECT count(*) as numberofwin,
case when first_score >second_score then first_player
else second_player end as winplayer
FROM matches group by case when first_score >second_score then first_player
else second_player end
) t1 on p.Player_ID =t1.winplayer
) select * from cte where rn=1
It works when you add the player_id in the GROUP BY because you know each player plays only in one group. So you group by the player in a certain group. That is why, logically, you can add the player_id to the GROUP BY.

SQl:how to write a query for least times id is presnt in the table

I have a table called "UserPlay" which as values like this
th_id route_id
1 1
1 2
1 2
1 3
1 3
I just want least time rout_id is used
I have to get output as this
th_id route
1 1
If I understand correctly, you want the route_id with the lowest count:
select route_id, count(*)
from UserPlay u
group by route_id
order by count(*) asc
limit 1;
You can get the list of the_id on it by including group_concat(the_id).

mySQL - winning streak

Hi I am trying to figure out a way of finding the largest winning streak for each member in my table. When the table was built, this was never in the plans to happen so is why Im seeking help on how I can achieve this.
My structure is as follows:
id player_id opponant_id won loss timestamp
If it is a persons game, the player id is their id. If they are being challenged by someone, their id is the opponant id and the won loss (1 or 0) is in relation to the player_id.
I want to find the greatest winning streak for each user.
Anyone have any ideas on how to do this with the current table structure.
regards
EDIT
here is some test data, where id 3 is the player in question:
id player_id won loss timestamp
1 6 0 1 2012-03-14 13:31:00
13 3 0 1 2012-03-15 13:10:40
17 3 0 1 2012-03-15 13:29:56
19 4 0 1 2012-03-15 13:37:36
51 3 1 0 2012-03-16 13:20:05
53 6 0 1 2012-03-16 13:32:38
81 3 0 1 2012-03-21 13:14:49
89 4 1 0 2012-03-21 14:01:28
91 5 0 1 2012-03-22 13:14:20
Give this a try. Edited to take into account loss rows
SELECT
d.player_id,
MAX(d.winStreak) AS maxWinStreak
FROM (
SELECT
#cUser := 0,
#winStreak := 0
) v, (
SELECT
player_id,
won,
timestamp,
#winStreak := IF(won=1,IF(#cUser=player_id,#winStreak+1,1),0) AS winStreak,
#cUser := player_id
FROM (
(
-- Get results where player == player_id
SELECT
player_id,
won,
timestamp
FROM matchTable
) UNION (
-- Get results where player == opponent_id (loss=1 is good)
SELECT
opponent_id,
loss,
timestamp
FROM matchtable
)
) m
ORDER BY
player_id ASC,
timestamp ASC
) d
GROUP BY d.player_id
This works by selecting all win/loses and counting the win streak as it goes through. The subquery is then grouped by player_id and the max winStreak as calculated as it looped through is output per-player.
It seemed to work nicely against my test dataset anyway :)
To do this more efficiently I would restructure, i.e.
matches (
matchID,
winningPlayerID,
timeStamp
)
players (
playerID
-- player name etc
)
matchesHasPlayers (
matchID,
playerID
)
Which would lead to an inner query of
SELECT
matches.matchID,
matchesHasPlayers.playerID,
IF(matches.winningPlayerID=matchesHasPlayers.playerID,1,0) AS won
matches.timestamp
FROM matches
INNER JOIN matchesHasPlayers
ORDER BY matches.timestamp
resulting in
SELECT
d.player_id,
MAX(d.winStreak) AS maxWinStreak
FROM (
SELECT
#cUser := 0,
#winStreak := 0
) v, (
SELECT
matchesHasPlayers.playerID,
matches.timestamp,
#winStreak := IF(matches.winningPlayerID=matchesHasPlayers.playerID,IF(#cUser=matchesHasPlayers.playerID,#winStreak+1,1),0) AS winStreak,
#cUser := matchesHasPlayers.playerID
FROM matches
INNER JOIN matchesHasPlayers
ORDER BY
matchesHasPlayers.playerID ASC,
matches.timestamp ASC
) d
GROUP BY d.player_id
SELECT * FROM
(
SELECT player_id, won, loss, timestamp
FROM games
WHERE player_id = 123
UNION
SELECT opponant_id as player_id, loss as won, won as loss, timestamp
FROM games
WHERE opponant_id = 123
)
ORDER BY timestamp
That will give you all the results for one player ordered by timestamp. Then you would need to loop those results and count winning records or else concatenate them all into a string and then use string functions to find your highest 11111 set in that string. That code will vary depending on the language you want to use, but logically those are the two choices.

MySQL Query for Average Grade of last 2 attempts

I have a table:
quiz userid attempt grade
1 3 1 33
1 3 2 67
1 3 3 90
1 3 4 20
Now, I want the last two attempts i.e., 4 and 3 and I want average grade of these 2 grades i.e, 90 and 20
Could anyone help me?
Use ORDER and LIMIT to get the 2 last attempts and the AVG aggregation function :
SELECT AVG(grade) AS average FROM (
SELECT grade FROM table
WHERE userid = 3
ORDER BY attempt DESC LIMIT 2) AS t
If you want to list both test results separately, with the average in each row, then something like this maybe (otherwise you just need the subquery for the average of the two tests):
SELECT userid, attempt, grade,
( SELECT AVG(grade)
FROM table
ORDER BY attempt DESC LIMIT 0, 2 ) AS avg_grade
FROM table
ORDER BY attempt DESC LIMIT 0, 2;

MySQL Selecting wrong column value in Group By query

Here's a real noobish MySQL query problem I'm having.
I have a high score table in a game I'm writing. The high score DB records a name, level, and score achieved. There are many near duplicates in the db. For example:
Name | Level | Score | Timestamp (key)
Bob 2 41 | 1234567.890
Bob 3 15 | 1234568.890
Bob 3 20 | 1234569.890
Joe 2 40 | 1234561.890
Bob 3 21 | 1234562.890
Bob 3 21 | 1234563.890
I want to return a "highest level achieved" high score list, with an output similar to:
Name | Level | Score
Bob 3 21
Joe 2 40
The SQL Query I currently use is:
SELECT *, MAX(level) as level
FROM highscores
GROUP BY name
ORDER BY level DESC, score DESC
LIMIT 5
However this doesn't quite work. The "Score" field output always seems to be randomly pulled from the group, instead of taking the corresponding score for the highest level achieved. Eg:
Name | Level | Score
Bob 3 41
Joe 2 40
Bob never got 41 points on level 3! How can I fix this?
You'll need to use a subquery to pull the score out.
select distinct
name,
max(level) as level,
(select max(score) from highscores h2
where h2.name = h1.name and h2.level = h1.level) as score
from highscores h1
group by name
order by level desc, score desc
Cheers,
Eric
It irks me that I didn't take the time to explain why this is the case when I posted the answer, so here goes:
When you pull back everything (*), and then the max level, what you'll get is each record sequentially, plus a column with the max level on it. Note that you're not grouping by score (which would have given you Bob 2 41, and Bob 3 21--two records for our friend Bob).
So, how the heck do we fix this? You need to do a subquery to additionally filter your results, which is what that (select max(score)...) is. Now, for each row that reads Bob, you will get his max level (3), and his max score at that level (21). But, this still gives us however many rows Bob has (e.g.-if he has 5 rows, you'll get 5 rows of Bob 3 21). To limit this to only the top score, we need to use a DISTINCT clause in the select statement to only return unique rows.
UPDATE: Correct SQL (can't comment on le dorfier's post):
SELECT h1.Name, h1.Level, MAX(h1.Score)
FROM highscores h1
LEFT OUTER JOIN highscores h2 ON h1.name = h2.name AND h1.level < h2.level
LEFT OUTER JOIN highscores h3 ON h1.name = h3.name AND h2.level = h3.level AND h1.score < h3.score
WHERE h2.Name IS NULL AND h3.Name IS NULL
GROUP BY h1.Name, h1.Level
This is efficient.
SELECT h1.Name, h1.Level, h1.Score
FROM highscores h1
LEFT JOIN highscores h2 ON h1.name = h2.name AND h1.level < h2.level
LEFT JOIN highscores h3 ON h1.name = h3.name AND h1.level = h3.level AND h1.score < h3.score
WHERE h2.id IS NULL AND h3.id IS NULL
You're looking for the level/score for which there is no higher level for that user, and no higher score that that level.
Interesting problem. Here's another solution:
SELECT hs.name, hs.level, MAX(score) AS score
FROM highscores hs
INNER JOIN (
SELECT name, MAX(level) AS level FROM highscores GROUP BY name
) hl ON hl.name = hs.name AND hl.level = hs.level
GROUP BY hs.name, hs.level;
Personally, I find this the easiest to understand, and my hunch is that it will be relatively efficient for the database to execute.
I like the above query best, but just for kicks... I find the following one amusing in a kludgey sort of way. Assuming score can't exceed 99999...
SELECT name, level, score
FROM highscores hs
INNER JOIN (
SELECT name, MAX(level * 100000 + score) AS hfactor
FROM highscores GROUP BY name
) hf ON hf.hfactor = hs.level * 100000 + hs.score AND hf.name = hs.name;