SQL filter rows without join - mysql

I'm always "irk" by unnecessary join. But in this case, I wonder if it's possible to not use join.
This is an example of the table I have:
id | team | score
1 | 1 | 300
2 | 1 | 257
3 | 2 | 127
4 | 2 | 533
5 | 3 | 459
This is what I want:
team | score | id
1 | 300 | 1
2 | 533 | 4
3 | 459 | 5
Doing a query looking like this:
(basically: who's the best player of each team)
SELECT team, MAX(score) AS score, id
FROM my_table
GROUP BY team
But I get something like that:
team | score | id
1 | 300 | 1
2 | 533 | 3
3 | 459 | 5
But it's not the third player that got 533 points, so the result have no consistency.
Is it possible to get truthworthy results without joining the table with itself? How to achieve that?

You can do it without joins by using subquery like this:
SELECT id, team, score
FROM table1 a
WHERE score = (SELECT MAX(score) FROM table1 b WHERE a.team = b.team);
However in big tables this can be very slow as you have to run the whole subquery for every row in your table.
However there's nothing wrong with using join to filter results like this:
SELECT id, team, score FROM table1 a
INNER JOIN (
SELECT MAX(score) score, team
FROM table1
GROUP BY team
) b ON a.score = b.score AND a.team = b.team
Although joining itself is quite expensive, this way you only have to run two actual queries regardless how many rows are in your tables. So in big tables this method can still be hundreds, if not thousands of times faster than the first method with subquery.

You can use variables:
SELECT id, team, score
FROM (
SELECT id, team, score,
#seq := IF(#t = team, #seq,
IF(#t := team, #seq + 1, #seq + 1)) AS seq,
#grp := IF(#t2 = team, #grp + 1,
IF(#t2 := team, 1, 1)) AS grp
FROM mytable
CROSS JOIN (SELECT #seq := 0, #t := 0, #grp := 0, #t2 := 0) AS vars
ORDER BY score DESC) AS t
WHERE seq <= 3 AND grp = 1
Variable #seq is incremented each time a new team is met as the records are being processed in descending score order. Variable #grp is used to enumerate records within each team partition. Records with #grp = 1 are the ones having the greatest score value within the team slice.
Demo here

Unfortantly , MySQL doesn't support window functions like ROW_NUMBER() which could have solved this easily.
There are several ways on doing that:
NOT EXISTS() :
SELECT * FROM YourTable t
WHERE NOT EXISTS(SELECT 1 FROM YourTable s
WHERE t.team = s.team AND s.score > t.score)
NOT IN() :
SELECT * FROM YourTable t
WHERE (t.team,t.score) IN(SELECT s.team,MAX(s.score)
FROM YourTable s
GROUP BY s.team)
A correlated query:
SELECT distinct t.id,t.team,
(SELECT s.score FROM YourTable s
WHERE s.team = t.team
ORDER BY s.score DESC
LIMIT 1)
FROM YourTable t
Or a join which I understand you already have.
EDIT : I take my words back, you can do it with a variable like #GiorgosBetsos solution.

You could do something like this:
SELECT team, score, id
FROM (SELECT *
,RANK() OVER
(PARTITION BY team ORDER BY score DESC) AS Rank
FROM my_table) ranked_result
WHERE Rank = 1;
Some info on Rank functionality: Clicketyclickclick

Related

How to pick a row randomly based on a number of tickets you have

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.

MYSQL - count how many times a person comes second in a competition

I have the following table called 'players' which contains scores from 3 games played by Tim, Bob and Jon.
playid | name | score
1 | Tim | 10
1 | Bob | 5
2 | Tim | 5
2 | Bob | 10
3 | Tim | 5
3 | Bob | 10
3 | Jon | 4
I want to be able to count the number of times that Tim, Bob and Jon have come second i.e. Tim = 2, Bob = 1, Jon = 0.
I have the following query:
SELECT name FROM players WHERE playid = 1 ORDER BY score Desc LIMIT 1, 1
Which returns the name of the person in second place in the first game i.e. Bob, but I can't figure out how to extend this to cover all games and players. Eventually I also want to be able to count the number of times they come 3rd, 4th etc.
Thanks in advance
Try with following one:
SELECT count(playid), name, score
FROM `players`
WHERE score = (SELECT MAX(score) FROM players WHERE score < (SELECT MAX(score) FROM players))
GROUP BY score, name;
With multiple joins and groupings:
select pl.name, ifnull(counter, 0) counter from (
select distinct name from players) pl
left join (
select players.name, count(*) counter from players
inner join (
select p.playid, max(p.score) as secondscore from (
select players.* from players
left join (
select playid, max(score) as maxscore
from players
group by playid) p
on p.playid = players.playid and p.maxscore = players.score
where p.maxscore is null) p
group by p.playid
) p
on p.playid = players.playid and p.secondscore = players.score
group by players.name) p
on p.name = pl.name
See the demo
You can use this query below to find the secondly-ranked people :
SELECT q2.playid, q2.name
FROM
(
SELECT q1.* , if(switch1,#r:=#r+1,#r:=0) as switch2
FROM
(
SELECT playid, score, name,
if(playid=#p,#p:=0,#p:=playid+1) as switch1
FROM players
JOIN ( SELECT #p:=0, #r:=-1 ) p2
ORDER BY playid, score desc
) q1
) q2
WHERE q2.switch2 = 1
ORDER BY q2.playid
playid name
------ ----
1 Bob
2 Tim
3 Tim
4 George
Rextester Demo
As per the comment by Raymond Nijland, in MySQL 8.0+ you can use window functions to achieve this:
SELECT name, COUNT(*) AS second_place_count
FROM (
SELECT
name,
playid,
ROW_NUMBER() OVER (PARTITION BY playid ORDER BY score DESC) AS rn
FROM players
) AS ranks
WHERE ranks.rn = 2
GROUP BY name
...or if you want to extend this to all places:
SELECT
name,
rn AS place,
COUNT(*) AS place_count
FROM (
SELECT
name,
playid,
ROW_NUMBER() OVER (PARTITION BY playid ORDER BY score DESC) AS rn
FROM players
) AS ranks
GROUP BY
name,
rn

How to retrieve number of records from mysql database in a group?

I have a table with multiple records in it for full name and surname.
---------------------------
id | name | lastname
---------------------------
1 | A smith | smith
2 | B smith | smith
3 | c smith | smith
4 | A josh | josh
5 | B josh | josh
6 | C josh | josh
7 | D josh | josh
8 | A white | white
9 | D white | white
10| z white | white
And so so....more than 100k records. Now what i want to do is to retrieve latest 7 records for each surname up to 9 surnames. I have 500 surnames but i just want latest 9 surnames.. In my application "latest" means "largest value of id column."
This is the command that i tried to make but when i execute it. i am not getting any response from server. this is happening because of database size and my command is taking a lot of time. its just keep me waiting:
SELECT * FROM `queue` s WHERE ( SELECT COUNT(*) FROM `queue` f WHERE f.lastname = s.lastname AND f.id >= s.id LIMIT 0 , 7) <=7
Can someone suggest me better way of retrieving my goal.
Let's build this up from the basics.
Your first step is to create a subquery to get the latest nine surnames (http://sqlfiddle.com/#!9/aee62e/19/0). By that I mean the surnames with the highest id values.
SELECT lastname, MAX(id) namerank
FROM t
GROUP BY lastname
ORDER BY MAX(id) DESC
LIMIT 9
And, in MySQL, that was the easy part. Now you need to retrieve the seven highest ranked (largest id) rows for each selected surname. As a start, you could do this to get all records for the selected surnames, in descending order by id. (http://sqlfiddle.com/#!9/aee62e/18/0).
SELECT t.*, namerank
FROM t
JOIN (
SELECT lastname, MAX(id) namerank
FROM t
GROUP BY lastname
ORDER BY MAX(id) DESC
LIMIT 9
) h ON t.lastname = h.lastname
ORDER BY t.lastname, t.id DESC
This is correct, but contains too many rows. Next we need to get the ranking for each lastname's rows. A lower ranking means a higher id value. This is the nasty hack in MySQL. (Nasty because it mixes procedural operations on local variables with the inherently declarative nature of SQL.) (http://sqlfiddle.com/#!9/aee62e/17/0)
SELECT IF(detail.lastname = #prev_lastname, #rank := #rank+1, #rank :=1) rank,
namerank,
#prev_lastname := detail.lastname lastname,
id,
name
FROM (
SELECT t.*, namerank
FROM t
JOIN (
SELECT lastname, MAX(id) namerank
FROM t
GROUP BY lastname
ORDER BY MAX(id) DESC
LIMIT 9
) h ON t.lastname = h.lastname
ORDER BY t.lastname, t.id DESC
) detail
JOIN (SELECT #rank := 0, #prev_lastname := '') initializer
Finally we need to wrap that whole mess in an outer query to pick off the seven highest ranked rows for each lastname value. (http://sqlfiddle.com/#!9/aee62e/16/0)
SELECT *
FROM (
SELECT IF(detail.lastname = #prev_lastname, #rank := #rank+1, #rank :=1) rank,
namerank,
#prev_lastname := detail.lastname lastname,
id,
name
FROM (
SELECT t.*, namerank
FROM t
JOIN (
SELECT lastname, MAX(id) namerank
FROM t
GROUP BY lastname
ORDER BY MAX(id) DESC
LIMIT 9
) h ON t.lastname = h.lastname
ORDER BY t.lastname, t.id DESC
) detail
JOIN (SELECT #rank := 0, #prev_lastname := '') initializer
) ranked
WHERE rank <= 7
ORDER BY namerank DESC, rank
I believe the technical term for the complexity of your requirement and this solution is "hairball." It definitely puts the structured in Structured Query Language.

How to get the rank of a row in mysql query

this my database structure
table : players
id | name | score
1 | Bob | 600
2 | Alex | 1400
3 | John | 800
4 | sara | 2000
I need to select john's row and count what is the john' rank OrderBy score
as you see john is 3rd (800) , sara is 1st (2000), Alex is 2nd (1400) in score ranks
Select #rownum := #rownum + 1 AS rank form players where id=3 OrderBy score
any idea ?
You can do it by a subquery and count the players who has score more than the score of a certian id
Select count(*) as rank
from players
where score > (select score from players where id=3)
But if you want to have other information beside the rank you can do it by
SELECT ranks . *
FROM (
SELECT #rownum := #rownum +1 ‘rank’, p.id, p.score
FROM players p, (SELECT #rownum :=0)r
ORDER BY score DESC
) ranks
WHERE id =3
select rank
from
(
Select id, name, #rownum := #rownum + 1 AS rank
from players
cross join (select #rownum := 0) r
Order By score desc
) tmp
where id = 3
Might be easier to do a self join, where the joined table score is greater (to get the rows with a higher score) and just do a count:-
SELECT COUNT(*)
FROM players a
INNER JOIN players b
ON a.score >= b.score
WHERE a.id = 3
Question is what to do with equal scores.

MySQL find user rank for each category

Let's say I have the following table:
user_id | category_id | points
-------------------------------
1 | 1 | 4
2 | 1 | 2
2 | 1 | 5
1 | 2 | 3
2 | 2 | 2
1 | 3 | 1
2 | 3 | 4
1 | 3 | 8
Could someone please help me to construct a query to return user's rank per category - something like this:
user_id | category_id | total_points | rank
-------------------------------------------
1 | 1 | 4 | 2
1 | 2 | 3 | 1
1 | 3 | 9 | 1
2 | 1 | 7 | 1
2 | 2 | 2 | 2
2 | 3 | 4 | 2
First, you need to get the total points per category. Then you need to enumerate them. In MySQL this is most easily done with variables:
SELECT user_id, category_id, points,
(#rn := if(#cat = category_id, #rn + 1,
if(#cat := category_id, 1, 1)
)
) as rank
FROM (SELECT u.user_id, u.category_id, SUM(u.points) as points
FROM users u
GROUP BY u.user_id, u.category_id
) g cross join
(SELEct #user := -1, #cat := -1, #rn := 0) vars
ORDER BY category_id, points desc;
You want to get the SUM of points for each unique category_id:
SELECT u.user_id, u.category_id, SUM(u.points)
FROM users AS u
GROUP BY uc.category_id
MySQL doesn't have analytic functions like other databases (Oracle, SQL Server) which would be very convenient for returning a result like this.
The first three columns are straightforward, just GROUP BY user_id, category_id and a SUM(points).
Getting the rank column returned is a bit more of a problem. Aside from doing that on the client, if you need to do that in the SQL statement, you could make use of MySQL user-defined variables.
SELECT #rank := IF(#prev_category = r.category_id, #rank+1, 1) AS rank
, #prev_category := r.category_id AS category_id
, r.user_id
, r.total_points
FROM (SELECT #prev_category := NULL, #rank := 1) i
CROSS
JOIN ( SELECT s.category_id, s.user_id, SUM(s.points) AS total_points
FROM users s
GROUP BY s.category_id, s.user_id
ORDER BY s.category_id, total_points DESC
) r
ORDER BY r.category_id, r.total_points DESC, r.user_id DESC
The purpose of the inline view aliased as i is to initialize user defined variables. The inline view aliased as r returns the total_points for each (user_id, category_id).
The "trick" is to compare the category_id value of the previous row with the value of the current row; if they match, we increment the rank by 1. If it's a "new" category, we reset the rank to 1. Note this only works if the rows are ordered by category, and then by total_points descending, so we need the ORDER BY clause. Also note that the order of the expressions in the SELECT list is important; we need to do the comparison of the previous value BEFORE it's overwritten with the current value, so the assignment to #prev_category must follow the conditional test.
Also note that if two users have the same total_points in a category, they will get distinct values for rank... the query above doesn't give the same rank for a tie. (The query could be modified to do that as well, but we'd also need to preserve total_points from the previous row, so we can compare to the current row.
Also note that this syntax is specific to MySQL, and that this is behavior is not guaranteed.
If you need the columns in the particular sequence and/or the rows in a particular order (to get the exact resultset specified), we'd need to wrap the query above as an inline view.
SELECT t.user_id
, t.category_id
, t.total_points
, t.rank
FROM (
SELECT #rank := IF(#prev_category = r.category_id, #rank+1, 1) AS rank
, #prev_category := r.category_id AS category_id
, r.user_id
, r.total_points
FROM (SELECT #prev_categor := NULL, #rank := 1) i
CROSS
JOIN ( SELECT s.category_id, s.user_id, SUM(s.points) AS total_points
FROM users s
GROUP BY s.category_id, s.user_id
ORDER BY s.category_id, total_points DESC
) r
ORDER BY r.category_id, r.total_points DESC, r.user_id DESC
) t
ORDER BY t.user_id, t.category_id
NOTE: I've not setup a SQL Fiddle demonstration. I've given an example query which has only been desk checked.