Ranking Where Clause - mysql

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

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;

SQL how to get the max price of the 33% cheapests products

I need to to get the max price of the 33% cheapests products. My idea is like this. Of course, this code is just an example. I need to use subqueries.
select max((select price from products order by preco limit 33% )) as result from products
For example
product_id price
1 10
2 50
3 100
4 400
5 900
6 8999
I need I query that returns 50, since 33% of the rows are 2, and the max value of the 2(33%) of the rows is 50.
In MySQL 8+, you would use window functions:
select avg(precio)
from (select p.*, row_number() over (order by precio) as seqnum,
count(*) over () as cnt
from products p
) p
where seqnum <= 0.33 * cnt;
Obviously there are multiple approaches to this but here is how I would do it.
Simply get a count on the table. This will let me pick the max price of the cheapest 33% of products. Let's say it returned n records. Third of that would be n/3. Here you can either round up or down but needs to be rounded in case of a fraction.
Then my query would be something like SELECT * FROM products ORDER BY price ASC LIMIT 1 OFFSET n/3. This would return me a single record with minimal calculations and look ups on MySQL side.
For MySQL versions under MySQL 8.0 you can use MySQL's user variables to simulate/emulate a ROW_NUMBER()
Query
SELECT
t.product_id
, t.price
, (#ROW_NUMBER := #ROW_NUMBER + 1) AS ROW_NUMBER
FROM
t
CROSS JOIN (SELECT #ROW_NUMBER := 0) AS init_user_variable
ORDER BY
t.price ASC
Result
| product_id | price | ROW_NUMBER |
| ---------- | ----- | ---------- |
| 1 | 10 | 1 |
| 2 | 50 | 2 |
| 3 | 100 | 3 |
| 4 | 400 | 4 |
| 5 | 900 | 5 |
| 6 | 8999 | 6 |
When we get the ROW_NUMBER we can use that in combination with ROW_NUMBER <= CEIL(((SELECT COUNT(*) FROM t) * 0.33));
Which works like this
(SELECT COUNT(*) FROM t) => Counts and returns 6
(SELECT COUNT(*) FROM t) * 0.33) Calculates 33% from 6 which is 1.98 and returns it
CEIL(..) Return the smallest integer value that is greater than or equal to 1.98 which is 2 in this case
ROW_NUMBER <= 2 So the last filter is this.
Query
SELECT
a.product_id
, a.price
FROM (
SELECT
t.product_id
, t.price
, (#ROW_NUMBER := #ROW_NUMBER + 1) AS ROW_NUMBER
FROM
t
CROSS JOIN (SELECT #ROW_NUMBER := 0) AS init_user_variable
ORDER BY
t.price ASC
) AS a
WHERE
ROW_NUMBER <= CEIL(((SELECT COUNT(*) FROM t) * 0.33));
Result
| product_id | price |
| ---------- | ----- |
| 1 | 10 |
| 2 | 50 |
see demo
To get get the max it's just as simple as adding ORDER BY a.price DESC LIMIT 1
Query
SELECT
a.product_id
, a.price
FROM (
SELECT
t.product_id
, t.price
, (#ROW_NUMBER := #ROW_NUMBER + 1) AS ROW_NUMBER
FROM
t
CROSS JOIN (SELECT #ROW_NUMBER := 0) AS init_user_variable
ORDER BY
t.price ASC
) AS a
WHERE
ROW_NUMBER <= CEIL(((SELECT COUNT(*) FROM t) * 0.33))
ORDER BY
a.price DESC
LIMIT 1;
Result
| product_id | price |
| ---------- | ----- |
| 2 | 50 |
see demo
If your version supports window functions, you can use NTILE(3) to divide the rows into three groups ordered by price. The first group will contain (about) "33%" of lowest prices. Then you just need to select the MAX value from that group:
with cte as (
select price, ntile(3) over (order by price) as ntl
from products
)
select max(price)
from cte
where ntl = 1
Demo
Prior to MySQL 8.0 I would use a temprary table with an AUTO_INCREMENT column:
create temporary table tmp (
rn int auto_increment primary key,
price decimal(10,2)
);
insert into tmp(price)
select price from products order by price;
set #max_rn = (select max(rn) from tmp);
select price
from tmp
where rn <= #max_rn / 3
order by rn desc
limit 1;
Demo

How to convert this query from MySQL to SQL Server?

Here is the source table:
+----+-------+
| Id | Score |
+----+-------+
| 1 | 3.50 |
| 2 | 3.65 |
| 3 | 4.00 |
| 4 | 3.85 |
| 5 | 4.00 |
| 6 | 3.65 |
+----+-------+
Here is the result table:
+-------+------+
| Score | Rank |
+-------+------+
| 4.00 | 1 |
| 4.00 | 1 |
| 3.85 | 2 |
| 3.65 | 3 |
| 3.65 | 3 |
| 3.50 | 4 |
+-------+------+
I have the MySQL version query, how to convert it to SQL server version? I tried to do the declare but I have no idea how to update the value of the variables.
SELECT Score, ranking AS Rank
FROM
(
SELECT
Score,
CASE
WHEN #dummy = Score
THEN #ranking := #ranking
ELSE #ranking := #ranking + 1
END as ranking,
#dummy := Score
FROM Scores, (SELECT #ranking := 0, #dummy := -1) init
ORDER BY Score DESC
)AS Result
Your code is a MySQL work-around for the ANSI standard DENSE_RANK() function (as explained by Sean Lange in a comment). The code simply looks like:
SELECT s.*, DENSE_RANK() OVER (ORDER BY Score DESC) as rank
FROM Scores s
ORDER BY Score DESC;
Incidentally, the MySQL code itself is not really accurate. The following is much safer:
SELECT Score, ranking AS Rank
FROM (SELECT Score,
(#rn := if(#dummy = Score, #rn,
if(#dummy := Score, #rn + 1, #rn + 1)
)
) as ranking
FROM Scores CROSS JOIN
(SELECT #rn := 0, #dummy := -1) init
ORDER BY Score DESC
) s;
The key difference is that all the assignments to variables occur in a single expression. MySQL does not guarantee the order of evaluations of expressions in a SELECT, so you should not use #dummy in one expression and then assign it in another.
SQL Server doesn't support variables used in that manner, so the syntax doesn't translate. Scalar variables are set once by a query, not set once for each row of the result set. MySQL's syntax here is a hack to get analytic-function-like behavior without analytic function support. You should just use:
SELECT Score,
DENSE_RANK() OVER(ORDER BY Score DESC) AS Rank
FROM Scores
ORDER BY Score DESC;
If you insist on not using DENSE_RANK(), you can use the SQL Server syntax from SQL Server 2000:
SELECT s1.Score,
(SELECT COUNT(DISTINCT s2.Score) FROM Scores s2 WHERE s1.Score <= s2.Score) AS Ranking
FROM Scores s1
ORDER BY s1.Score DESC;

Rank users in mysql by their points

I am trying to rank my students by their points that I've calculated before
but the problem is if students have same points they both should be in same rank
E.g
Student 1 has full points
Student 2 has full points
they both have to be rank as 1;
Here an example of my database
the query I am trying to do is (just for select then I can insert the values to my column)
SELECT a.points
count(b.points)+1 as rank
FROM examresults a left join examresults b on a.points>b.points
group by a.points;
Edit for being more clear:
Student 1 points 80
Student 2 points 77.5
Student 3 points 77.5
Student 4 points 77
their ranks should be like
Student 1 Rank 1
Student 2 Rank 2
Student 3 Rank 2
Student 4 Rank 3
my current query returns a values like
As it is missing the third rank. (because second rank has 2 values)
This is just a fix of Gordon solution using variables. The thing is your rank function isnt the way rank should work. (student 4 should be rank 4)
SQL Fiddle Demo You can add more student to improve the testing.
select er.*,
(#rank := if(#points = points,
#rank,
if(#points := points,
#rank + 1,
#rank + 1
)
)
) as ranking
from students er cross join
(select #rank := 0, #points := -1) params
order by points desc;
OUTPUT
| id | points | ranking |
|----|--------|---------|
| 1 | 80 | 1 |
| 2 | 78 | 2 |
| 3 | 78 | 2 |
| 4 | 77 | 3 |
| 5 | 66 | 4 |
| 6 | 66 | 4 |
| 7 | 66 | 4 |
| 8 | 15 | 5 |
You want a real rank, which is calculated by the ANSI standard rank() function. You can implement this in MySQL using this logic:
select er.*,
(select 1 + count(*)
from examresults er2
where er2.points > er.points
) as ranking
from exampleresults er;
For larger tables, you can do this with variables, but it is a rather awkward:
select er.*,
(#rank := if(#rn := #rn + 1 -- increment row number
if(#points = points, #rank, -- do not increment rank
if(#points := points, -- set #points
#rn, #rn -- otherwise use row number
)
)
)
) as ranking
from examresults er cross join
(select #rn := 0, #rank := 0, #points := -1) params
order by points desc;
this query achieve what do you want:
SELECT student_id , points, (select count(distinct(points))+1 as rank
from examresults internal
where internal.points > external.points order by points)
FROM examresults external
group by student_id

MySQL: Find max consecutive rows in a table based on value

Using MySQL, I am trying to find the highest number of consecutive rows in a table based on a value. For the sake of simplicity, my table looks like this:
+----+-------+
| ID | VALUE |
+----+-------+
| 1 | A |
| 2 | B |
| 3 | A |
| 4 | A |
| 5 | B |
| 6 | B |
| 7 | A |
| 8 | A |
| 9 | A |
| 10 | B |
+----+-------+
In this example, if I wanted the highest number of consecutive rows for 'A', I would get 3. For 'B', I would get 2. Even returning a result set of the counts of consecutive rows for 'A' would be preferable. I am newer to SQL so hints would be appreciated too. Any suggestions?
You can do it using variables:
SELECT VALUE, MAX(cnt) AS maxCount
FROM (
SELECT VALUE, COUNT(grp) AS cnt
FROM (
SELECT ID, VALUE, rn - rnByVal AS grp
FROM (
SELECT ID, VALUE,
#rn := #rn + 1 AS rn,
#rnByVal := IF (#val = VALUE,
IF (#val := VALUE, #rnByVal + 1, #rnByVal + 1),
IF (#val := VALUE, 1, 1)) AS rnByVal
FROM mytable
CROSS JOIN (SELECT #rn := 0, #rnByVal := 0, #val := '') AS vars
ORDER BY ID) AS t
) AS s
GROUP BY VALUE, grp ) AS u
GROUP BY VALUE
Variables #rn and #rnByVal are used in order to simulate ROW_NUMBER window function, currently not available in MySQL. The second variable (#rnByVal) performs a count over VALUE partitions.
Using #rn - #rnByVal in an outer query we can calculate grp field, which identifies islands of consecutive rows having the same VALUE. Performing a GROUP BY on VALUE, grp we can calculate the population of these islands and, finally, in the outermost query, get the max population per VALUE.
Demo here