rank() function SQL - mysql

I want to rank the following Scores table where same scores will have same rank.
+----+-------+
| Id | Score |
+----+-------+
| 1 | 3.50 |
| 2 | 4.00 |
| 3 | 4.00 |
| 4 | 3.50 |
+----+-------+
Can someone help why is the following query throwing a syntax error ? Also, is the logic incorrect?
Code:
select Score, RANK() OVER (order by Score desc) as rank
from Scores
order by Score desc;
Error message:
Line 1: SyntaxError: near '(order by Score desc) as rank
from Scores
order by Score desc'

E.g.: (And assuming you want ranks of 1 and 2 rather than 1 and 3)
SELECT x.*
, CASE WHEN #prev = score THEN #i:=#i ELSE #i:=#i+1 END rank
, #prev:=score
FROM my_table x
, (SELECT #prev:=null,#i:=0) vars
ORDER
BY score DESC
, id;
For sports ranking, you can do this:
SELECT a.*
, FIND_IN_SET(score,
(SELECT GROUP_CONCAT(score ORDER BY score DESC)
-- inclusion of DISTINCT here will output as above
FROM my_table)
) x
FROM my_table a;
But there are certain caveats associated with this solution

Related

Get column after group by lastest date

Please help me to get table 2 from table 1
A simple summarisation can be achieved using GROUP BY
select
code1
, max(code2) as code2
, count(*) as times
, max(`date`) as max_dt
from table1
group by
code1
However the result is different to the image:
+-------+-------+-------+------------+
| code1 | code2 | times | max_dt |
+-------+-------+-------+------------+
| 1 | D | 4 | 2020-02-21 |
| 2 | NNNN | 2 | 2021-01-21 |
+-------+-------+-------+------------+
Note:
The maximum of code2 may not be what you expect it to be. e.g. "D" is after "BBBBB" as the data is alphabetical. not based on length of string.
maximum of date for code1 isn't 16/2
I would not recommend naming any column "date" as it is usually a reserved word and can cause difficulties when developing queries.
for MySQL prior to version 8 a technique to get "the most recent" row may be used as follows:
SELECT code1, code2, `date`
, (select count(*) from mytable t2 where d.code1 = t2.code1) as times
FROM (
SELECT
#row_num :=IF(#prev_value = t.code1, #row_num + 1, 1) AS rn
, t.code1
, t.code2
, t.`date`
, #prev_value := t.code1
FROM mytable t
CROSS JOIN (SELECT #row_num :=1, #prev_value :='') vars
ORDER BY
t.code1
, t.`date` DESC
) as d
WHERE rn = 1
;
Or, for MySQL version 8 or later it is possible to use a much simpler query as there are windowing functions available through the over() clause:
SELECT code1, code2, `date`, times
FROM (
SELECT
row_number() over(partition by t.code1
order by t.`date` DESC) AS rn
, t.code1
, t.code2
, t.`date`
, count(*) over(partition by t.code1) as times
FROM mytable t
) as d
WHERE rn = 1
;
The results of the 2nd and 3rd queries are the same:
+-------+-------+------------+-------+
| code1 | code2 | date | times |
+-------+-------+------------+-------+
| 1 | D | 2020-02-21 | 4 |
| 2 | NNNN | 2021-01-21 | 2 |
+-------+-------+------------+-------+
solution demonstrated at db<>fiddle here

Group by ordered date

I have the following simple table:
id | patient_id | case_number | created_at
1 | 1 | x | 2021-02-25 10:57:24
2 | 1 | y | 2021-02-25 10:59:24
3 | 2 | z | 2021-02-25 10:57:14
4 | 2 | w | 2021-02-25 10:57:29
I want to get for each patient_id, its most recent case_number.
Meaning, final result of sql query I want is:
patient_id | case_number
1 | y
2 | w
This is what I've been trying:
SELECT *
FROM (SELECT patient_id, case_number FROM my_table ORDER BY created_at DESC) AS TEMP
GROUP BY patient_id
But this state returns:
patient_id | case_number
1 | x
2 | z
How to fix it?
If your mysql version didn't support Row_number window function, You can try to use self join
SELECT t1.patient_id ,t2.case_number
FROM (
SELECT MAX(created_at) latestDate,
patient_id
FROM my_table
GROUP BY patient_id
) t1 INNER JOIN my_table t2
ON t1.patient_id = t2.patient_id AND t1.latestDate = t2.created_at
From your comment, your MySQL version might be supporting ROW_NUMBER window function, you can use that to get the number which is the most recent date.
SELECT t1.patient_id,t1.case_number
FROM (
SELECT patient_id,
case_number,
ROW_NUMBER() OVER(PARTITION BY patient_id ORDER BY created_at DESC) rn
FROM my_table
) t1
WHERE rn = 1
Use window function FIRST_VALUE():
SELECT DISTINT patient_id,
FIRST_VALUE(case_number) OVER (PARTITION BY patient_id ORDER BY created_at DESC) case_number
FROM my_table
try this instead but still need to select created_time:
select distinct patient_id,case_number,time(created_time) from patients order by time(created_time) desc limit 2;

Overall Score Based on Top Three Score

I have a table with this data
+------+-----+————-----+
|Props |Score|Type |
+------+-----+-------- +
|1.EY | 30|Core
|2.FG | 29|Core
|2.YUE | 29|Core
|3.VB. | 28|Elective
|4.RX. | 67|Elective
|5.XE. | 89|Elective
|6.TF. | 60|Elective
|7.HK | 76|Elective
|8.ER | 58|Elective
I want to calculate the overall score by adding all Core scores plus any of the three best Elective scores. I cant seem to find my way around it.
Expected Results is 320: 3 Core Score + 3 best Elective
i.e(30+29+29)+(89+76+67)
Use UNION ALL for the 2 cases of scores and then sum over the returned scores:
select sum(t.score) totalscore
from (
select score from tablename
where type = 'Core'
union all
select t.score from (
select score from tablename
where type = 'Elective'
order by score desc
limit 3
) t
) t
See the demo.
Result:
| totalscore |
| ---------- |
| 320 |
You could rank the records by type within type partitions in an inner query and do a conditional sum in the outer query, like:
SELECT
SUM(CASE
WHEN type = 'Core' THEN score
WHEN type = 'Elective' AND rn <= 3 THEN score
ELSE 0
END) res
FROM (
SELECT
t.*,
ROW_NUMBER() OVER(PARTITION BY type ORDER BY score DESC) rn
FROM mytable t
) x
Demo on DB Fiddle with your sample data:
| res |
| --- |
| 320 |

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;