MYSQL Ranking Issue - mysql

I am stuck on how to modify my query to get the ranking results that I am looking for. I have been looking at the questions and queries on SO but can not get the results. I can get the query to do the calculation and return data but the "rank" is not correct. Any nudge would be fantastic!
Table 1 contains school data:
+-----+------------+---------------+
| SID | schoolName | schoolCountry |
+-----+------------+---------------+
| 1 | ASD | UAE |
| 2 | ASIJ | Japan |
| 3 | ASP | France |
+-----+------------+---------------+
Table 2 contains review data (my query has more columns, but this is how it works).
+-----+----------+--------+----+----+----+----+----+
| RID | schoolID | active | Q1 | Q2 | Q3 | Q4 | Q5 |
+-----+----------+--------+----+----+----+----+----+
| 1 | 1 | 1 | 8 | 9 | 5 | 1 | 9 |
| 2 | 2 | 1 | 7 | 6 | 6 | 7 | 9 |
| 3 | 1 | 0 | 1 | 4 | 7 | 8 | 5 |
| 4 | 3 | 1 | 2 | 10 | 6 | 7 | 5 |
+-----+----------+--------+----+----+----+----+----+
I am trying to create different ranks (Country, Region, Overall) by averaging the overall review score for a school. I am currently working on the country ranking and my query so far is.
SELECT SID, schoolName, rank, average
FROM (
SELECT (#rank := #rank + 1) AS rank,schools.SID, schools.schoolName,
ROUND(AVG(IF(reviews.active = 1, ((Q1+Q2+Q3+Q4+Q5+Q6+Q7+Q8+Q9+Q10+Q11+Q12+Q13+Q14+Q15+Q16+Q17+Q18+Q19+Q20+Q21+Q22+Q23+Q24+Q25+Q26+Q27+Q28+Q29+Q30+Q31+Q32+Q33+Q34+Q35+Q36+Q37+Q38+Q39+Q40+Q41+Q42+Q43+Q44+Q45+Q46+Q47+Q48+Q49+Q50+Q51+Q52)/(52*10)*10), NULL)) ,1) AS average
FROM schools
RIGHT JOIN reviews ON reviews.schoolID = schools.SID
CROSS JOIN (SELECT #rank := 0) AS vars
WHERE schools.schoolCountry = 'United Arab Emirates'
GROUP BY schools.SID
) as order_ranked
ORDER BY `order_ranked`.`average` DESC
My output comes back as:
+-----+----------------------------------------+------+---------+--+
| SID | schoolName | rank | average | |
+-----+----------------------------------------+------+---------+--+
| 568 | GEMS Wellington Primary School | 3 | 8.3 | |
| 1 | American School of Dubai | 1 | 8.1 | |
| 561 | Dubai American Academy | 4 | 7.9 | |
| 560 | Deira International School | 11 | 7.7 | |
| 569 | GEMS World Academy Dubai | 10 | 7.0 | |
| 570 | Greenfield Community School | 8 | 6.7 | |
| 565 | GEMS American Academy Abu Dhabi | 6 | 6.0 | |
| 584 | Universal American School | 5 | 5.9 | |
| 558 | American Academy In Al Mizhar | 7 | 5.5 | |
| 579 | The Cambridge High School Abu Dhabi | 9 | 4.8 | |
| 576 | Ras Al Khaimah English Speaking School | 2 | 4.3 | |
+-----+----------------------------------------+------+---------+--+
As you can see it ranks, but not correctly. I just can't figure out why.

I'm not sure why you would be using right join for this. In MySQL, you often have to sort in the subquery before using the variables.
SELECT SID, schoolName, (#rank := #rank + 1) AS rank, average
FROM (SELECT s.SID, s.schoolName,
ROUND(AVG(Q1+Q2+Q3+Q4+Q5+Q6+Q7+Q8+Q9+Q10+Q11+Q12+Q13+Q14+Q15+Q16+Q17+Q18+Q19+Q20+Q21+Q22+Q23+Q24+Q25+Q26+Q27+Q28+Q29+Q30+Q31+Q32+Q33+Q34+Q35+Q36+Q37+Q38+Q39+Q40+Q41+Q42+Q43+Q44+Q45+Q46+Q47+Q48+Q49+Q50+Q51+Q52)/(52*10)*10)) AS average
FROM schools s JOIN
reviews r
ON r.schoolID = s.SID
WHERE s.schoolCountry = 'United Arab Emirates' AND r.isactive = 1
GROUP BY schools.SID
ORDER BY average DESC
) sr CROSS JOIN
(SELECT #rank := 0) AS vars
ORDER BY average DESC

Rank should depend on the ordering of average..but there is no order by in the inner query. First compute the average and calculate rank thereafter. Also, i moved the where condition to join because the way you have it is equivalent to an inner join.
SELECT SID, schoolName, #rank := #rank + 1 AS rank, average
FROM (
SELECT schools.SID, schools.schoolName,
ROUND(AVG(IF(reviews.active = 1, ((Q1+Q2+Q3+Q4+Q5+Q6+Q7+Q8+Q9+Q10+Q11+Q12+Q13+Q14+Q15+Q16+Q17+Q18+Q19+Q20+Q21+Q22+Q23+Q24+Q25+Q26+Q27+Q28+Q29+Q30+Q31+Q32+Q33+Q34+Q35+Q36+Q37+Q38+Q39+Q40+Q41+Q42+Q43+Q44+Q45+Q46+Q47+Q48+Q49+Q50+Q51+Q52)/(52*10)*10), NULL)) ,1) AS average
FROM schools
RIGHT JOIN reviews ON reviews.schoolID = schools.SID AND schools.schoolCountry = 'United Arab Emirates'
GROUP BY schools.SID,schools.schoolName
) as order_ranked
CROSS JOIN (SELECT #rank := 0) AS vars
ORDER BY `average` DESC

Related

Retrieving a variable number of rows using a table join

This is an addition layer of complexity on another question I asked here: Using GROUP BY and ORDER BY in same MySQL query
Same table structure and problem, except this time imagine that the past_election table is now set up as...
| election_ID | Date | jurisdiction | Race | Seats |
|-------------|------------|----------------|---------------|-------|
| 1 | 2016-11-08 | federal | president | 1 |
| 2 | 2016-11-08 | state_district | state senator | 2 |
(last record has seats set as 2 instead of 1.)
I want to use the Seats number to grab different numbers of records, ordered by the number of votes, for each group. So in this case with the following additional tables...
candidates
| Candidate_ID | FirstName | LastName | MiddleName |
|--------------|-----------|----------|------------|
| 1 | Aladdin | Arabia | A. |
| 2 | Long | Silver | John |
| 3 | Thor | Odinson | NULL |
| 4 | Baba | Yaga | NULL |
| 5 | Robin | Hood | Locksley |
| 6 | Sherlock | Holmes | J. |
| 7 | King | Kong | Null |
past_elections-candidates
| ID | PastElection | Candidate | Votes |
|----|--------------|-----------|-------|
| 1 | 1 | 1 | 200 |
| 2 | 1 | 2 | 100 |
| 3 | 1 | 6 | 50 |
| 4 | 2 | 3 | 75 |
| 5 | 2 | 4 | 25 |
| 6 | 2 | 5 | 150 |
| 7 | 2 | 7 | 100 |
I would expect the following output:
| election_ID | FirstName | LastName | votes | percent |
|-------------|-----------|----------|-------|---------|
| 1 | Aladdin | Arabia | 200 | 0.5714 |
| 2 | Robin | Hood | 150 | 0.4286 |
| 2 | King | Kong | 100 | 0.2857 |
I've tried setting a variable and using that with a LIMIT statement but variables don't work in limits. I've also tried using ROW_NUMBER() (I'm not using MySQL 8.0 so this won't work but I'd be willing to upgrade if it did) or a related workaround like #row_number := IF ... and then filtering based on the row number but nothing has worked.
Last tried query:
SELECT pe.election_ID as elec,
pe.Seats as s,
pecs.row_num,
c.FirstName,
c.LastName,
pecs.max_votes AS votes,
pecs.max_votes / pecs.total_votes AS percent
FROM past_elections pe
JOIN `past_elections-candidates` pec ON pec.PastElection = pe.election_ID
JOIN (SELECT PastElection,
Candidate,
#row_num := IF(PastElection = #current_election, #current_election + 1, 1) as row_num,
MAX(Votes) AS max_votes,
SUM(Votes) AS total_votes,
#current_election := PastElection
FROM `past_elections-candidates`
GROUP BY PastElection) pecs ON pecs.PastElection = pec.PastElection AND pecs.row_num <= pe.Seats
JOIN candidates c ON c.Candidate_ID = pec.Candidate
Use MySQL 8 regardless ;)
Use ROW_NUMBER to order the past elections:
SELECT *, ROW_NUMBER() OVER(PARTITION BY pastelection ORDER BY votes DESC) as rown
FROM `past_elections-candidates`
Join this to past_elections as a subquery (this is just the bit you're stuck on with the "using pe.seats to vary the number of rows returned per election" and doesn't include the percent bits:
SELECT *
FROM
past_elections pe
INNER JOIN
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY pastelection ORDER BY votes DESC) as rown
FROM `past_elections-candidates`
) pecr
ON pecr.pastelection = pe.electionid AND
pecr.rown <= pe.seats
If you want to test things out on 8 before you upgrade, loads of the db fiddle sites support v8
ps; percent-y stuff can be done at the same time as the ROW_NUMBER with eg:
votes/SUM(votes) OVER(PARTITION BY past_election)
eg for election ID 1 that sum will be 200+100+50, giving 200/350 = ~57%
SELECT *, votes/SUM(votes) OVER(PARTITION BY past_election) as pcnt, ROW_NUMBER() OVER(PARTITION BY pastelection ORDER BY votes DESC) as rown
FROM `past_elections-candidates`
You need to calc it before filtering
I don't have the right fields listed but this is as close as I'll probably get tonight... I've gotten the rows I need but need to join the candidate table to get the name out...
Using Dense_Rank seems to work for this...
SELECT * FROM (
SELECT pec.PastElection,
c.FirstName,
c.LastName,
pec.Votes,
pecs.totalVotes,
pe.Seats as s,
DENSE_RANK() OVER(PARTITION BY PastElection ORDER BY Votes DESC) as rank_votes
FROM `past_elections-candidates` pec
JOIN (SELECT PastElection,
Max(Votes) as maxVotes,
Sum(Votes) as totalVotes
FROM `past_elections-candidates`
GROUP BY PastElection) pecs ON pecs.PastElection = pec.PastElection
JOIN `past_elections` pe ON pec.PastElection = pe.election_ID
JOIN candidates c ON c.Candidate_ID = pec.Candidate
) t WHERE rank_votes <= s;
This results in
| PastElection | FirstName | LastName | Votes | totalVotes | s | rank_votes |
|--------------|-----------|----------|-------|------------|---|------------|
| 1 | Aladdin | Arabia | 200 | 350 | 1 | 1 |
| 2 | Robin | Hood | 150 | 350 | 2 | 1 |
| 2 | King | Kong | 100 | 350 | 2 | 2 |
I guess it's just kind of messy having the rank_votes and s columns in the data, but that's honestly fine with me if it gets the results I need.

SQL - Sum Top N values in each row

This is my SQL table.
+-------+------+------+------+------+
| name | q1 | q2 | q3 | q4 |
+-------+------+------+------+------+
| Alex | 5 | 4 | 10 | 7 |
| Brown | 7 | 6 | 4 | 1 |
| Chris | 10 | 10 | 9 | 10 |
| Dave | 8 | 4 | 6 | 0 |
+-------+------+------+------+------+
I'd like to sum the top 2 scores from each user in my SQL query above.
For example, the top 2 scores of Alex are 10 and 7, and so the sum is 10 + 7 = 17
I have tried the following query:
SELECT NewStudents.name, SUM(q1+q2+q3+q4) FROM NewStudents
GROUP BY NewStudents.name;
To sum all q1, q2, q3, q4 but this query sums all q1 to q4, not the top 2 scores among q1 to q4.
How can I construct the statement that I want to do in mySQL?
As stated in #Shadow in comment.. Your database need restructure again.. Because that's not database work.. You can restructure and make design like this..
+-------+----+--------+
| name | q | point |
+-------+----+--------+
| Alex | 1 | 5 |
| Alex | 2 | 4 |
| Alex | 3 | 10 |
| Alex | 4 | 7 |
| Brown | 1 | 7 |
| Brown | 2 | 6 |
| Brown | 3 | 4 |
| Brown | 4 | 1 |
| Chris | 1 | 10 |
| Chris | 2 | 10 |
| Chris | 3 | 9 |
| Chris | 4 | 10 |
| Dave | 1 | 8 |
| Dave | 2 | 4 |
| Dave | 3 | 6 |
| Dave | 4 | 0 |
+-------+----+--------+
And for the query you can do like this:
select
name, sum(point)
from(
select
name, q, point,
ROW_NUMBER() OVER (PARTITION BY name ORDER BY point DESC) as ranked
from newstudents) rankedSD
where
ranked in (1,2)
group by
name
You can check the demo here:
Demo<>Fiddle
Edit : You can use Window Function. You can read Row_Number() Function
A normalised design might look like this:
name q score
Alex 1 5
Alex 2 4
Alex 3 10
Alex 4 7
In older versions of MySQL, you can use variables for this purpose:
select name, sum(q)
from (select nq.*,
(#rn := if(#n = name, #rn + 1,
if(#n := name, 1, 1)
)
) as rn
from (select nq.*
from ((select name, q1 as q from t
) union all
(select name, q2 as q from t
) union all
(select name, q3 as q from t
) union all
(select name, q4 as q from t
)
) nq
order by name, q desc
) nq cross join
(select #n := '', #rn := 0) params
) nq
where rn <= 2;

Select groups with the three biggest values

Consider this data:
---+-----------+-------+--+
| id | name | grade | |
+----+-----------+-------+--+
| 13 | Maria | 10 | |
| 18 | Lorenzo | 10 | |
| 2 | Cissa | 10 | |
| 3 | Neto | 9 | |
| 15 | Gabriel | 9 | |
| 10 | Laura | 9 | |
| 12 | JoĆ£ozinho | 8 | |
| 16 | Sergio | 8 | |
| 8 | Adriele | 8 | |
| 6 | Jorgito | 8 | |
| 5 | Aline | 8 | |
| 1 | Cintia | 8 | |
| 19 | Fabiana | 7 | |
| 11 | Vinicius | 7 | |
| 9 | Tatiane | 7 | |
| 7 | Chico | 7 | |
| 4 | Marcos | 7 | |
| 14 | Pedro | 6 | |
| 17 | Mauricio | 6 | |
| 20 | Leonardo | 6 | |
+----+-----------+-------+--+
I need the students with the three biggest grades. I think I need to group the data by grade and limit to the top 3 groups.
"SELECT * FROM student GROUP BY grade LIMIT 3" only gives me 3 rows, that's not what I want.
I've tried to use HAVING to filter the groups, but without success. I don't want to set the filter grade>MAX(grade)-2, because theoretically I will not know the grades. But this filter didn't work anyway.
I'am using MySQL. Please help!
You can do this using a join:
select s.*
from student s join
(select grade
from student
group by grade
order by grade desc
limit 3
) g3
on s.grade = g3.grade;
In most databases, you an do this using in:
select s.*
from student s
where s.grade in (select grade
from student
group by grade
order by grade desc
limit 3
);
However, MySQL seems to reject this syntax.
select s1.*
from students s1
join (select distinct grade from students
order by grade desc limit 3) s2 on s1.grade = s2.grade
Alternatively:
select *
from students
where grade >= (select distinct grade from students
order by grade desc limit 2,1)
select s.*
from student s join
(select top 3 grade
from student
group by grade
order by grade desc
) g3
on s.grade = g3.grade;

How to get Latest N Records of selected Group

I want to run a query on MySql version 5.1.9 that returns me only top two (order by JoiningDate) of selected Dept.
For example, my data is like:
+-------+------------------------------------------+----------+------------+
| empid | title | Dept | JoiningDate|
+-------+------------------------------------------+----------+------------+
| 1 | Research and Development | 1 | 2015-08-06 |
| 2 | Consultant | 2 | 2015-08-06 |
| 3 | Medical Consultant | 3 | 2015-08-06 |
| 4 | Officer | 4 | 2015-08-06 |
| 5 | English Translator | 5 | 2015-08-06 |
| 6 | Teacher | 1 | 2015-08-01 |
| 7 | Physical Education | 2 | 2015-08-01 |
| 8 | Accountant | 3 | 2015-08-01 |
| 9 | Science Teacher | 4 | 2015-08-01 |
| 10 | Home Science | 5 | 2015-08-01 |
| 11 | Research Assistant | 1 | 2015-08-05 |
| 12 | Consultant | 2 | 2015-08-05 |
| 13 | Consultant HR | 3 | 2015-08-05 |
| 14 | Technical Lead | 4 | 2015-08-05 |
| 15 | Hindi Translator | 5 | 2015-08-05 |
| 16 | Urdu Teacher | 1 | 2015-08-02 |
| 17 | Physical Education | 2 | 2015-08-02 |
| 18 | Accountant | 3 | 2015-08-02 |
| 19 | Science | 4 | 2015-08-02 |
| 20 | Home Science | 5 | 2015-08-02 |
+-------+------------------------------------------+----------+------------+
I want the query to output the latest joined two empid's of Dept (1,2,3) i.e:
+-------+------------------------------------------+----------+------------+
| empid | title | Dept | JoiningDate|
+-------+------------------------------------------+----------+------------+
| 1 | Research and Development | 1 | 2015-08-06 |
| 11 | Research Assistant | 1 | 2015-08-05 |
| 2 | Consultant | 2 | 2015-08-06 |
| 12 | Consultant | 2 | 2015-08-05 |
| 3 | Medical Consultant | 3 | 2015-08-06 |
| 13 | Consultant HR | 3 | 2015-08-05 |
+-------+------------------------------------------+----------+------------+
In mysql you can use user defined variables to achieve you desired results
SELECT
t.empid,
t.title,
t.Dept,
t.JoiningDate
FROM
(
SELECT
*,
#r:= CASE WHEN #g = b.Dept THEN #r + 1 ELSE 1 END rounum,
#g:= b.Dept
FROM (
SELECT *
FROM table1
CROSS JOIN (SELECT #r:= NULL,#g:=NULL) a
WHERE Dept IN(1,2,3)
ORDER BY Dept,JoiningDate DESC
) b
) t
WHERE t.rounum <=2
DEMO
Use a correlated sub-select to count number of rows with same date but a later JoiningDate. If less than 2, return the row.
select empid, title, Dept, JoiningDate
from tablename t1
where (select count(*) from tablename t2
where t2.Dept = t1.Dept
and t2.JoiningDate > t1.JoiningDate) < 2
Query
select *
from emp_ t1
where
(
select count(*) from emp_ t2
where t2.Dept = t1.Dept
and t2.JoiningDate > t1.JoiningDate
) <= 1
and t1.Dept in (1,2,3)
order by t1.Dept;
SQL Fiddle
Can also achieve it by giving a rownumber.
Query
select t2.empid,
t2.title,
t2.Dept,
t2.JoiningDate
from
(
select empid,
title,
Dept,
JoiningDate,
(
case Dept
when #curA
then #curRow := #curRow + 1
else #curRow := 1 and #curA := Dept end
) as rn
from employee t,
(select #curRow := 0, #curA := '') r
where Dept in (1,2,3)
order by Dept,JoiningDate desc
)t2
where rn < 3;
SQL Fiddle

Ranking multiple entries sql

I have a table in which I log the the visits each user has on it's profile page.
Given the fact that the table entries are like
id user time ip
is it possible to display the ranking of a certain user's visits using a sigle query?
Example: Your ranking: #400 - meaning the user ranks 400 on the number of page hits.
Here is one method. The innermost subquery counts the number of visits for each user. The next subquery calculates a variable called #uservisits which stores the value for the given user. The outer query then uses this to calculate the rank:
select count(*) as rank
from (select user, numvisits, #uservisits := if(user = $user, numvisits, #uservisits)
from (select user, count(*) as numvisits
from table t
group by user
) t cross join
(select #uservisits := 0) init
) t
where numvisits >= #uservisits
This sort of thing?
SELECT #rank := #rank+1 AS rank, province, cityCount
FROM
( SELECT #rank := 0 ) z
JOIN
( SELECT province, COUNT(*) AS cityCount
FROM Canada
GROUP BY province
) x
ORDER BY cityCount DESC;
+------+---------------------------+------------+
| rank | province | cityCount |
+------+---------------------------+------------+
| 1 | Quebec | 1045 |
| 2 | Ontario | 891 |
| 3 | British Columbia | 716 |
| 4 | Saskatchewan | 573 |
| 5 | Alberta | 573 |
| 6 | Newfoundland and Labrador | 474 |
| 7 | Nova Scotia | 331 |
| 8 | Manitoba | 299 |
| 9 | New Brunswick | 210 |
| 10 | Yukon | 114 |
| 11 | Nunavut | 107 |
| 12 | Northwest Territories | 94 |
| 13 | Prince Edward Island | 57 |
+------+---------------------------+------------+