I have 2 different tables in my database by the name of: rank, settings.
Here is how each table looks like with a few records in them:
Table #rank:
id points userid
-- ----- ------
1 500 1
2 300 2
3 900 3
4 1500 4
5 100 5
6 700 6
7 230 7
8 350 8
9 850 9
10 150 10
Table #settings:
userid active
------ ------
1 0
2 1
3 1
4 1
5 1
6 0
7 1
8 1
9 0
10 1
What I basically want to achieve is to select a specific row from #rank by ID, sort it by points and select 3 rows above the specific ID and 3 row below the specific ID but only for rows where the active column (from #settings) for the user equals 1.
For example:
I would like to select from #rank the ID of 8, and it should return me the following:
rank points userid
---- ----- ------
2 150 10
3 230 7
4 300 2
5 350 8
6 900 3
7 1500 4
I have created quite an extensive query for this, but the problem is, that it is ranking the columns before it decides that the user is active or not. However I need to rank the columns after it is decided that the user is active or not.
SELECT sub2.sort, sub2.points, sub2.userid
FROM
(
SELECT #sort1 := #sort1 + 1 AS sort, puu.points, puu.userid
FROM rank as puu,
(SELECT #sort1 := 0) s
LEFT JOIN
(
settings as p11
)
ON puu.userid = p11.userid,
WHERE p11.active = 1
ORDER BY puu.points DESC
) sub1
INNER JOIN
(
SELECT #sort2:=#sort2+1 AS sort, p2.points, p2.userid
FROM rank as p2,
(SELECT #sort2 := 0) s
LEFT JOIN
(
settings as p12
)
ON p2.userid = p12.userid,
WHERE p12.active = 1
ORDER BY points DESC
) sub2
ON sub1.userid = :userid
AND sub2.sort BETWEEN (sub1.sort - 5) AND (sub1.sort + 5)
Can you guys find any solution for my problem? If you can provide an SQLfiddle demo, that would be really awesome!
SELECT sort, points, user_id, active FROM (
SELECT #pos := #pos + 1 AS sort, id, points, r.user_id, s.active,
IF(user_id = :userid, #userpos := #pos, 0)
FROM rank r
JOIN settings s USING(user_id)
JOIN (SELECT #pos := 0, #userpos := 0) p
WHERE s.active = 1
ORDER BY points DESC
) list
WHERE sort BETWEEN #userpos - 3 AND #userpos + 3
I made a fiddle here: sqlfiddle
Possibly using 2 unioned queries to get the values before the ranking:-
SELECT #rank:=#rank+1 AS rank, points, userid
FROM
(
SELECT id, points, userid
FROM
(
SELECT rank.id, rank.points, rank.userid
FROM rank
INNER JOIN
(
SELECT points
FROM rank
WHERE id = 8
) sub0
ON rank.points >= sub0.points
INNER JOIN settings
ON rank.userid = settings.userid
WHERE settings.active = 1
ORDER BY rank.points LIMIT 3
) sub1
UNION ALL
SELECT id, points, userid
FROM
(
SELECT rank.id, rank.points, rank.userid
FROM rank
INNER JOIN
(
SELECT points
FROM rank
WHERE id = 8
) sub0
ON rank.points < sub0.points
INNER JOIN settings
ON rank.userid = settings.userid
WHERE settings.active = 1
ORDER BY rank.points DESC LIMIT 3
) sub1
) sub2
CROSS JOIN (SELECT #rank:=0) sub3
ORDER BY points
Related
I have this table called my_users
my_id | name | raffle_tickets
1 | Bob | 3
2 | Sam | 59
3 | Bill | 0
4 | Jane | 10
5 | Mike | 12
As you can see Sam has 59 tickets so he has the highest chance of winning.
Chance of winning:
Sam = 59/74
Bob = 3/74
Jane = 10/74
Bill = 0/74
Mike = 12/74
PS: 74 is the number of total tickets in the table (just so you know I didn't randomly pick 74)
Based on this, how can I randomly pick a winner, but ensure those who have more raffles tickets have a higher chance of being randomly picked? Then the winner which is picked, has 1 ticket deducted from their total tickets
UPDATE my_users
SET raffle_tickets = raffle_tickets - 1
WHERE my_id = --- Then I get stuck here...
Server version: 5.7.30
For MySQL 8+
WITH
cte1 AS ( SELECT name, SUM(raffle_tickets) OVER (ORDER BY my_id) cum_sum
FROM my_users ),
cte2 AS ( SELECT SUM(raffle_tickets) * RAND() random_sum
FROM my_users )
SELECT name
FROM cte1
CROSS JOIN cte2
WHERE cum_sum >= random_sum
ORDER BY cum_sum LIMIT 1;
For 5+
SELECT cte1.name
FROM ( SELECT t2.my_id id, t2.name, SUM(t1.raffle_tickets) cum_sum
FROM my_users t1
JOIN my_users t2 ON t1.my_id <= t2.my_id
WHERE t1.raffle_tickets > 0
GROUP BY t2.my_id, t2.name ) cte1
CROSS JOIN ( SELECT RAND() * SUM(raffle_tickets) random_sum
FROM my_users ) cte2
WHERE cte1.cum_sum >= cte2.random_sum
ORDER BY cte1.cum_sum LIMIT 1;
fiddle
You want a weighted pull from a random sample. For this purpose, variables are probably the most efficient solution:
select u.*
from (select u.*, (#t := #t + raffle_tickets) as running_tickets
from my_users u cross join
(select #t := 0, #r := rand()) params
where raffle_tickets > 0
) u
where #r >= (running_tickets - raffle_tickets) / #t and
#r < (running_tickets / #t);
What this does is calculate the running sum of tickets and then divide by the number of tickets to get a number between 0 and 1. For example this might produce:
my_id name raffle_tickets running_tickets running_tickets / #t
1 Bob 3 3 0.03571428571428571
2 Sam 59 62 0.7380952380952381
4 Jane 10 72 0.8571428571428571
5 Mike 12 84 1
The ordering of the original rows doesn't matter -- which is why there is no ordering in the subquery.
The ratio is then used with rand() to select a particular row.
Note that in the outer query, #t is the total number of tickets.
Here is a db<>fiddle.
I have lists of users with his points and game id. I need to find the rank of the specified user based on the game order by the maximum lb_point. Below is my table.
lb_id user_id game_id room_id lb_point
------------------------------------------------
1 1 2 1 670
2 1 1 2 200
3 1 2 2 650
4 1 1 1 400
5 3 2 1 700
6 4 2 5 450
7 2 1 3 550
8 2 1 1 100
9 1 1 1 200
I have already done this by using PHP code and its working fine as follows.
$game_id = 2;
$user_id = 1;
$query_rnk = $this->db->query('SELECT user_id AS uid FROM leader_board WHERE game_id = "'.$game_id.'" GROUP by user_id ORDER BY lb_point DESC');
if ($query_rnk->num_rows() > 0){
$j=1;
foreach($query_rnk->result() as $row_rnk){
if($row_rnk->uid == $user_id){
$rnk_status = 1;
break;
}
$j++;
}
if($rnk_status == 1){
$resp['rank'] = $j;
}
}
Answer is: 2.
But i need to find this by using one query. Any idea?
You could do with a select counting the number of rows of a proper select
select count(*)
from (
select distinct user_id
from leader_board
where lb_point >= (select max( lb_point )
from leader_board
where user_id = 1
and game_id = 2 )
and game_id = 2
) t
If you want to get the rank of user_id = 1, try this:
SELECT urank
FROM (
SELECT user_id AS uid, #rank := #rand + 1 AS urank
FROM leader_board
CROSS JOIN (SELECT #rank := 0) t
WHERE game_id = '$game_id'
GROUP by user_id
ORDER BY lb_point DESC
) tmp
WHERE uid = '$user_id'
SELECT count(*)+1 as Rank from (select lb_point from games where game_id =2 order by lb_point DESC) as list where list.lb_point > (select max(lb_point) from games where user_id = 1 and game_id = 2)
This will give the rank 2 for user_id = 1 in game_id = 2 which is the answer you've mentioned in question.
I have a participant table:
userid name
1 John
2 Sam
3 Harry
And there is contest table:
contestid contestname
1 abc
2 def
3 ghi
Score table looks like this:
id contestid userid score
1 1 1 200
2 1 2 300
3 1 3 250
4 2 1 500
5 2 2 400
6 3 2 800
Now, given an userid, I need to find out his rank in all the contest.
The Rank should be based on Contest and Score.
Output should be like this for userid=1:
contestid rank
1 3
2 1
3 Nil
How can I get this output?
1. If you need it Rank by Score and Contest then try this
SET #rank := 0;
SET #prev := NULL;
SELECT contestid,userid,score,Rank FROM
(
SELECT contestid,userid,score,#rank := IF(#prev = contestid, #rank + 1, 1) AS rank,
#prev := contestid
FROM scores ORDER BY contestid, score DESC
) S Where userid = 1
FIDDLE DEMO
2. If you need it Rank by Score then try this
SELECT contestid,userid,score, FIND_IN_SET( score,
(SELECT GROUP_CONCAT(score ORDER BY score DESC) FROM scores)) AS rank
FROM scores
Where UserId = 1
Fiddle Demo
3. If you want UserName and COntest Name then try this
SELECT C.contestname,P.name,S.score, FIND_IN_SET( S.score,
(SELECT GROUP_CONCAT(score ORDER BY score DESC) FROM scores)) AS rank
FROM scores S Join participant P ON P.userid =C.userid
Join contest C ON C.contestid = S.contestid
Where UserId = 1
I need to pull the name of the students who stood second positions from grade 1 to grade 12. each grade has separate databases with similar table structure
I have the following data:
Set 1
uid marks
1 10
2 20
3 17
4 17
5 20
6 20
Set 2
uid marks
1 10
2 20
3 17
4 17
5 20
6 17
7 20
I need a query which can say uid 3,4 are second in set 1 and 3,4,6 are second in set 2.
i need it in a single query because there are several set of databases
what could be the possible way?
I tried:
SELECT * FROM TBL WHERE marks ! = SELECT MAX(marks) from tbl
but it fetched all marks except the highest
Try this out:
SELECT uid, marks FROM (
SELECT uid, marks, #rank := #rank + (#prevMarks != marks) rank, #prevMarks := marks
FROM t, (SELECT #rank := 0, #prevMarks := 0) init
ORDER BY marks
) s
WHERE rank = 2
Fiddle here.
Another alternative without User Defined Variables:
SELECT t.uid, t.marks FROM t
JOIN (
SELECT DISTINCT marks FROM t
ORDER BY marks
LIMIT 1, 1
) s
ON t.marks = s.marks
Output:
| UID | MARKS |
|-----|-------|
| 3 | 17 |
| 4 | 17 |
Use LIMIT and ORDER BY
SELECT * FROM TBL ORDER BY marks DESC LIMIT 1,1
There you ordered all students by marks fro hi to low. And then limit return from second (0 is first record) and return only one record.
If need all students with second mark, the use subquery
SELECT * FROM TBL WHERE marks = (
SELECT marks FROM TBL ORDER BY marks DESC GROUP BY marks LIMIT 1,1
)
SELECT *
FROM table
WHERE mark = (
SELECT MAX(mark)
FROM table
WHERE mark <
(
SELECT MAX(mark)
FROM table
)
)
Try this
SELECT t.marks, t.uid, (
SELECT COUNT( marks ) +1
FROM tbl t1
WHERE t.marks < t1.marks
) AS rank
FROM tbl t
LIMIT 0 , 30
now you can use rank column with bit modification below
SELECT * from (
SELECT t.marks, t.uid, (
SELECT COUNT( marks ) +1
FROM tbl t1
WHERE t.marks < t1.marks
) AS rank
FROM tbl t
) alias where rank=n (2 here)
I tried asking question before, but it's hard to ask in specific without right terminology I am not quite familiar with. So here is an example
Take this query for example:
(
SELECT *
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
)
UNION ALL
(
SELECT c.*
FROM comments c JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.parent_id = p.id
LIMIT 5
)
id parent_id depth title
1 0 0 Title 1
2 0 0 Title 2
3 1 1 Title 3
4 1 1 Title 4
5 1 1 Title 5
6 1 1 Title 6
7 1 1 Title 7
I get two depth 0 rows and in join I get 5 child elements of those two returned queries as well. What I would like to get is to get 5 child elements of each of those two queries, total of 10 rows (of depth 1). For example:
id parent_id depth title
1 0 0 Title 1
2 0 0 Title 2
3 1 1 Title 3
4 1 1 Title 4
5 1 1 Title 5
6 1 1 Title 6
7 1 1 Title 7
8 2 1 Title 8
9 2 1 Title 9
10 2 1 Title 10
11 2 1 Title 11
12 2 1 Title 12
Is that even possible with adjacency list and a requirement to return everything as union (flat)?
edit:
Thanks to Bill Karwin's answer, I got it working now. I wonder still if there is a shorter way to write this. I have 6 (0-5) depth levels, so my query is rather long (and probably not optimal). Here is what it looks like for three levels (you can imagine what the full one looks like).
-- DEPTH LEVEL 0
(
SELECT * FROM (
SELECT *, 1 as _rn, #parent:=0
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) as D0
)
union all
-- DEPTH LEVEL 1
(
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.comment_id,#row+1,1) AS _rn, #parent:=c.comment_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.comment_id = p.id
) AS _ranked
WHERE _ranked._rn <= 5
)
union all
-- DEPTH LEVEL 2
(
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.comment_id,#row+1,1) AS _rn, #parent:=c.comment_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
(
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.comment_id,#row+1,1) AS _rn, #parent:=c.comment_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.comment_id = p.id
) AS _ranked
WHERE _ranked._rn <= 2
)
) p ON c.comment_id = p.id
) AS _ranked
WHERE _ranked._rn <= 2
)
You can't do this with LIMIT, because LIMIT is applied after the result set is completely finished, after all joining, grouping, sorting, etc.
You're using a variation of the greatest-n-per-group type of query. It's tricky to do this in MySQL because MySQL doesn't support the ROW_NUMBER() window function supported by many other SQL databases.
Here's a workaround for MySQL, in which user-defined variables can take the place of partitioned row numbers:
SELECT *
FROM (
SELECT c.*, #row:=IF(#parent=c.parent_id,#row+1,1) AS _rn, #parent:=c.parent_id
FROM (SELECT #parent:=null) AS _init
STRAIGHT_JOIN comments c
INNER JOIN
(
SELECT id
FROM comments
WHERE depth = 0
ORDER BY id DESC
LIMIT 2
) p ON c.parent_id = p.id
) AS _ranked
WHERE _ranked._rn <= 5