I believe it can be solve by temp table/stored procedure but in case it can be done by single SQL statement.
Goal: List all row with count down by year, however number of row of each year is different. Row can be order by date
Result Arm to:
|-Count Down-|-Date-------|
| 3 | 2013-01-01 | <- Start with number of Row of each year
| 2 | 2013-03-15 |
| 1 | 2013-06-07 |
| 5 | 2014-01-01 | <- Start with number of Row of each year
| 4 | 2014-03-17 |
| 3 | 2014-07-11 |
| 2 | 2014-08-05 |
| 1 | 2014-11-12 |
SQL:
Select #row_number:=#row_number-1 AS CountDown, Date
FROM table JOIN
(Select #row_number:=COUNT(*), year(date) FROM table GROUP BY year(date))
Is there any solution for that?
The subquery that gets the count by year needs to return the year, so you can join it with the main table to get the starting number for the countdown. And you need to detect when the year changes, so you need another variable for that.
SELECT #row_number := IF(YEAR(d.Date) = #prevYear, #row_number-1, y.c) AS CountDown,
d.Date, #prevYear := YEAR(d.Date)
FROM (SELECT Date
FROM Table1
ORDER BY Date) AS d
JOIN
(Select count(*) AS c, year(date) AS year
FROM Table1
GROUP BY year(date)) AS y
ON YEAR(d.Date) = y.year
CROSS JOIN (SELECT #prevYear := NULL) AS x
DEMO
You can do the count down using variables (or correlated subqueries). The following does the count, but the returned data is not in the order you specify:
select (#rn := if(#y = year(date), #rn + 1,
if(#y := year(date), 1, 1)
)
) as CountDown, t1.*
from table1 cross join
(select #y := 0, #rn := 0) vars
order by date desc;
That is easily fixed with another subquery:
select t.*
from (select (#rn := if(#y = year(date), #rn + 1,
if(#y := year(date), 1, 1)
)
) as CountDown, t1.*
from table1 cross join
(select #y := 0, #rn := 0) vars
order by date desc
) t
order by date;
Note the complicated expression for assigning CountDown. This expression is setting both variables (#y and #rn) in a single expression. MySQL does not guarantee the order of evaluation of expressions in a select. If you assign these in different expressions, then they might be executed in the wrong order.
Related
I have a list of value in my column. And want to query the range.
Eg. If values are 1,2,3,4,5,9,11,12,13,14,17,18,19
I want to display
1-5,9,11-14,17-19
Assuming that each value is stored on a separate row, you can use some gaps-and-island technique here:
select case when min(val) <> max(val)
then concat(min(val), '-', max(val))
else min(val)
end val_range
from (select val, row_number() over(order by val) rn from mytable) t
group by val - rn
order by min(val)
The idea is to build groups of consecutive values by taking the difference between the value and an incrementing rank, which is computed using row_number() (available in MySQL 8.0):
Demo on DB Fiddle:
| val_range |
| :-------- |
| 1-5 |
| 9 |
| 11-14 |
| 17-19 |
In earlier versions, you can emulate row_number() with a correlated subquery, or a user variable. The second option goes like:
select case when min(val) <> max(val)
then concat(min(val), '-', max(val))
else min(val)
end val_range
from (select #rn := 0) x
cross join (
select val, #rn := #rn + 1 rn
from (select val from mytable order by val) t
) t
group by val - rn
order by min(val)
As a complement to other answers:
select dn.val as dnval, min(up.val) as upval
from mytable up
join mytable dn
on dn.val <= up.val
where not exists (select 1 from mytable a where a.val = up.val + 1)
and not exists (select 1 from mytable b where b.val = dn.val - 1)
group by dn.val
order by dn.val;
1 5
9 9
11 14
17 19
Needless to say, but using an OLAP function like #GNB does, is orders of magnitude more efficient.
A short article on how to mimic OLAP functions in MySQL < 8 can be found at:
mysql-row_number
Fiddle
EDIT:
If another dimension is introduced (in this case p), something like:
select dn.p, dn.val as dnval, min(up.val) as upval
from mytable up
join mytable dn
on dn.val <= up.val
and dn.p = up.p
where not exists (select 1 from mytable a where a.val = up.val + 1 and a.p = up.p)
and not exists (select 1 from mytable b where b.val = dn.val - 1 and b.p = dn.p)
group by dn.p, dn.val
order by dn.p, dn.val;
can be used, see Fiddle2
I have a query that calculates dense ranks based on the value of a column :
SELECT id,
score1,
FIND_IN_SET
(
score1,
(
SELECT GROUP_CONCAT(score1 ORDER BY score1 DESC) FROM scores
)
) as rank
FROM score_news;
This is what the query results look like:
+----+--------+------+
| id | score1 | rank |
+----+--------+------+
| 1 | 15 | 1 |
| 2 | 15 | 1 |
| 3 | 14 | 3 |
| 4 | 13 | 4 |
+----+--------+------+
The query takes Nx longer time when number of scores increases by N times. Is there any way I can optimize this ? My table size in the order of 106
NOTE: I have already tried a technique using mysql user variables but I get inconsistent results when I run it on a large set. On investigation I found this in the MySQL docs:
The order of evaluation for user variables is undefined and may change
based on the elements contained within a given query. In SELECT #a, #a
:= #a+1 ..., you might think that MySQL will evaluate #a first and
then do an assignment second, but changing the query (for example, by
adding a GROUP BY, HAVING, or ORDER BY clause) may change the order of
evaluation...The general rule is never to assign a value to a user
variable in one part of a statement and use the same variable in some
other part of the same statement. You might get the results you
expect, but this is not guaranteed.
My attempt with user variables :
SELECT
a.id,
#prev := #curr as prev,
#curr := a.score1 as curr,
#rank := IF(#rank = 0, #rank + 1, IF(#prev > #curr, #rank+#ties, #rank)) AS rank,
#ties := IF(#prev = #curr, #ties+1, 1) AS ties
FROM
scores a,
(
SELECT
#curr := null,
#prev := null,
#rank := 0,
#ties := 1,
#total := count(*)
FROM scores
WHERE score1 is not null
) b
WHERE
score1 is not null
ORDER BY
score1 DESC
)
The solution with variables could work, but you need to first order the result set, and only then work with the variable assignments:
SELECT a.id,
#rank := IF(#curr = a.score1, #rank, #rank + #ties) AS rank,
#ties := IF(#curr = a.score1, #ties + 1, 1) AS ties,
#curr := a.score1 AS curr
FROM (SELECT * FROM scores WHERE score1 is NOT NULL ORDER BY score1 DESC) a,
(SELECT #curr := null, #rank := 0, #ties := 1) b
NB: I placed the curr column last in the select clause to save one variable.
You can also use following query to get your dense rank without using user defined variables
SELECT
a.*,
(SELECT
COUNT(DISTINCT score1)
FROM
scores b
WHERE a.`score1` < b.score1) + 1 rank
FROM
scores a
ORDER BY score1 DESC
Demo
Demo using your data set
An index on score1 column might help you
I have a below table and wants to select only last 2 entries of all users.
Source table:
-------------------------------------
UserId | QuizId(AID)|quizendtime(AID)|
--------------------------------------
1 10 2016-5-12
2 10 2016-5-12
1 11 2016-6-12
2 12 2016-8-12
3 12 2016-8-12
2 13 2016-8-12
1 14 2016-9-12
3 14 2016-9-12
3 11 2016-6-12
Expected output is like, (should list only recent 2 quizid entries for all users)
-------------------------------------
UserId | QuizId(AID)|quizendtime(AID)|
--------------------------------------
1 14 2016-9-12
1 11 2016-6-12
2 13 2016-8-12
2 12 2016-8-12
3 14 2016-9-12
3 12 2016-8-12
Any idea's to produce this output.
Using MySQL user defined variables you can accomplish this:
SELECT
t.UserId,
t.`QuizId(AID)`,
t.`quizendtime(AID)`
FROM
(
SELECT
*,
IF(#sameUser = UserId, #a := #a + 1 , #a := 1) row_number,
#sameUser := UserId
FROM your_table
CROSS JOIN (SELECT #a := 1, #sameUser := 0) var
ORDER BY UserId , `quizendtime(AID)` DESC
) AS t
WHERE t.row_number <= 2
Working Demo
Note: If you want at most x number of entries for each user then change the condition in where clause like below:
WHERE t.row_number <= x
Explanation:
SELECT
*,
IF(#sameUser = UserId, #a := #a + 1 , #a := 1) row_number,
#sameUser := UserId
FROM your_table
CROSS JOIN (SELECT #a := 1, #sameUser := 0) var
ORDER BY UserId , `quizendtime(AID)` DESC;
This query sorts all the data in ascending order of userId and descending order of quizendtime(AID).
Now take a walk on this (multi) sorted data.
Every time you see a new userId assign a row_number (1). If you see the same user again then just increase the row_number.
Finally filtering only those records which are having row_number <= 2 ensures the at most two latest entries for each user.
EDIT: As Gordon pointed out that the evaluation of expressions using user defined variables in mysql is not guaranteed to follow the same order always so based on that the above query is slightly modified:
SELECT
t.UserId,
t.`QuizId(AID)`,
t.`quizendtime(AID)`
FROM
(
SELECT
*,
IF (
#sameUser = UserId,
#a := #a + 1,
IF(#sameUser := UserId, #a := 1, #a:= 1)
)AS row_number
FROM your_table
CROSS JOIN (SELECT #a := 1, #sameUser := 0) var
ORDER BY UserId , `quizendtime(AID)` DESC
) AS t
WHERE t.row_number <= 2;
WORKING DEMO V2
User-defined variables are the key to the solution. But, it is very important to have all the variable assignments in a single expression. MySQL does not guarantee the order of evaluation of expressions in a select -- and, in fact, sometimes processes them in different orders.
select t.*
from (select t.*,
(#rn := if(#u = UserId, #rn + 1,
if(#u := UserId, 1, 1)
)
) as rn
from t cross join
(select #u := -1, #rn := 0) params
order by UserId, quizendtime desc
) t
where rn <= 2;
Table has aggregated values but i need to return multiple rows if the value is greater than one.
Here is how the table looks now:
date description amount
1/1/2015 alpha 3
1/1/2015 beta 1
Here is how i need it to return:
date description amount
1/1/2015 alpha 1
1/1/2015 alpha 1
1/1/2015 alpha 1
1/1/2015 beta 1
Any help would be greatly appreciated.
You need a table of numbers. Something like this works for up to 3 and can be easily extended:
select t.date, t.description, 1 as amount
from table t join
(select 1 as n union all select 2 union all select 3) n
on n.n <= t.amount;
EDIT:
If you have enough rows in the table for the larger amounts, you can do:
select t.date, t.description, 1 as amount
from table t join
(select #rn := #rn + 1 as n
from table cross join (select #rn := 0) vars
) n
on n.n <= t.amount;
This worked perfectly.
select t.date, t.description, 1 as amount
from table t join
(select #rn := #rn + 1 as n
from table cross join (select #rn := 0) vars
) n
on n.n <= t.amount;
Hopefully the title makes any sense.
For this example I'll have the next table in my database
measurements
==================================
stn | date | temp | time =
1 | 01-12-2001 | 2.0 | 14:30 =
1 | 01-12-2001 | 2.1 | 14:31 =
1 | 03-12-2001 | 1.9 | 21:34 =
2 | 01-12-2001 | 4.5 | 12:48 =
2 | 01-12-2001 | 4.7 | 12:49 =
2 | 03-12-2001 | 4.9 | 11:01 =
==================================
And so on and so forth.
Each station (stn) has many measurements, one per day second. Now I want to select the temp of each station of the last 30 days measurements where the station has at least 30 temperature measurements.
I was playing with subquerys and group by, but I can't seem to figure it out.
Hope someone can help me out here.
edited the table
My example was oversimplified leaving a critical piece of information out. Please review the question.
select t1.stn,t1.date,t1.temp,t1.rn from (
select *,
#num := if(#stn = stn, #num + 1, 1) as rn,
#stn := stn as id_stn
from table,(select #stn := 0, #num := 1) as r
order by stn asc, date desc) as t1
inner join (select `stn`
from table
where concat_ws(' ',date,time) >= now() - interval 30 day
group by `stn`
having count(*) >= 30) as t
on t1.stn = t.stn
and t1.rn <= 30
order by stn,date desc,time desc
This is the query that should select Last 30 entries where there are at least 30 entries for a station
This query is based on the answer here by nick rulez, so please upvote him
SELECT t1.stn, t1.date, t1.temp, t1.time FROM
(
SELECT *,
#num := if(#stn = stn, #num + 1, 1) as rn,
#stn := stn as id_stn
FROM
`tablename`,
(SELECT #stn := 0, #num := 1) as r
ORDER BY stn asc, date desc
) as t1
INNER JOIN
(
SELECT `stn`
FROM `tablename`
GROUP BY `stn`
HAVING COUNT(*) >= 30
) as t
ON t1.stn = t.stn
AND t1.rn <= 30
ORDER BY stn, date desc, time desc
I have tested it on a sample database I made based on your schema and is working fine.
To know more about such queries have a look here Within-group quotas (Top N per group)
SELECT stn, date, temp FROM
(
SELECT stn, date, temp, #a:=IF(#lastStn=stn, #a+1, 1) countPerStn, #lastStn:=stn
FROM cache
GROUP BY stn, date
ORDER BY stn, date DESC
) as tempTable
WHERE countPerStn > 30;
Is the query I was looking for, sorry if my question was 'so wrong' that it pushed you all in the wrong direction. I'll up vote the answers who'm helped me to find the needed query.