sql - #curRow based on column - mysql

I'm creating rank system so I need to set rank position for every user.
I do this:
SELECT #curRow := #curRow + 1 AS position,
Sum(point_user.amount) AS points
FROM users u
JOIN point_user ON u.id = point_user.user_id
JOIN (SELECT #curRow := 0) r
GROUP BY
u.id
ORDER BY
position ASC
And I'm getting:
+----------+----------+
| position | points |
+----------+----------+
| 1 | 86 |
| 2 | 239 |
| 3 | 45 |
+----------+----------+
Now, how can I set row number, based on points?
Should looks like:
+----------+----------+
| position | points |
+----------+----------+
| 1 | 239 |
| 2 | 86 |
| 3 | 45 |
+----------+----------+
UPDATE
SELECT (#curRow := #curRow + 1) as position,
points,
u2.id
FROM (SELECT Sum(pu.amount) AS points
FROM users u JOIN
point_user pu
ON u.id = pu.user_id
GROUP BY u.id
ORDER BY points DESC
) upu, users u2
CROSS JOIN (
SELECT #curRow := 0
) params
ORDER BY points DESC

In your query, it might be sufficient to change the ORDER BY to ORDER BY points DESC -- because that seems to be your intention.
However, MySQL has a known problem with variables and GROUP BY. The solution is simply to use a subquery:
SELECT (#curRow := #curRow + 1) as position, points
FROM (SELECT Sum(pu.amount) AS points
FROM users u JOIN
point_user pu
ON u.id = pu.user_id
GROUP BY u.id
ORDER BY points DESC
) upu CROSS JOIN
(SELECT #curRow := 0) params
ORDER BY points DESC;

Related

How to reference generated/aliased table in same query?

I want to find a user's position in a leaderboard and return the 4 users above and 4 users below their position.
My table, 'predictions', looks something like this:
+----+---------+--------+-------+---------+
| id | userId | score | rank | gameId |
+----+---------+--------+-------+---------+
| 1 | 12 | 11 | 1 | 18 |
| 2 | 1 | 6 | 4 | 18 |
| 3 | 43 | 7 | 3 | 12 |
| 4 | 4 | 9 | 2 | 18 |
| 5 | 98 | 2 | 5 | 19 |
| 6 | 3 | 0 | 6 | 18 |
+----+---------+--------+-------+---------+
Obviously this isn't properly ordered, so I run this:
SELECT l.userId,
l.rank,
l.score,
l.createdAt,
#curRow := #curRow + 1 AS row_number
FROM (SELECT * FROM `predictions` WHERE gameId = 18) l
JOIN (SELECT #curRow := 0) r
ORDER BY rank ASC
which gets me a nice table with each entry numbered.
I then want to search this generated table, find the row_number where userId = X, and then return the values 'around' that result.
I think I have the logic of the query down, I just can't work out how to reference the table 'generated' by the above query.
It would be something like this:
SELECT *
FROM (
SELECT l.userId,
l.rank,
l.score,
l.createdAt,
#curRow := #curRow + 1 AS row_number
FROM (SELECT * FROM `predictions` WHERE gameId = 18) l
JOIN (SELECT #curRow := 0) r
ORDER BY rank ASC) generated_ordered_table
WHERE row_number < (SELECT row_number FROM generated_ordered_table WHERE userId = 1)
ORDER BY row_number DESC
LIMIT 0,5
This fails. What I'm trying to do is to generate my first table with the correct query, give it an alias of generated_ordered_table, and then reference this 'table' later on in this query.
How do I do this?
MySQL version 8+ could have allowed the usage of Window functions, and Common Table Expressions (CTEs); which would have simplified the query quite a bit.
Now, in the older versions (your case), the "Generated Rank Table" (Derived Table) cannot be referenced again in a subquery inside the WHERE clause. One way would be to do the same thing twice (select clause to get generated table) again inside the subquery, but that would be relatively inefficient.
So, another approach can be to use Temporary Tables. We create a temp table first storing the ranks. And, then reference that temp table to get results accordingly:
CREATE TEMPORARY TABLE IF NOT EXISTS gen_rank_tbl AS
(SELECT l.userId,
l.rank,
l.score,
l.createdAt,
#curRow := #curRow + 1 AS row_number
FROM (SELECT * FROM `predictions` WHERE gameId = 18) l
JOIN (SELECT #curRow := 0) r
ORDER BY rank ASC)
Now, you can reference this temp table to get the desired results:
SELECT *
FROM gen_rank_tbl
WHERE row_number < (SELECT row_number FROM gen_rank_tbl WHERE userId = 1)
ORDER BY row_number DESC
LIMIT 0,5
You could use a bunch of unions
select userid,rank,'eq'
from t where gameid = 18 and userid = 1
union
(
select userid,rank,'lt'
from t
where gameid = 18 and rank < (select rank from t t1 where t1.userid = 1 and t1.gameid = t.gameid)
order by rank desc limit 4
)
union
(
select userid,rank,'gt'
from t
where gameid = 18 and rank > (select rank from t t1 where t1.userid = 1 and t1.gameid = t.gameid)
order by rank desc limit 4
);
+--------+------+----+
| userid | rank | eq |
+--------+------+----+
| 1 | 4 | eq |
| 4 | 2 | lt |
| 12 | 1 | lt |
| 3 | 6 | gt |
+--------+------+----+
4 rows in set (0.04 sec)
But it's not pretty
You can use two derived tables:
SELECT p.*,
(#user_curRow = CASE WHEN user_id = #x THEN rn END) as user_rn
FROM (SELECT p.*, #curRow := #curRow + 1 AS rn
FROM (SELECT p.*
FROM predictions p
WHERE p.gameId = 18
ORDER BY rank ASC
) p CROSS JOIN
(SELECT #curRow := 0, #user_curRow := -1) params
) p
HAVING rn BETWEEN #user_curRow - 4 AND #user_currow + 4;

MySQL: How to get leaderboard position, for each event in a series

I have the following data structure (simplified):
Users:
ID
Name
Events:
ID
Date
Results:
ID
User ID (foreign key)
Event ID (foreign key)
Points: (int)
I would like to know (ideally the most efficient way):
How to get a user's position in a 'league' compared to other users. And - - If possible using one query (or sub queries), how to break this down by event, e.g. the user's position after the 1st event, 2nd event, 3rd event etc.
I can get the leaderboard with:
select users.name, SUM(results.points) as points
from results
inner join users on results.user_id = users.id
group by users.id
order by points DESC
However, I'd like to know a user's position without having to return the entire table if possible.
Edit: I have supplied some sample data here.
Ideal output:
| User ID | Rank |
| 3 | 1 |
| 1 | 2 |
| 2 | 3 |
and something similar to (not exactly like this, it's flexible, just something that shows the user's rank from each event)
| User ID | After Event | Rank |
| 1 | 1 | 1 |
| 1 | 2 | 1 |
| 1 | 3 | 2 |
| 2 | 1 | 2 |
| 2 | 2 | 2 |
| 2 | 3 | 1 |
| 3 | 1 | 3 |
| 3 | 2 | 3 |
| 3 | 3 | 3 |
MySQL 8.0+ supports window functions so the use of dense_rank() comes in handy.
MySQL under 8.0 solution
Since your version is 5.7 you could imitate this like below:
select
t.id,
CASE WHEN #prevRank = points THEN #currRank
WHEN #prevRank := points THEN #currRank := #currRank + 1
END AS rank
from (
select users.id, SUM(results.points) as points
from results
inner join users on results.user_id = users.id
group by users.id
order by points DESC
) t
cross join (SELECT #currRank := 0, #prevRank := NULL) r
If you need data for particular user then add a WHERE condition to filter out everyone else in an outer query:
select *
from (
<< above query here >>
) t
where id = ? -- your id here
MySQL 8.0+ solution
rank is a reserved keyword so backticks are required when naming a column. We're using dense_rank window function which will assign ranks based od descending sorting of points acquired:
select id, dense_rank() over (order by points desc) as `rank`
from (
select users.id, SUM(results.points) as points
from results
inner join users on results.user_id = users.id
group by users.id
) t
order by `rank`
SET #rowno = 0;
select UserID, max(points), #rowno:=#rowno+1 as rank from
(
select users.id as UserID ,users.name as users_name,events.name, SUM(results.points) as points
from results
inner join users on results.user_id = users.id
inner join events on results.event_id= events.id
group by users.id,events.name,users.name
order by points DESC
) as T
group by UserID
order by max(points) desc

Ranking Where Clause

I have a problem with my Query. I want to select data from my Ranking Query.
My query output is Perfect Like:
------------------------------
Rank | ID | Username | Value
-------------------------------
1 | 5 | Julian | 5000
2 | 2 | Masha | 2400
3 | 4 | Misha | 2300
4 | 1 | Jackson | 1900
5 | 9 | Beruang | 400
-------------------------------
But when I select ID = 4, the output like this:
------------------------------
Rank | ID | Username | Value
-------------------------------
***1*** | 4 | Misha | 2300
-------------------------------
The output of ranking is 1, not 3.
My Query is :
SELECT #curRank := #curRank + 1 AS rank,
a.id, a.username
FROM partimer a CROSS JOIN
(SELECT #curRank := 0) vars
# WHERE a.id = 4
ORDER By id;
If Rank is dinamically computed, you could do this:
SELECT *
FROM (
SELECT #curRank := #curRank + 1 AS rank
, a.id
, a.username
FROM partimer a
CROSS JOIN (SELECT #curRank := 0) vars
ORDER BY value
) p
WHERE p.id = 4;
This way, you store temporary table with rank, and then select from it.
you should like this
SELECT *
FROM (
...{your Query}...
) qry
WHERE qry.id = 4
Your rank is calculated dynamically in the query. The issue here is that these dynamic calculations are applied after the where clause. In other words, when MySQL executes your query, it first filters out all the rows that adhere to the where clause, and only then applies the rank calculation. In the given query, the row with id=4 is indeed the 1st row between all the rows that adhere to the where clause.
One way to get your desired behavior is to perform the original query first and only then filter the results by using this query as a subquery and applying the where clause to the outer query:
SELECT *
FROM (SELECT #curRank := #curRank + 1 AS rank, a.id, a.username
FROM partimer a
CROSS JOIN (SELECT #curRank := 0) vars
ORDER By id) t
WHERE id = 4

Found ranking position for user

I have a these tables
USERS
+----------+---------------+
| id_users | usr_email |
+----------+---------------+
| 1 | a#domain.com |
| 2 | b#domain.com |
| 3 | c#domain.com |
| 4 | d#domain.com |
| 5 | e#domain.com |
+----------+---------------+
RANKING
+-------------+-----------+----------+
| id_ranking | id_users | points |
+-------------+-----------+----------+
| 50 | 1 | 27 | //3rd
| 51 | 2 | 55 | //1st
| 52 | 3 | 9 | //5th
| 53 | 4 | 14 | //4th
| 54 | 5 | 38 | //2nd
+-------------+-----------+----------+
I would like to retireve user's data along with it's ranking position, filtering by e-mail. So for example if I want info for mail c#domain.com I should get
+----------+--------|---------------+
| id_users | points | rank_position |
+----------+--------|---------------+
| 3 | 9 | 5 |
+----------+--------|---------------+
I've found this piece of query that returns the ranking position
SELECT x.id_users, x.position
FROM (
SELECT t1.id_ranking, t1.id_users, #rownum := #rownum + 1 AS position
FROM ranking t1
JOIN (SELECT #rownum := 0) r ORDER BY t1.points desc
) x
WHERE x.id_users = 3
But I can't manage to use it in my old query
select u.*, r.points
from users u
left join ranking r on r.id_users = u.id_users
where u.usr_email = 'c#domain.com'
My attemp
select u.*, r.points, p.*
from users u
left join ranking r on r.id_users = u.id_users,
(SELECT x.id_users, x.position
FROM (
SELECT t1.id_ranking, t1.id_users, #rownum := #rownum + 1 AS position
FROM ranking t1
JOIN (SELECT #rownum := 0) r ORDER BY t1.points desc
) x
WHERE x.id_users = u.id_users) p
where u.usr_email = 'c#domain.com'
Any help?
You are missing a join condition. But also, the outer join to ranking is not necessary. You can "remember" the points in the subquery:
select u.*, r.points, r.position
from users u left join
(select r.*, #rownum := #rownum + 1 AS position
from ranking r CROSS JOIN
(SELECT #rownum := 0) r
order by r.points desc
) r
ON r.id_users = u.id_users
where u.usr_email = 'c#domain.com'
select u.id_users,
r.points,
count(ifnull(r2.id_users, 0)) + 1 as rank_position
from users u
join ranking r
on u.id_users = r.id_users
left join ranking r2
on r2.points > r.points
where u.usr_email = 'c#domain.com'
group by u.id_users, r.points
Fiddle: http://sqlfiddle.com/#!2/1444e8/1/0
Rather than a variable this counts the number of users who have more points than the given user, and then adds one. This is an equivalent way of calculating their rank, in terms of number of points.

Points on highest record in Mysql

My table
+------+-------+--------+
| NAME | MARKS | POINTS |
+------+-------+--------+
| S1 | 53 | (null) |
| S2 | 55 | (null) |
| S3 | 56 | (null) |
| S4 | 55 | (null) |
| S5 | 52 | (null) |
| S6 | 51 | (null) |
| S7 | 53 | (null) |
+------+-------+--------+
Refer : http://www.sqlfiddle.com/#!2/5d046/1
I would like to add 3,2,1 points to the highest Marks. Here S3 goes to 3 points, S2,S4 goes to 2 points and S1,S7 goes to 1 points.
Final outputs looks,
+------+-------+--------+
| NAME | MARKS | POINTS |
+------+-------+--------+
| S1 | 53 | 1 |
| S2 | 55 | 2 |
| S3 | 56 | 3 |
| S4 | 55 | 2 |
| S5 | 52 | 0 |
| S6 | 51 | 0 |
| S7 | 53 | 1 |
+------+-------+--------+
Plz help
My suggestion is that you first calculate the ranking of each mark, and then use that in a case statement in an update.
The following query shows one way to calculate the ranking:
select t.*,
#rn := if(#marks = marks, #rn, #rn + 1) as ranking,
#marks := marks
from myTable t cross join
(select #rn := 0, #marks := -1) const
order by t.marks desc;
(As a note: I am a bit uncomfortable with this method, because MySQL does not guarantee the order of evaluation of the two expressions with constants. If #marks were set before #rn, then it wouldn't work. In practice, that does not seem to happen. And, this is more efficient that the equivalent with a correlated subquery.)
You can then put this into an update using join:
update myTable join
(select t.*,
#rn := if(#marks = marks, #rn, #rn + 1) as ranking,
#marks := marks
from myTable t cross join
(select #rn := 0, #marks := -1) const
order by t.marks desc
) mr
on myTable.Name = mr.Name
set myTable.Points := (case when mr.ranking = 1 then 3
when mr.ranking = 2 then 2
when mr.ranking = 3 then 1
else 0
end);
This has been tested on your SQL Fiddle.
You can do it via variables (see samples in other answers), or via case:
select
myTable.*,
case
when max1.marks is not null then 3
when max2.marks is not null then 2
when max3.marks is not null then 1
else 0
end as score
from
myTable
LEFT JOIN
(select marks from myTable order by marks desc limit 1) AS max1
ON myTable.marks=max1.marks
LEFT JOIN
(select marks from myTable order by marks desc limit 2,1) AS max2
ON myTable.marks=max2.marks
LEFT JOIN
(select marks from myTable order by marks desc limit 3,1) AS max3
ON myTable.marks=max3.marks;
the demo can be found here.
UPDATE myTable t1
INNER JOIN
(
SELECT #row:=#row-1 AS RowPoints, Marks
FROM (
SELECT Marks
FROM myTable
GROUP BY Marks
ORDER BY Marks DESC
LIMIT 3
) AS TopMarks
INNER JOIN (SELECT #row:=4) AS RowInit
) AS AddPoints ON t1.Marks = AddPoints.Marks
SET Points = COALESCE(Points, 0) + AddPoints.RowPoints;
This should work just fine. You should and and index on the Marks column.
The simplest way to do this:
SELECT t.Name Name, t.Marks Marks,
(CASE WHEN Marks = (Select max(marks) from mytable) THEN 3 ELSE 0 END+
CASE WHEN Marks = (Select min(marks) from (Select distinct marks
from mytable order by marks desc limit 2) a) THEN 2 ELSE 0 END+
CASE WHEN Marks = (Select min(marks) from (Select distinct marks
from mytable order by marks desc limit 3) b) THEN 1 ELSE 0 END)
AS `Points`
FROM mytable t;
SQL Fiddle