Ranking position without duplicates - mysql

I have an issue with a query,
this is my old question that was previously solved
mysql get ranking position grouped with joined table
The problem is that when two players have the same score, the query returns the same ranking position, like 1-1-2-3 ecc. How can I fix this?
In the player's table there are also player_tickets (that is the number of game played) and player_date that is a timestamp.
I thought to get my ranking on base of player_score first, player_tickets then, and finally player_date
This is my older query
SELECT *,
(SELECT 1 + Count(*)
FROM players p2
WHERE p2.`player_game` = p.`player_game`
AND p2.player_score > p.player_score
AND p2.player_status = 0) AS ranking
FROM players p
ORDER BY `player_game`,
player_score DESC

You can simply add more columns to the order by of the window function:
rank() over (
partition by player_game_id
order by player_score desc, player_tickets desc, player_date
) as rank
If you really want to avoid having the same rank twice, you can also use row_number(), which guarantees this - when there are ties, row_number() affects distinct numbers (whose order is hence undefined).

Just add the ranking criteria to your WHERE clause:
SELECT *,
(
SELECT 1 + COUNT(*)
FROM players p2
WHERE p2.player_game = p.player_game
AND
(
(p2.player_score > p.player_score) OR
(p2.player_score = p.player_score AND p2.player_tickets > p.player_tickets) OR
(p2.player_score = p.player_score AND p2.player_tickets = p.player_tickets AND p2.player_date > p.player_date) OR
(p2.player_score = p.player_score AND p2.player_tickets = p.player_tickets AND p2.player_date = p.player_date AND p2.player_id > p.player_id)
)
) AS ranking
FROM players p
ORDER BY player_game, player_score DESC;

You can add additional comparisons in the subquery. Or more easily, use variables:
select p.*,
#rn := if(#g = player_game, #rn + 1,
if(#g := player_game, 1, 1)
) as ranking
from (select p.*
from players p
order by player_game, player_score desc, player_tickets desc, player_date desc
) p cross join
(select #rn := 0, #g := 0) as seqnum;
In newer versions, you would just use row_number() if you don't want ties.

Related

SELECT Where ID in (List of IDs) and limit records of each IDs in MySQL

I have met a situation that I have a list of IDs of a Store table and need to fetch the latest 10 files from each store.
SELECT *
FROM tblFiles
WHERE storeId in (IDs)
ORDER BY createdDate DESC
LIMIT 10
But, this limits the whole results. I found an answer to a similar SO question. But, the answer recommends using loop for each ID. This results in multiple DB hit.
Another option is to fetch all records and group them in the code. But, this will be heavy if there are large no.of records.
It'll be nice if it can be handled at the query level. Any help will be appreciated.
NB: The tables used here are dummy ones.
Pre-MySQL 8.0, the simplest method is probably variables:
select f.*
from (select f.*,
(#rn := if(#s = storeId, #rn + 1,
if(#s := storeId, 1, 1)
)
) as rn
from (select f.*
from tblfiles f
where storeId in (IDs)
order by storeId, createdDate desc
) f cross join
(select #s := 0, #rn := 0) params
) f
where rn <= 10;
In MySQL 8+ or MariaDB 10.3+, you would simply use window functions:
select f.*
from (select f.*,
row_number() over (partition by storeid order by createdDate desc) as seqnum
from tblfiles f
) f
where seqnum <= 10;
In older versions of MySQL and MariaDB, the innermost subquery may not be needed.
use select in where
SELECT * from tblFiles where storeId in (SELECT id from store ORDER BY datefield/id field desc limit 10)
You could workaround it with an UNIONed query, where each subquery searches for a particular id and enforces a LIMIT clause, like :
(SELECT *
FROM tblFiles
WHERE storeId = ?
ORDER BY createdDate DESC
LIMIT 10)
UNION
(SELECT *
FROM tblFiles
WHERE storeId = ?
ORDER BY createdDate DESC
LIMIT 10)
...
With this solution only one db hit will happen, and you are guarantee to get the LIMIT on a per id basis. Such a SQL can easily be generated from within php code.
Nb : the maximum allowed of UNIONs in a mysql query is 61.

MYSQL subquery in a LIMIT

I was wondering if it's possible to use a subquery inside a LIMIT.
The reason why I'd like to use this, is to return 20% (1/5th) of the best buying customers.
For instance (though this clearly doesn't work):
SELECT id, revenue
FROM customers
ORDER BY revenue DESC
LIMIT (SELECT (COUNT(*) / 5) FROM customer)
Is there a way to make a subquery in a limit, or return 20% in a different way?
A typical way of doing this using ANSI SQL is with window functions:
SELECT id, revenue
FROM (SELECT c.*,
ROW_NUMBER() OVER (ORDER BY revenue DESC) as seqnum,
COUNT(*) OVER () as cnt
FROM customers
) c
WHERE seqnum <= cnt * 0.2
ORDER BY revenue DESC;
Most databases support these functions.
MySQL is one of the few databases that do not support window functions. You can use variables:
SELECT id, revenue
FROM (SELECT c.*, (#rn := #rn + 1) as rn
FROM customers c CROSS JOIN
(SELECT #rn := 0) params
ORDER BY c.revenue DESC
) c
WHERE rn <= #rn / 5; -- The subquery runs first so #rn should have the total count here.

Mysql query is really slow. How do I increase the speed of the query?

I want to the latest results for my patients. The following sql returns 69,000 results after 87 seconds in mysqlworkbench. I have made both 'date' and 'patientid' columns as index.
select Max(date) as MaxDate, PatientID
from assessment
group by PatientID
I think my table has approximately 440,000 in total. Is it because that my table is 'large'?
Is there a way to increase the speed of this query, because I will have to embed this query inside other queries. For example like below:
select aa.patientID, assessment.Date, assessment.result
from assessemnt
inner join
(select Max(date) as MaxDate, PatientID
from assessment
group by PatientID) as aa
on aa.patientID = assessment.patientID and aa.MaxDate = assessment.Date
The above will give me the latest assessment results for each patient. Then I will also embed this piece of code to do other stuff... So I really need to speed up things. Anyone can help?
I wonder if this version would have better performance with the right indexes:
select a.patientID, a.Date, a.result
from assessemnt a
where a.date = (select aa.date
from assessment aa
where aa.patientID = a.patientID
order by aa.date desc
limit 1
);
Then you want an index on assessment(patientID, date).
EDIT:
Another approach uses an index on assessment(patient_id, date, result):
select a.*
from (select a.patient_id, a.date, a.result,
(#rn := if(#p = a.patient_id, #rn + 1,
if(#p := a.patient_id, 1, 1)
)
) as rn
from assessment a cross join
(select #p := -1, #rn := 0) params
order by patient_id desc, date desc
) a
where rn = 1;

MySQL FROM Subquery with parent group by

I need to show a ranking lists for a sport we manage.
It needs to sum up the 4 best results for each player (from a table that could have hundreds of results per player) and then sort the entire list from the player with the most points to least points.
The query below returns
ERROR 1054 (42S22): Unknown column 'r1.user_id' in 'where clause'
so I've gone off track somewhere.
SELECT r1.user_id, (
SELECT SUM(points)
FROM (
SELECT *
FROM ranking_matrix_points r2
WHERE user_id=r1.user_id
ORDER BY points DESC
LIMIT 4
) r3
) AS total_points
FROM ranking_matrix_points r1
WHERE
user.status IN('active')
GROUP BY r1.user_id
ORDER BY total_points DESC
One possible solution might be to number the rows for each user in order of points descending and then sum up the points with a rank <= 4. This might not perform very well though, and also you'll get a problem with ties (but you would have using limit too).
select user_id, sum(points) total_points
from (
select user_id, points,
(
case user_id
when #cur_user
then #row := #row + 1
else #row := 1 and #cur_user := user_id end
) as rank
from ranking_matrix_points,
(select #row := 0, #cur_user := '') r
order by user_id, points desc
) t
where rank <= 4
group by user_id;
I'm pretty sure there are better ways to do this but I can't think of any at the moment. This would have been very easy in just about any database with support for window functions, but sadly MySQL doesn't support any yet.
You don't need a double query, just
SELECT user_id, SUM(points)
FROM ranking_matrix_points
WHERE user.status in('active')
GROUP BY user_id
ORDER BY total_points DESC
LIMIT 4
or
SELECT TOP 4 user_id, SUM(points)
FROM ranking_matrix_points
WHERE user.status in('active')
GROUP BY user_id
ORDER BY total_points DESC

COUNT() with nulls, inside subquery

I have a bit of a problem with an advanced query that I am struggling to get my head around.
Essentally there are votes in a votes table that correspond to a given soundtrack. My query needs to get a rank for a soundtrack based on the votes that it has been awarded.
My approach below works just fine when there are votes in the table but the rank is given a NULL value when there are none in there.
Here's the query:
SELECT soundtrack.*,
(SELECT WrappedQuery.rank
FROM (SELECT #rownum := #rownum + 1 AS rank,
prequery.soundtrack_id
FROM (SELECT #rownum := 0) sqlvars,
(SELECT Count(*),
soundtrack_id
FROM vote
GROUP BY vote.soundtrack_id
ORDER BY Count(*) DESC) prequery) WrappedQuery
WHERE WrappedQuery.soundtrack_id = soundtrack.id) AS rank
FROM soundtrack
WHERE soundtrack.id = 33
AND live = 1
ORDER BY rank ASC
I have a feeling the problem is to do with the (SELECT COUNT(*)) part, but everything I have tried so far isn't working out.
Hoping someone could shed some light on my issue.
EDIT
Here's the SQLFiddle
http://www.sqlfiddle.com/#!2/c8db2/2/0
THAT ONE IS GOOD:
SELECT soundtrack.*,
(SELECT WrappedQuery.rank
FROM (SELECT #rownum := #rownum + 1 AS rank,
prequery.soundtrack_id
FROM (SELECT #rownum := 0) sqlvars,
(
SELECT COALESCE(COUNT(vote.soundtrack_id),0) AS no_rows,
soundtrack.id AS soundtrack_id
FROM soundtrack
LEFT JOIN vote ON soundtrack.id=vote.soundtrack_id
GROUP BY soundtrack.id
ORDER BY 1 DESC
) prequery) WrappedQuery
WHERE WrappedQuery.soundtrack_id = soundtrack.id) AS rank
FROM soundtrack
ORDER BY rank ASC;
SEE: http://www.sqlfiddle.com/#!2/74698/2/0
I've had some luck ranking in my own work using the row_number function. But otherwise, the coalesce function might help you out.
SELECT soundtrack.*, rankquery.rank
FROM (
SELECT row_number() over(partition by prequery.soundtrack_id order by prequery.num_votes) as rank,
prequery.soundtrack_id
FROM (
SELECT COALESCE(COUNT(*),0) as num_votes, soundtrack_id
FROM vote
GROUP BY soundtrack_id
ORDER BY num_votes DESC
) prequery
) rankquery
INNER JOIN soundtrack
rankquery.soundtrack_id = soundtrack.id
WHERE soundtrack.id = 33
AND live = 1
ORDER BY rank
SELECT soundtrack.*, rankquery.rank
FROM(
SELECT prequery.*, #rownum := #rownum + 1 AS rank
(
SELECT COALESCE(Count(*),0) as num_votes,
soundtrack_id
FROM vote
GROUP BY soundtrack_id
ORDER BY num_votes DESC
) as prequery,
(SELECT #rownum := 0) as sqlvars
) rankquery
INNER JOIN soundtrack
rankquery.soundtrack_id = soundtrack.id
WHERE soundtrack.id = 33
AND soundtrack.live = 1
ORDER BY rankquery.rank ASC