How to get all rows with second highest value - mysql

I have this following table:
name value year
A 1 2015
A 2 2014
A 3 2013
B 1 2015
B 3 2013
C 1 2015
C 2 2014
How can I get, for each name, the row with the second highest year, like this:
name value year
A 2 2014
B 3 2013
C 2 2014
I tried the following query but no success:
select name, value, year
from TABLE_NAME
WHERE year IN (select year from TABLE_NAME order by year desc limit 1,1)
The previous query gives me this error:
"SQL Error (1235): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery' "
And I can't change the MySQL version (5.6.25) right now, because the solution is already in production.
Any help, please?

One way to solve n per group in MySQL is to simulate ROW_NUMBER. Note that this will only return one value per name.
SELECT
name,
value,
year
FROM
(SELECT
t.name,
t.value,
t.year,
#rn := if(#prev = t.name, #rn + 1,1) as rn,
#prev:=t.name
FROM
test_table as t
JOIN (SELECT #Prev:= Null, #Rn := 0) as v
ORDER BY
t.name,
T.year desc) as t
WHERE
rn = 2;
How this works.
SELECT #Prev:= Null, #Rn := 0 initializes two variables #Prev and #Rn.
#rn := if(#prev = t.name, #rn + 1,1) as rn set the variable of #rn to either 1 or #rn + 1 depending on if #prev = t.Name and returns the value of #rn as the column rn
#prev:=t.name sets the value of #prev equal to the current value of name
if you run
SELECT
t.name,
t.value,
t.year,
#prev = t.name as eval,
#rn := if(#prev = t.name, #rn + 1,1) as rn,
#prev:=t.name as prev
FROM
test_table as t
JOIN (SELECT #Prev:= Null, #Rn := 0) as v
ORDER BY
t.name,
T.year desc
I would expect something like
name value year eval rn prev
A 1 2015 false 1 null
A 2 2014 true 2 A
A 3 2013 true 3 A
B 1 2015 false 1 A
B 3 2013 true 2 B
C 1 2015 false 1 B
C 2 2014 true 2 C
Wrapping into a subquery and the filtering for rn=2 gives you the desired result

My strategy is to use a grouping to find the highest years. Then join with the original table to remove the highest years. Finally do a grouping on the combined table to find the second highest year for each name. (If you need value you can do an INNER JOIN with the original table to find it.)
SELECT name, MAX(year)
FROM
(SELECT name, year
FROM TABLE_NAME) AS x1
INNER JOIN
(SELECT name, MAX(year) AS year
FROM TABLE_NAME
GROUP BY name, year
) AS x2
ON x1.name = x2.name AND x1.year <> x2.year
GROUP BY name
ORDER BY name ASC ;

Try this :
select * from test_table where year = (select distinct year from test_table order by year desc limit 1,1)

It should work if you rewrite it as a join:
select b.name, b.value, b.year
from (select year
from table_name
order by year desc
limit 1, 1) a
join table_name b
on b.year = a.year

Related

How to get TopN query group by month MYSQL

There's a table like:
months contact COUNT
202007 asdas 45
202007 madhouse 1
202007 RORC YANG 1
202007 RORG 2
202007 ROR 5
202008 SARINA 1
202008 SMB 1
How can I get top 4 query result each month?
Expected result:
months contact COUNT
202007 asdas 45
202007 ROR 5
202007 RORG 2
202008 SARINA 1
202008 SMB 1
I'm working with mysql5.6
Here are 2 choices. The first uses rank() over() which does not guarantee only 4 rows per month (there could be more) and the second uses row_number() over() which will limit number of rows to a max of 4 per month
select
*
from (
select
* , rank() over(partition by months order by c desc) as cr
from (
select months, contact, count(*) as c
from mytable
group by months, contact
) as g
) as d
where cr <= 4
;
select
*
from (
select
* , row_number() over(partition by months order by c desc) as rn
from (
select months, contact, count(*) as c
from mytable
group by months, contact
) as g
) as d
where rn <= 4
;
see demo
for older MySQL try a row number hack:
select
*
from (
select
#row_num :=IF(#prev_value=g.months,#row_num+1,1)AS RowNumber
, g.months
, g.contact
, g.c
, #prev_value := g.months
from (
select months, contact, count(*) as c
from mytable
group by months, contact
) as g
CROSS JOIN (SELECT #row_num :=1, #prev_value :='') vars
ORDER BY g.months, g.contact
) as d
where RowNumber <= 4
see that in demo
TOP5
SELECT z.months, z.contact, z.count
FROM
(SELECT
x.*,
#rownum := #rownum + 1,
IF(#part = x.months,#r := #r + 1,#r := 1) AS rank,
#part := x.months
FROM
(
SELECT
*
FROM
my_table e
ORDER BY
e.months ASC,e.count DESC) X,
(
SELECT
#rownum := 0,
#part := NULL,
#r := 0) rt)z
WHERE z.rank <=5

MySQL SUM TOP 2 RECORDS FOR EACH CATEGORY

I have a table for students scores, am trying to sum top 2 marks for all student for a particular category.I have search for similar post but have not gotten correct answer
I have tried summing the marks but am only getting result for two students instead of all students and it does not give me correct value.
SELECT SUM(marks) as totalmarks,stdid
FROM (( select marks,stdid
from finalresult
where `subjectcategory` = 1
AND `classId`='3' AND `year`='2018'
AND `term`='2' AND `type`='23'
order by marks desc
LIMIT 2 ))t1
GROUP BY stdid
An auxiliary subquery might be used for iteration
SELECT
stdid, marks
FROM
(
SELECT stdid, marks,
#rn := IF(#iter = stdid, #rn + 1, 1) AS rn,
#iter := stdid
FROM finalresult
JOIN (SELECT #iter := NULL, #rn := 0) AS q_iter
WHERE `subjectcategory` = 1
AND `classId`='3'
AND `year`='2018'
AND `term`='2'
AND `type`='23'
ORDER BY stdid, marks DESC
) AS T1
WHERE rn <= 2
this solution ignores ties and takes only two for each Student ID.
Demo
In MySQL 8+, you would do:
SELECT stdid, SUM(marks) as totalmarks
FROM (SELECT fr.*,
ROW_NUMBER() OVER (PARTITION BY stdid ORDER BY marks DESC) as seqnm
FROM finalresult fr
WHERE subjectcategory = 1 AND
classId = 3 AND
year = 2018 AND
term = 2 AND
type = 23
) fr
WHERE seqnum <= 2
GROUP BY stdid;
Note that I removed the single quotes. Things that look like numbers probably are. And you should not mix type -- put the quotes back if the values really are stored as strings.
In earlier versions, probably the simplest method is to use variables, but you have to be very careful about them. MySQL does not guarantee the order of evaluation of variables in SELECT, so you cannot assign a variable in one expression and use it in another.
A complicated expression solves this. Also, it is best to sort in a subquery (the latest versions of MySQL 5+ require this):
SELECT stdid, SUM(marks) as totalmarks
FROM (SELECT fr.*,
(#rn := IF(#s = stdid, #rn + 1,
IF(#s := stdid, 1, 1)
)
) as seqnum
FROM (SELECT fr.*
FROM finalresult fr
WHERE subjectcategory = 1 AND
classId = 3 AND
year = 2018 AND
term = 2 AND
type = 23
ORDER BY stdid, marks DESC
) fr CROSS JOIN
(SELECT #s = '', #rn := 0) params
WHERE seqnum <= 2
GROUP BY stdid;

How to rank mysql with groupby

Guy I am trying to ranking some data from my database, and I notice that it's going very wrong when I put the group by clause;
SET #rank=0;
SELECT #rank:=#rank+1 AS RankSemGenero
,a.nome AS Artista
,f.nome AS Musica
,SUM(rnk.total) AS Tocadas
,rnk.mes AS Mes
,rnk.dia AS Dia
,current_timestamp() AS Criado_Em_Sem_Genero
,23 AS RankComGenero
,current_timestamp() AS Criado_Em_Com_Genero
/*,CASE rnk.categoria
WHEN 1 then 'AM'
WHEN 2 then 'FM'
WHEN 3 then 'Web'
WHEN 4 then 'Comunitaria'
END AS Categoria_Radio*/
,'Todas' AS TipoEmissora
,5 AS Relevancia_Emissora
,'Nacional' AS Local
,5 AS Relevancia_Local
,1 AS fl_ativo
FROM rnk201901 rnk
LEFT JOIN artistas a ON rnk.artista = a.id
LEFT JOIN fonogramas f ON rnk.fonograma = f.id
WHERE rnk.dia = 10
-- AND rnk.fonograma = 35876
-- GROUP BY rnk.fonograma
ORDER BY rnk.total DESC;
This code above bringing the information on the right way 1 until ....
But if I change the GROUP BY line, I am receiving something like: 1700 instead of 1.
GROUP BY rnk.fonograma
Any idea how to handle this group by counting 1 by 1?
Thanks!!
You need to use a subquery, when using variables with group by:
select (#rank := #rank + 1) as rank, t.*
from (<your aggregation query here with order by>) t cross join
(select #rank := 0) params;

Select the distinct latest 2 rows based on the timestamp column

I have a table like below. I want to extract the latest(based on time) 2 rows having same id. If no rows are same do not return anything. Then subtract the values of the latest row with the second latest and return a table with the ID and the value result.
Below is the table. 1st column is the id. Second is the value, third is the time. Id is not primary or unique
Id value time
3 2 2019-01-11 18:59:07.403
2 7 2019-01-10 18:58:40.400
4 5 2019-01-12 18:58:42.400
2 2 2019-01-11 18:59:23.147
5 -5 2019-01-12 18:58:42.400
3 8 2019-01-12 18:59:27.670
2 5 2019-01-12 18:59:43.777
The result should be
id value
2 3
3 6
One possible solution uses aggregation to get the IDs which occur more than once and correlated subqueries with ORDER BY and LIMIT to get the latest and second latest value.
SELECT x.id,
(SELECT t.value
FROM elbat t
WHERE t.id = x.id
ORDER BY t.time DESC
LIMIT 0, 1)
-
(SELECT t.value
FROM elbat t
WHERE t.id = x.id
ORDER BY t.time DESC
LIMIT 1, 1) value
FROM (SELECT t.id
FROM elbat t
GROUP BY t.id
HAVING count(*) > 1) x;
db<>fiddle
In MySQL 8+, you can use window functions and conditional aggregation
select t.id,
sum(case when seqnum = 1 then value else - value end) as diff
from (select t.*,
row_number() over (partition by id order by time desc) as seqnum
from elbat t
) t
where seqnum in (1, 2)
group by id
order by max(time) desc
limit 2;
The same idea can be adapted to earlier versions, using variables:
select t.id,
sum(case when seqnum = 1 then value else - value end) as diff
from (select t.*,
(#rn := if(#i = id, #rn + 1,
if(#i := id, 1, 1)
)
) as seqnum
from (select t.* from elbat t order by id, time desc) t cross join
(select #i := -1, #rn := 0) params
) t
where seqnum in (1, 2)
group by id
order by max(time) desc
limit 2;

MYSQL - Total registrations per day

I have the following structure in my user table:
id(INT) registered(DATETIME)
1 2016-04-01 23:23:01
2 2016-04-02 03:23:02
3 2016-04-02 05:23:03
4 2016-04-03 04:04:04
I want to get the total (accumulated) user count per day, for all days in DB
So result should be something like
day total
2016-04-01 1
2016-04-02 3
2016-04-03 4
I tried some sub querying, but somehow i have now idea how to achieve this with possibly 1 SQL statement. Of course if could group by per day count and add them programmatically, but i don't want to do that if possible.
You can use a GROUP BY that does all the counts, without the need of doing anything programmatically, please have a look at this query:
select
d.dt,
count(*) as total
from
(select distinct date(registered) dt from table1) d inner join
table1 r on d.dt>=date(r.registered)
group by
d.dt
order by
d.dt
the first subquery returns all distinct dates, then we can join all dates with all previous registrations, and do the counts, all in one query.
An alternative join condition that can give some improvements in performance is:
on d.dt + interval 1 day > r.registered
Not sure why not just use GROUP BY, without it this thing will be more complicated, anyway, try this;)
select
date_format(main.registered, '%Y-%m-%d') as `day`,
main.total
from (
select
table1.*,
#cnt := #cnt + 1 as total
from table1
cross join (select #cnt := 0) t
) main
inner join (
select
a.*,
if(#param = date_format(registered, '%Y-%m-%d'), #rowno := #rowno + 1 ,#rowno := 1) as rowno,
#param := date_format(registered, '%Y-%m-%d')
from (select * from table1 order by registered desc) a
cross join (select #param := null, #rowno := 0) tmp
having rowno = 1
) sub on main.id = sub.id
SQLFiddle DEMO