Order by highest value alternating with lowest value - mysql

I currently need to order data by highest value down, and then lowest value up, in between.
My Query is close, but doesn't quite order by largest down, though it is inserting the lowest in between:
DEMO Fiddle
select users.*
from users CROSS JOIN (select #even := 0, #odd := 0) param
order by
IF(score > 1, 2*(#odd := #odd + 1), 2*(#even := #even + 1) + 1),
score DESC;
Current Results
Email Score
----- --------
foo1#gmail.com 42
foo5#gmail.com 1
foo2#gmail.com 49
foo6#gmail.com 0
foo3#gmail.com 37
foo4#gmail.com 7
foo#gmail.com 22
Desired Results
Email Score
----- --------
foo2#gmail.com 49
foo6#gmail.com 0
foo1#gmail.com 42
foo5#gmail.com 1
foo3#gmail.com 37
foo4#gmail.com 7
foo#gmail.com 22

You can achieve using MySQL but avoid such complex SQL statements as the same can be achieved using programming language very easily.
SET #totalRows := (CASE WHEN (SELECT COUNT(*) FROM users) IS NULL THEN 0 ELSE (SELECT COUNT(*) FROM users) END);
PREPARE stmt1 FROM '(SELECT t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num, email, score FROM users UNION ALL SELECT ROW_NUMBER() OVER (ORDER BY score ASC) AS row_num, email, score FROM users) AS t ORDER BY t.row_num, t.score DESC LIMIT 0,?)';
EXECUTE stmt1 USING #totalRows;
Here is the explanation to achieve it:
Set variable #totalRows contains total rows in users table as that many rows will be displayed as a final result set.
Used ROW_NUMBER() function of MySQL to set ordering based on SOCRE field DESCENDING and ASCENDING for another result set.
Combined both the result set using UNION ALL statement of MySQL
Add the LIMIT keyword to make sure the final result must have a total number of rows that should not exceed #totalRows variable.
As in MySQL LIMIT statement we can't pass a dynamic value at a query level. I used the approach of prepare a statement.
You can ignore row_num column I used as final result set.
Hope my solution will help you.

For MySql 8.0+ you can use ROW_NUMBER() window function:
SELECT email, score
FROM (
SELECT *,
ROW_NUMBER() OVER (ORDER BY score DESC) rn1,
ROW_NUMBER() OVER (ORDER BY score ASC) rn2
FROM users
) t
ORDER BY LEAST(rn1, rn2), rn1;
For previous versions you can simulate ROW_NUMBER() with correlated subqueries (with the cost of poor performance for large datasets):
SELECT email, score
FROM (
SELECT u1.*,
(SELECT COUNT(*) FROM users u2 WHERE u2.score > u1.score) rn1,
(SELECT COUNT(*) FROM users u2 WHERE u2.score < u1.score) rn2
FROM users u1
) t
ORDER BY LEAST(rn1, rn2), rn1;
See the demo.

Related

Getting the user's position in the leaderboard

Query stopped working (getting the user's position in the leaderboard):
SELECT
`rank`, `uid`, `battleWinScore`
FROM
(SELECT
#rank:=#rank+1 AS `rank`, `uid`, `battleWinScore`
FROM
`rating`, (SELECT #rank := 0) r
ORDER BY `battleWinScore` DESC
) t
WHERE uid = 572;
In the rating table, we need to get the user's position by field battleWinScore.
I am absolutely not good at mysql. Help =)
If you are running MySQL 8.0, just use row_number() (or rank(), if you want to consistently allow ties).
select *
from (
select uid, battleWinScore, rank() over(order by battleWinScore desc) rn
from rating
) r
where uid = 572
In earlier versions, I would recommend a correlated subquery rather than user variables:
select uid, battleWinScore
(select count(*) + 1 from rating r1 where r1.battleWinScore > r.battleWinScore) as rn
from rating r
where uid = 572

SQL query not returning distinct values

I have a game leaderboard comprised of 500 rows of data and I wrote a script to return that data and have no duplicate scores. However, I am getting duplicate scores returned to me. Here is my script.
SELECT DISTINCT
username, score,
FIND_IN_SET(score, (SELECT DISTINCT GROUP_CONCAT(score ORDER BY score DESC)
FROM TPS_STATS)) AS rank
FROM
TPS_STATS
ORDER BY
rank ASC
LIMIT 100;
An example of the duplicate results I am seeing is posted as an image.
If your version of MySql is 8.0 then you can use row_number():
SELECT
username,
score,
row_number() OVER (ORDER BY score desc, username) rn
FROM TPS_STATS
ORDER BY score desc, username
LIMIT 100
See the demo.
If it is lower:
select
username,
score,
(select count(*) from TPS_STATS where score > t.score) +
(select count(*) from TPS_STATS where score = t.score and username < t.username) + 1
rank
from TPS_STATS t
order by rank, username
limit 100
See the demo

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 - Percentile calculation and update it in other column in the same table

I have a table in MySQL (phpMYAdmin) with the following columns
I am trying to determine the percentile for each row and update that value in the G1Ptile column. G1Ptile column is the percentile calculation based on G1%. I am using the following based on John Woo's answer given here
SELECT `G1%`,
(1-ranks/totals)*100 Percentile FROM (
SELECT distinct `G1%`,
#rank:=#rank + 1 ranks,
(SELECT COUNT(*) FROM PCount) totals
FROM PCount a,
(SELECT #rank:=0) s
ORDER BY `G1%` DESC ) s;
and get the following output
The output is in a select statement, I want to be able to update it to the G1Ptile column in my table, however I am unable to update it using
UPDATE `PCount` SET `G1Ptile`= --(All of the select query mentioned above)
Can you please help with modifying the query/suggest an alternative so that I can use the percentile values obtained using the above query and update it into G1Ptile in the same table. One more problem I have is that there are two 20% values in G1%, however the percentile assigned to one is 20 and other is 30. I want both of them to be 20 and the next row in the series to be 30.
I would write your calculation as:
SELECT `G1%`,
(1 - ranks / totals) * 100 as Percentile
FROM (SELECT `G1%`,
(#rank := #rank + 1) ranks,
(SELECT COUNT(*) FROM PCount) as totals
FROM (SELECT DISTINCT `G1%`
FROM PCount
ORDER BY `G1%` DESC
) p CROSS JOIN
(SELECT COUNT(*) as totals, #rank := 0
FROM Pcount
) params
) p;
I made certain changes more consistent with how MySQL processes variables. In particular, the SELECT DISTINCT and ORDER BY are in a subquery. This is necessary in more recent versions of MySQL (although in the most recent you can use window functions).
This can now be incorporated into an update using JOIN:
UPDATE PCount p JOIN
(SELECT `G1%`,
(1 - ranks / totals) * 100 as Percentile
FROM (SELECT `G1%`,
(#rank := #rank + 1) ranks,
(SELECT COUNT(*) FROM PCount) as totals
FROM (SELECT DISTINCT `G1%`
FROM PCount
ORDER BY `G1%` DESC
) p CROSS JOIN
(SELECT COUNT(*) as totals, #rank := 0
FROM Pcount
) params
) pp
) pp
ON pp.`G1%` = p.`G1%`
SET p.G1Ptile = pp.percentile;

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