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.
Related
So i get 10 results from my first select and 1 from the other one after union like this:
(SELECT a.*,
b.*
FROM all a,
names b
WHERE b.name_id = a.name_id
ORDER BY name_id DESC
LIMIT 10)
UNION
(SELECT a.*,
b.*
FROM all a,
names b
WHERE b.name_id = a.name_id
ORDER BY request_id ASC
LIMIT 1)
i would like to get the result of the second select as the second last result like this
********
name_id 100
name_id 99
name_id 98
name_id 97
name_id 96
name_id 95
name_id 94
name_id 93
name_id 92
name_id 1 <- second select result as second last result
name_id 91
********
Can someone help pls?
Synthesize a row number column for the query as it stands and shuffle positions as needed.
SELECT x.name
, x.name_id
FROM (
SELECT #rownum:=#rownum + 1 as row_number,
t.name,
t.name_id
FROM (
-- original query from the question starts here
(SELECT b.name,
a.name_id
FROM allx a,
names b
WHERE b.name_id = a.name_id
ORDER BY name_id DESC
LIMIT 10)
UNION
(SELECT b.name,
a.name_id
FROM allx a,
names b
WHERE b.name_id = a.name_id
ORDER BY request_id ASC
LIMIT 1)
) t,
(SELECT #rownum := 0) r
) x
ORDER BY CASE row_number
WHEN 10 THEN 11
WHEN 11 THEN 10
ELSE row_number
END
;
(Note that the query has been sightly modified to avoid syntax errors / support the demo: table all has been named allx, explicit projections of the union's subqueries).
That gets complicated quickly thus next to ad hoc reporting it is preferable to synthesize an attribute in the subqueries of the union that reflects a global order.
Demo here (SQL fiddle)
Credits
Row number synthesizing taken from this SO answer
Interesting question given
+----+--------+
| id | sname |
+----+--------+
| 1 | sname1 |
| 2 | sname2 |
| 3 | sname3 |
| 4 | sname4 |
| 5 | sname5 |
| 6 | sname6 |
+----+--------+
6 rows in set (0.001 sec)
(select id,sname,#r:=#r+1 rn
from users
cross join(select #r:=0) r
order by sname desc limit 3
)
union
(
select u.id,u.sname,
#r:=#r - .9
from users u
left join (select id from users order by sname desc limit 3) u1 on u1.id = u.id
where u1.id is null
order by u.id asc limit 0,1
)
order by rn;
Where a variable is used to calculate a row number in the first sub query, since this variable is not reset in the second query a simple piece of arithmetic works out where to position the second sub query result. Note the second sub query uses a left join to check that the result has not already appeared in the first sub query,
I would suggest union all and three selects:
SELECT an.*
FROM ((SELECT a.*, n.*, 1 as ord
FROM all a JOIN
names n
ON n.name_id = a.name_id
ORDER BY n.name_id DESC
LIMIT 9
) UNION ALL
(SELECT a.*, n.*, 3 as ord
FROM all a JOIN
names n
ON n.name_id = a.name_id
ORDER BY n.name_id DESC
LIMIT 9 OFFSET 9
) UNION ALL
(SELECT a.*, b.*
FROM all a JOIN
names n
WHERE n.name_id = a.name_id
ORDER BY request_id ASC
LIMIT 1
)
) an
ORDER BY ord, name_id;
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.
MySQL #1235 - This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
Given 1 table as following
Item | Name | Price
----- ------------ --------
1 | Adidas | 310.00
2 | Nike Run | 30.00
3 | Puma | 150.00
4 | Nike Women | 20.00
5 | NB | 20.00
Would like to select records and return the sum amount. Do not sum up the 2 highest prices' record.
SELECT SUM(Price) as total_amount
FROM `test`
WHERE Item NOT IN (
SELECT Price
FROM `test`
ORDER BY Price DESC
LIMIT 2)
Expected Result:
total_amount
------------
70.00
How to use JOIN or alternative LIMIT in Subquery in this query?
Thank you.
You need a temp table:
SELECT SUM(Price) FROM test WHERE Item NOT IN (
SELECT * FROM (
SELECT Item FROM test ORDER BY Price DESC LIMIT 2
) AS tmp
)
Here's one option using a subquery with limit / offset:
select sum(price)
from (
select *
from test
order by price desc
limit 999999999
offset 2
) t
SQL Fiddle Demo
Just make sure the limit value is greater than the number of potential rows (which evidently is 18446744073709551615)...
Or you could use user-defined variables:
select sum(price)
from (
select *, #rn:=#rn + 1 rn
from test cross join (select #rn:= 0) t
) t
where rn > 2
If you looking to exclude the 2 highest prices which could be more than 2 records, this will also work with user defined variables:
select sum(price)
from (
select *, #rn:=if(#prevPrice=price, #rn,
if(#prevPrice:=price, #rn + 1, #rn + 1)) rn
from test cross join (select #rn:= 0, #prevPrice:= null) t
) t
where rn > 2
More Fiddle
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
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.