PHP/MySQL Rank by multiple columns - php

I'm developing a game that scores players in 2 different ways, A, B.
In this game I have a ranking page that shows what position you are in ranking A, B and A+B as well as a list of players ordered by A+B.
What would be the most efficient way to retrieve your rank in these different scoring situations as well as the nearby users for rank A+B (to display on the list)?
I assume I would have to do a pass over every user at least once. Should I attempt this with multiple left joins and subselects and count users with score A/B/A+B greater than yours, or just query for the whole user+score list and calculate the ranks with a PHP function?
EXAMPLE:
UID | A | B
----------------
1 | 100 | 50
2 | 150 | 20
3 | 10 | 100
Assuming the user viewing the ranking is UID=2 we should see:
SCORE LIST (A+B):
2 - 170
1 - 150
3 - 110
You are #1 in Score A.
You are #3 in Score B.
You are #1 in Score A+B.

create table my_table
(UID int not null auto_increment primary key
,A int not null
,B int not null
);
insert into my_table values
(1,100,50),
(2,150,20),
(3,10,100);
SELECT uid
, FIND_IN_SET(a,a_score) a_rank
, FIND_IN_SET(b,b_score) b_rank
, FIND_IN_SET(a+b,ab_score)ab_rank
FROM my_table
, ( SELECT GROUP_CONCAT(DISTINCT a ORDER BY a DESC) a_score
, GROUP_CONCAT(DISTINCT b ORDER BY b DESC) b_score
, GROUP_CONCAT(DISTINCT a+b ORDER BY a+b DESC) ab_score
FROM my_table
) n
[WHERE uid=2];
+-----+--------+--------+---------+
| uid | a_rank | b_rank | ab_rank |
+-----+--------+--------+---------+
| 1 | 2 | 2 | 2 |
| 2 | 1 | 3 | 1 |
| 3 | 3 | 1 | 3 |
+-----+--------+--------+---------+
http://sqlfiddle.com/#!9/97da6/13

Related

Get rows above and below (neighbouring rows) a certain row, based on two criteria SQL

Say I have a table like so:
+---+-------+------+---------------------+
|id | level |score | timestamp |
+---+-------+------+---------------------+
| 4 | 1 | 70 | 2021-01-14 21:50:38 |
| 3 | 1 | 90 | 2021-01-12 15:38:0 |
| 1 | 1 | 20 | 2021-01-14 13:10:12 |
| 5 | 1 | 50 | 2021-01-13 12:32:11 |
| 7 | 1 | 50 | 2021-01-14 17:15:20 |
| 8 | 1 | 55 | 2021-01-14 09:20:00 |
| 10| 2 | 99 | 2021-01-15 10:50:38 |
| 2 | 1 | 45 | 2021-01-15 10:50:38 |
+---+-------+------+---------------------+
What I want to do is show 5 of these rows in a table (in html), with a certain row (e.g. where id=5) in the middle and have the two rows above and below it (in the correct order). Also where level=1. This will be like a score board but only showing the user's score with the two above and two below.
So because scores can be the same, the timestamp column will also need to be used - so if two scores are equal, then the first person to get the score is shown above the other person.
E.g. say the user is id=5, I want to show
+---+-------+------+---------------------+
|id | level |score | timestamp |
+---+-------+------+---------------------+
| 4 | 1 | 70 | 2021-01-14 21:50:38 |
| 8 | 1 | 55 | 2021-01-14 09:20:00 |
| 5 | 1 | 50 | 2021-01-13 12:32:11 |
| 7 | 1 | 50 | 2021-01-14 17:15:20 |
| 2 | 1 | 45 | 2021-01-15 10:50:38 |
| 1 | 1 | 20 | 2021-01-14 13:10:12 |
+---+-------+------+---------------------+
Note that id=7 is below id=5
I am wondering does anyone know a way of doing this?
I have tried this below but it is not outputting what I need (it is outputting where level_id=2 and id=5, and the other rows are not in order)
((SELECT b.* FROM table a JOIN table b ON b.score > a.score OR (b.score = a.score AND b.timestamp < a.timestamp)
WHERE a.level_id = 1 AND a.id = 5 ORDER BY score ASC, timestamp DESC LIMIT 3)
UNION ALL
(SELECT b.* FROM table a JOIN table b ON b.score < a.score OR (b.score = a.score AND b.timestamp > a.timestamp)
WHERE a.level_id = 1 AND a.id = 5 ORDER BY score DESC, timestamp ASC LIMIT 2))
order by score
If it is easier to output all rows in the table, say where level = 1, so it is a full score board.. and then do the getting a certain row and two above and below it using PHP I'd also like to know please :) ! (possibly thinking this may keep the SQL simpler)?
You can use cte and inner join as follows:
With cte as
(select t.*,
dense_rank() over (order by score) as dr
from your_table t)
Select c.*
From cte c join cte cu on c.dr between cu.dr - 2 and cu.dr + 2
Where cu.id = 5
Ordwr by c.dr, c.timestamp
I would suggest window functions:
select t.*
from (select t.*,
max(case when id = 7 then score_rank end) over () as id_rank
from (select t.*,
dense_rank() over (order by score) as score_rank
from t
where level = 1
) t
) t
where score_rank between id_rank - 2 and id_rank + 2;
Note: This returns 5 distinct score values, which may result in more rows depending on duplicates.
Here is a db<>fiddle.
EDIT:
If you want exactly 5 rows using the timestamp, then:
select t.*
from (select t.*,
max(case when id = 7 then score_rank end) over () as id_rank
from (select t.*,
dense_rank() over (order by score, timestamp) as score_rank
from t
where level = 1
) t
) t
where score_rank between id_rank - 2 and id_rank + 2
order by score;
Note: This still treats equivalent timestamps as the same, but they seem to be unique in your data.

Delete all rows, except last 10 for each client that has related row(s) in the table in one query?

So my situation is this:
Clients table - has client data etc, not too exciting
Recently Viewed table - table that has recently viewed things for the client(s), And has structure like this:
( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
, client_id INT NOT NULL
, cookie_user_id INT NOT NULL
, hotel_id INT NOT NULL
, added DATETIME NOT NULL
, comment TEXT
,status TINYINT NOT NULL DEFAULE 1
);
I currently have a partially working SQL to delete rows in the recently viewed table that right now globally limits number of latest remaining undeleted records in it. This is how it looks like now
DELETE FROM `recently_viewed`
WHERE `recently_viewed`.`id` NOT IN (
SELECT id
FROM (
SELECT `id`
FROM `recently_viewed`
WHERE `client_id` IN (SELECT `id` FROM `klijenti`)
ORDER BY `id` DESC
LIMIT 5
) x
)
AND `client_id` <> 0
"LIMIT 5" part should limit to the N records to remain in recently viewed table on a "per client" basis. Right now it limits records in recently viewed table to 5 no matter how many clients actually have records there. So if I have 10 clients, each of them has 8 records in that table, I would like this query to delete as many oldest records as needed to leave only 5 newest recently viewed items for EACH client and not just leave 5 overall in the table, ignoring the "per each client" logic. Hope that makes sense to you :)
Currently, this query would be ok if I would first fetch all clients in the app and then do a foreach loop to make another query for each client and leave 5 of his latest recently viewed items, but would like to do this in one SQL query instead.
How could this be done ? Thank you
You can do it like this:
DELETE FROM `recently_viewed`
WHERE `recently_viewed`.`id` NOT IN (
SELECT id
FROM (
SELECT t.`id`,count(*) as rnk
FROM `recently_viewed` t
INNER JOIN `recently_viewed` s
ON(t.`client_id` = s.`client_id` and t.added <= s.added)
WHERE t.`client_id` IN (SELECT `id` FROM `klijenti`)
GROUP BY t.`ID`
) x
WHERE rnk <= 5
)
AND `client_id` <> 0
You can use vartiables to in order to count the 5 more recent records per client_id:
DELETE FROM `recently_viewed`
WHERE `recently_viewed`.`id` NOT IN
(
SELECT id
FROM (
SELECT `id`,
#rn := IF(#cid = `client_id`, #rn + 1,
IF(#cid := `client_id`, 1, 1)) AS rn
FROM `recently_viewed`
CROSS JOIN (SELECT #rn := 0, #cid := 0) AS vars
WHERE `client_id` IN (SELECT `id` FROM `klijenti`)
ORDER BY `client_id`, `id` DESC) x
WHERE x.rn <= 5
)
Giorgos's answer is faster, but here's another method...
Consider the following...
SELECT * FROM my_table ORDER BY x,i;
+---+------+
| i | x |
+---+------+
| 2 | A |
| 3 | A |
| 6 | A |
| 8 | A |
| 1 | B |
| 5 | B |
| 4 | C |
| 7 | C |
| 9 | C |
+---+------+
Let's say we want to select the two latest i for each x. Here's one way to do that...
SELECT m.* FROM my_table m JOIN my_table n ON n.x = m.x AND n.i >= m.i GROUP BY m.i HAVING COUNT(*) <= 2;
+---+------+
| i | x |
+---+------+
| 1 | B |
| 5 | B |
| 6 | A |
| 7 | C |
| 8 | A |
| 9 | C |
+---+------+
The inverse of this set can be found as follows....
SELECT m.* FROM my_table m JOIN my_table n ON n.x = m.x AND n.i >= m.i GROUP BY m.i HAVING COUNT(*) > 2;
+---+------+
| i | x |
+---+------+
| 2 | A |
| 3 | A |
| 4 | C |
+---+------+
...which in turn can be incorporated in a DELETE. Here's a crude method for doing that...
DELETE a FROM my_table a
JOIN
( SELECT m.* FROM my_table m JOIN my_table n ON n.x = m.x AND n.i >= m.i GROUP BY m.i HAVING COUNT(*) > 2 ) b
ON b.i = a.i;
Query OK, 3 rows affected (0.03 sec)
SELECT * FROM my_table ORDER BY x,i;
+---+------+
| i | x |
+---+------+
| 6 | A |
| 8 | A |
| 1 | B |
| 5 | B |
| 7 | C |
| 9 | C |
+---+------+
As I say, if performance is critical, then look at a solution along the lines that Giorgos has provided.

MySQL - Grouping multiple rows where criteria is the same

I have a table of movie ratings that contains millions of rows containing userid's, movieid's and ratings.
| userId | movieId | rating |
------------------------------
| 1 | 213 | 5 |
| 1 | 245 | 4 |
| 2 | 213 | 4 |
| 2 | 245 | 4 |
| 3 | 657 | 5 |
| 3 | 245 | 5 |
I'm trying to figure out a way of grouping together userId's that contain matching sets of movieId's. Ideally I want the query to only find matches if they have at least 5 movieId's in common and if the rating is above 4, but I've simplified it for this example.
In the instance above, userId 1 and 2 would be the only users that match as they both contain the same movieIds. I need a statement that would essentially replicate this. Thanks in advance for any help.
You can perform a self-join on matching movies, filter out records with uninteresting ratings, group by user-pairs and then filter the resulting groups for only those that have at least the requisite number of matching records:
SELECT a.userId, b.userId
FROM myTable a JOIN myTable b USING (movieId)
WHERE a.userId < b.userId
AND a.rating > 4
AND b.rating > 4
GROUP BY a.userId, b.userId
HAVING COUNT(*) >= 5
select movieId, rating
from tablename
group by movieId
having count(userId) > 1 and rating > 4;
this gives me movieId 245 and rating 5, which should be correct according to your provided example data, have more than 1 userId and a rating greater than 4.

mysql - Max occurences of a given value in a table

I have a table like this
+-------+-------+-------+-------+
| id | cid | grade |g_point|
+-------+-------+-------+-------+
| 1 | 10 | A+ | 1 |
| 2 | 10 | A+ | 1 |
| 3 | 10 | B | 3 |
| 4 | 11 | A | 2 |
| 5 | 11 | A+ | 1 |
| 6 | 12 | B | 3 |
the column g_point is the values associated to each grade. forexample A+ grade considers highest so I assign the value of A+ is one(highest starts from 1 to 10) and so on. These g_point values are constant. Now what I want to do is I want to show the maximum grade against each course and also if somehow there are only two entries of different grades I want to compare it with the g_point and choose whose value is lower because lower integer value means higher grade. the result should be like this and also sorted from top grade to lower.
+-------+-------+
| cid | grade |
+-------+-------+
| 10 | A+ |
| 11 | A+ |
| 12 | B |
I have tried this query
SELECT coursecodeID AS cid, (SELECT grade
FROM feedback
WHERE coursecodeID = cid
GROUP BY grade
ORDER BY COUNT(*) DESC LIMIT 0,1) AS g
FROM feedback
GROUP BY coursecodeID
but in this query I don't know how can I compare it with g_point value and also the courses is not showing in order(from highest grade to lowest).
NOTE: I want to choose the grade having the maximum number of occurrences per course id. For example here in this table course id 10 has 2 A+ grade so we'll consider A+ and if clash happens like one is A+ and the other is B+, then we'll have to compare it with the g_point
This works, but needs the 'g_point' to alse be returned.
SELECT cid,grade,MIN(g_point)
FROM grades
GROUP BY cid
This is more reliable, as it generates the Grade in the sub-query, and then appends it to the main table.
SELECT cid, (
SELECT grade
FROM grades g2
WHERE g2.cid = g1.cid
ORDER BY g_point
LIMIT 1
) AS grade
FROM grades g1
GROUP BY cid
You can use the following query:
SELECT DISTINCT m1.cid, m1.grade
FROM mytable AS m1
INNER JOIN (
SELECT cid, MIN(g_point) AS maxGrade
FROM mytable
GROUP BY cid ) m2
ON m1.cid = m2.cid AND m1.g_point = m2.maxGrade
The derived table contains the minimum g_point per cid. If you join it back to the original table, then you can get the maximum grade per cid.
Demo here
EDIT:
You can alternatively use a correlated sub-query:
SELECT cid, (SELECT grade
FROM mytable AS m2
WHERE m2.cid = m1.cid
ORDER BY g_point LIMIT 1) AS maxGrade
FROM mytable AS m1
GROUP BY cid
Demo here
EDIT2:
It looks like you want to get the grade having the maximum number of occurrences per cid. In case there are more than one grades sharing this maximum number, then fetch the grade with the lowest g_point.
You can do it using variables:
SELECT cid, grade
FROM (
SELECT cid, grade,
#row_number := IF (#cid <> cid,
IF (#cid := cid, 1, 1),
IF (#cid := cid, #row_number+1, #row_number+1)) AS rn
FROM (
SELECT cid, grade,
COUNT(*) AS cnt,
(SELECT g_point
FROM mytable AS m2
WHERE m1.grade = m2.grade
LIMIT 1) AS g_point
FROM mytable AS m1
GROUP BY cid, grade
) t
CROSS JOIN (SELECT #row_number:=-1, #cid:=-1) AS vars
ORDER BY cid, cnt DESC, g_point
) s
WHERE rn = 1
Demo here
Something to think about...
SELECT * FROM ints;
+---+
| i |
+---+
| 0 |
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
+---+
SELECT i
, CONCAT(CHAR((i/2)+64),IF(MOD(i,2)=1,'+',''))n
FROM ints
WHERE i > 0;
+---+------+
| i | n |
+---+------+
| 1 | A+ |
| 2 | A |
| 3 | B+ |
| 4 | B |
| 5 | C+ |
| 6 | C |
| 7 | D+ |
| 8 | D |
| 9 | E+ |
+---+------+

Mysql Select query - not able to limit records

I have a ranking table in which i have all players with their ranks.
id | rank | playername | is_available | ranking_name | ranking_id
-------------------------------------------------------------------
1 | 1 | testname1 | 1 | australia open | 1
2 | 2 | testname2 | 1 | australia open | 1
3 | 3 | testname3 | 0 | australia open | 1
4 | 4 | testname4 | 1 | australia open | 1
5 | 1 | testname5 | 1 | japan open | 2
This table is huge and for each ranking_id there can be more than 500 players.
Now every player can challenge a match to upper players x% above him. This x is set by superadmin. If x = 10, player "testname4" can challenge (num of players in a ranking_id * 10/100) = 4*10/100=0.4 round to 1. so testname4 can challenge one one player above him. But his above player "testname3" is not available. So he should be given next available player. I want the output like
//testname4 can challenge below players
id | rank | playername | status |
--------------------------------------
2 | 2 | testname2 | available |
3 | 3 | testname3 | not available |
What i did:
//testname4 wants to challenge. So i know his rank and other information
$selectSql = mysql_query("SELECT * from rankingTable where ranking_id = 1 AND rank < 4");
This is giving me "testname1" record as well. how can i limit this ? And most biggest problem is i need to show the ranks in ascending order. Its not easy using order by here.
First determine the users to be challenged by performing a SELECT ... WHERE is_available = 1 ORDER BY rank DESC LIMIT ?, then take MIN(rank) to find the highest ranked such user, then use that information to filter the table to users ranked between oneself and that user:
SELECT rankingTable.*
FROM rankingTable, (
SELECT MIN(a_rank) AS lower, b_rank - 1 AS upper
FROM (
SELECT a.rank AS a_rank, b.rank AS b_rank
FROM rankingTable AS a JOIN rankingTable AS b USING (ranking_id)
WHERE ranking_id = ?
AND b.playername = ?
AND a.rank < b.rank
AND a.is_available = 1
ORDER BY a.rank DESC
LIMIT ?
) AS ranks
) AS limits
WHERE rank BETWEEN limits.lower AND limits.upper
ORDER BY rank DESC
See it (including determination of the number of users to be selected) on sqlfiddle.
Try just adding LIMIT 1 or LIMIT 0, 1 (beginning, how many records) to the end of your query

Categories