Determine growth in value in a specific time range - mysql

Given the following data set example, how should I structure my SQL query in order to determine if the value has grown over time (given a time range in the query) for a specific UserId by returning either a positive/negative growth percentage result or a true/false result
UserId
timestamp
value
1
1617711825
350
1
1617711829
400
1
1617711830
450
5
1617711831
560
Given the above example, we can observe that the value for UserId=1 has grown by a certain percentage.
The expected result would be:
UserId
growthPercentage
hasValueIncreased
1
50%
1

You can get the first and last values and then do whatever calculation you like. One method is:
select userId, value_first, value_last,
(value_first < value_last) as is_growing,
100 * ((value_last / value_first) - 1) as increase_percentage
from (select t.*,
first_value(value) over (partition by userId order by timestamp) as value_first,
first_value(value) over (partition by userId order by timestamp desc) as value_last
from t
) t
group by userId, value_first, value_last;

Schema and insert statements:
create table mytable(UserId int,timestamps timestamp,value int);
insert into mytable values(1, FROM_UNIXTIME(1617711825), 350);
insert into mytable values(1, FROM_UNIXTIME(1617711829), 400);
insert into mytable values(1, FROM_UNIXTIME(1617711830), 450);
insert into mytable values(5, FROM_UNIXTIME(1617711831), 560);
Query:
select usermin.userid, 100*(usermax.value-usermin.value)/usermin.value as growthPercentage,
(case when usermax.value>usermin.value then 1 else 0 end)hasValueIncreased
from
(SELECT USERID, VALUE FROM mytable where userid=1
order by timestamps
limit 1) usermin
inner join
(SELECT USERID, VALUE FROM mytable where userid=1
order by timestamps desc
limit 1) usermax
on usermin.userid=usermax.userid
Output:
|userid | growthPercentage | hasValueIncreased
|-----: | ---------------: | ----------------:
| 1 | 28.5714 | 1
db<>fiddle here

Related

How to select column based on the MAX value when doing GROUP BY on different value in MySQL

I have a MySQL table tbl below:
now I run the following query:
SELECT DAY_NUMBER, WEEK_NUMBER, MAX(total_volume)
FROM `tbl`
GROUP BY WEEK_NUMBER
I get the following output:
Everything looks good except for the DAY_NUMBER. The DAY_NUMBER does not reflect the TOTAL_VOLUME from which I query the MAX(TOTAL_VOLUME). If you look at WEEK_NUMBER 31, the MAX(TOTAL_VOLUME) 777 has DAY_NUMBER 7 but the query has 5.
My goal is to have DAY_NUMBER reflect MAX(TOTAL_VOLUME) value DAY_NUMBER.
could you please tell me how do I fix it?
With NOT EXISTS:
SELECT t.*
FROM tbl t
WHERE NOT EXISTS (
SELECT 1 FROM tbl
WHERE WEEK_NUMBER = t.WEEK_NUMBER AND total_volume > t.total_volume
)
Use like this,
SELECT DAY_NUMBER, WEEK_NUMBER, total_volume
FROM `tbl` WHERE total_volume IN(SELECT MAX(total_volume)
FROM `tbl`
GROUP BY WEEK_NUMBER)
You will get Desired output
Do not use GROUP BY for a query where you should be filtering.
I recommend a correlated subquery:
select t.*
from tbl t
where t.total_volume = (select max(t2.total_volume)
from tbl t2
where t2.week_number = t.week_number
);
Alternatively, you can use window functions in MySQL 8+:
select t.*
from (select t.*,
row_number() over (partition by week_number order by volume desc) as seqnum
from tbl t
) t
where seqnum = 1;
With join
drop table if exists t;
create table t
(day_number int, week_number int, value int);
insert into t values
(1,1,10),(2,1,20),
(10,2,20),(11,2,10);
select t.day_number,t.week_number,t.value
from t
join
(select week_number,max(value) m
from t
group by week_number
) w on w.week_number = t.week_number and w.m = t.value;
+------------+-------------+-------+
| day_number | week_number | value |
+------------+-------------+-------+
| 2 | 1 | 20 |
| 10 | 2 | 20 |
+------------+-------------+-------+
2 rows in set (0.00 sec)
In the event of a draw you will get all days which match the max value.

MySQL - Find MAX of grouped SUM (without LIMIT)

I would like to get the user_id and the sum of amount for the users who have largest summed amount. I cannot use LIMIT because that will return only 1 record (summed amount may be same for multiple users)
Here is my data schema and some records
CREATE TABLE transactions (
id BIGINT(20) NOT NULL AUTO_INCREMENT,
user_id BIGINT(20) NOT NULL,
amount FLOAT NOT NULL, PRIMARY KEY (id)
);
INSERT INTO transactions (user_id, amount) VALUES
(1, 1000),
(1, 1000),
(1, 1000),
(2, 2000),
(2, 1000),
(3, 1000);
Here are the expected result.
+---------+------+
| user_id | sum |
+---------+------+
| 1 | 3000 |
| 2 | 3000 |
+---------+------+
I can get the above result by using the following sql. However, I don't know is there any better approach or not. Is it necessary to repeat the same subquery twice? Thanks.
SELECT T1.user_id, T1.sum
FROM (
SELECT user_id, SUM(amount) as sum
FROM transactions
GROUP BY user_id
) T1
WHERE T1.sum = (
SELECT MAX(T2.sum)
FROM (
SELECT user_id, SUM(amount) as sum
FROM transactions
GROUP BY user_id
) T2
)
GROUP BY T1.user_id;
Well you can simplify your query to
SELECT user_id, SUM(amount) as sum
FROM transactions
GROUP BY user_id
HAVING SUM(amount) = (
SELECT SUM(amount) as sum
FROM transactions
GROUP BY user_id
ORDER BY SUM(amount) DESC
LIMIT 1
)

Ho to assign Previous value in column for each record

I have one table scenario in which data looks like this .
Request Id Field Id Current Key
1213 11 1001
1213 12 1002
1213 12 103
1214 13 799
1214 13 899
1214 13 7
In this when loop starts for first Request ID then it should check all the field ID for that particular request ID. then data should be look like this .
Request Id Field Id Previous Key Current Key
1213 11 null 1001
1213 12 null 1002
1213 12 1002 103
1214 13 null 799
1214 13 799 899
1214 13 899 7
When very first record for Field id for particular request id come then for it should be take null values in Previous key column and the current key will remain the same.
When the second record will come for same field ID its should take previous value of first record in Previous key column and when third record come it should take previous value of second record in Previous column and so on .
When the new field ID came the same thing should be repeated again.
Please let me know if you need any more info.Much needed your help.
You can check this.
Declare #t table (Request_Id int, Field_Id int, Current_Key int)
insert into #t values (1213, 11, 1001),(1213, 12, 1002), (1213, 12, 103) , (1214, 13, 799), (1214, 13, 899), (1214, 13, 7)
;with cte
as (
select 0 rowno,0 Request_Id, 0 Field_Id, 0 Current_Key
union
select ROW_NUMBER() over(order by request_id) rowno, * from #t
)
select
t1.Request_Id , t1.Field_Id ,
case when t1.Request_Id = t2.Request_Id and t1.Field_Id = t2.Field_Id
then t2.Current_Key
else null
end previous_key
, t1.Current_Key
from cte t1, cte t2
where t1.rowno = t2.rowno + 1
Refer link when you want to compare row value
When the second record will come for same field ID...
Tables don't work this way: there is no way to tell that 1213,12,1002 is the "previous" record of 1213,12,103 as you assume in your example.
Do you have any data you can use to sort your records properly? Request id isn't enough because, even if you guarantee that it increments monotonically for each operation, each operation can include multiple values for the same item id which need to be sorted relative to each other.
IN SQL 2008
You do not have the benefit of the lead and lag functions. Instead you must do a query for the new column. Make sure you query both tables in the same order, and add a row_num column. Then select the greatest row_num that is not equal to the current row_num and has the same request_id and field_id.
select a.request_id,
a.field_id,
(select x.current_key
from (select * from (select t.*, RowNumber() as row_num from your_table t) order by row_num desc) x
where x.request_id = a.request_id
and x.field_id = a.field_id
and x.row_num < a.row_num
and RowNumber()= 1
) as previous_key,
a.current_key
from (select t.*, RowNumber()as row_num from your_table t) a
IN SQL 2012+
You can use the LAG or LEAD functions with the OVER clause to get the previous or next nth row value:
select
Request_Id,
Field_Id,
lag(Current_Key,1) over (partition by Request_ID, Field_ID) as Previous_Key
,Current_Key
from your table
You should probably look at how you order your results too. If you have multiple results lag will only grab the next row in the default order of the table. If you had another column to order by such as a date time you could do the following:
lag(Current_Key,1) over (partition by Request_ID, Field_ID order by timestampColumn)
try this,
declare #tb table (RequestId int,FieldId int, CurrentKey int)
insert into #tb (RequestId,FieldId,CurrentKey) values
(1213,11,1001),
(1213,12,1002),
(1213,12,103),
(1214,13,799),
(1214,13,899),
(1214,13, 7)
select RequestId,t.FieldId,
case when t.FieldId=t1.FieldId then t1.CurrentKey end as PreviousKey,t.CurrentKey from
(select *, ROW_NUMBER() over (order by RequestId,FieldId) as rno
from #tb) t left join
(select FieldId,CurrentKey,
ROW_NUMBER() over (order by RequestId,FieldId) as rno from #tb) t1 on t.rno=t1.rno+1

Is there a simpler way to find MODE(S) of some values in MySQL

MODE is the value that occurs the MOST times in the data, there can be ONE MODE or MANY MODES
here's some values in two tables (sqlFiddle)
create table t100(id int auto_increment primary key, value int);
create table t200(id int auto_increment primary key, value int);
insert into t100(value) values (1),
(2),(2),(2),
(3),(3),
(4);
insert into t200(value) values (1),
(2),(2),(2),
(3),(3),
(4),(4),(4);
right now, to get the MODE(S) returned as comma separated list, I run the below query for table t100
SELECT GROUP_CONCAT(value) as modes,occurs
FROM
(SELECT value,occurs FROM
(SELECT value,count(*) as occurs
FROM
T100
GROUP BY value)T1,
(SELECT max(occurs) as maxoccurs FROM
(SELECT value,count(*) as occurs
FROM
T100
GROUP BY value)T2
)T3
WHERE T1.occurs = T3.maxoccurs)T4
GROUP BY occurs;
and the below query for table t200 (same query just with table name changed) I have 2 tables in this example because to show that it works for cases where there's 1 MODE and where there are multiple MODES.
SELECT GROUP_CONCAT(value) as modes,occurs
FROM
(SELECT value,occurs FROM
(SELECT value,count(*) as occurs
FROM
T200
GROUP BY value)T1,
(SELECT max(occurs) as maxoccurs FROM
(SELECT value,count(*) as occurs
FROM
T200
GROUP BY value)T2
)T3
WHERE T1.occurs = T3.maxoccurs)T4
GROUP BY occurs;
My question is "Is there a simpler way?"
I was thinking like using HAVING count(*) = max(count(*)) or something similar to get rid of the extra join but couldn't get HAVING to return the result i wanted.
UPDATED:
as suggested by #zneak, I can simplify T3 like below:
SELECT GROUP_CONCAT(value) as modes,occurs
FROM
(SELECT value,occurs FROM
(SELECT value,count(*) as occurs
FROM
T200
GROUP BY value)T1,
(SELECT count(*) as maxoccurs
FROM
T200
GROUP BY value
ORDER BY count(*) DESC
LIMIT 1
)T3
WHERE T1.occurs = T3.maxoccurs)T4
GROUP BY occurs;
Now is there a way to get ride of T3 altogether?
I tried this but it returns no rows for some reason
SELECT value,occurs FROM
(SELECT value,count(*) as occurs
FROM t200
GROUP BY `value`)T1
HAVING occurs=max(occurs)
basically I am wondering if there's a way to do it such that I only need to specify t100 or t200 once.
UPDATED: i found a way to specify t100 or t200 only once by adding a variable to set my own maxoccurs like below
SELECT GROUP_CONCAT(CASE WHEN occurs=#maxoccurs THEN value ELSE NULL END) as modes
FROM
(SELECT value,occurs,#maxoccurs:=GREATEST(#maxoccurs,occurs) as maxoccurs
FROM (SELECT value,count(*) as occurs
FROM t200
GROUP BY `value`)T1,(SELECT #maxoccurs:=0)mo
)T2
You are very close with the last query. The following finds one mode:
SELECT value, occurs
FROM (SELECT value,count(*) as occurs
FROM t200
GROUP BY `value`
LIMIT 1
) T1
I think your question was about multiple modes, though:
SELECT value, occurs
FROM (SELECT value, count(*) as occurs
FROM t200
GROUP BY `value`
) T1
WHERE occurs = (select max(occurs)
from (select `value`, count(*) as occurs
from t200
group by `value`
) t
);
EDIT:
This is much easier in almost any other database. MySQL supports neither with nor window/analytic functions.
Your query (shown below) does not do what you think it is doing:
SELECT value, occurs
FROM (SELECT value, count(*) as occurs
FROM t200
GROUP BY `value`
) T1
HAVING occurs = max(occurs) ;
The final having clause refers to the variable occurs but does use max(occurs). Because of the use of max(occurs) this is an aggregation query that returns one row, summarizing all rows from the subquery.
The variable occurs is not using for grouping. So, what value does MySQL use? It uses an arbitrary value from one of the rows in the subquery. This arbitrary value might match, or it might not. But, the value only comes from one row. There is no iteration over it.
I realize this is a very old question but in looking for the best way to find the MODE in a MySQL table, I came up with this:
SELECT [column name], count(*) as [ccount] FROM [table] WHERE [field] = [item] GROUP BY [column name] ORDER BY [ccount] DESC LIMIT 1 ;
In my actual situation, I had a log with recorded events in it. I wanted to know during which period (1, 2 or 3 as recorded in my log) the specific event occurred the most number of times. (Eg, the MODE of "period" column of the table for that specific event
My table looked like this (abridged):
EVENT_TYPE | PERIOD
-------------------------
1 | 3
1 | 3
1 | 3
1 | 2
2 | 1
2 | 1
2 | 1
2 | 3
Using the query:
SELECT event_type, period, count(*) as pcount FROM proto_log WHERE event_type = 1 GROUP BY period ORDER BY pcount DESC LIMIT 1 ;
I get the result:
> EVENT_TYPE | PERIOD | PCOUNT
> --------------------------------------
1 | 3 | 3
Using this result, the period column ($result['period'] for example) should contain the MODE for that query and of course pcount contains the actual count.
If you wanted to get multiple modes, I suppse you could keep adding other criteria to your WHERE clause using ORs:
SELECT event_type, period, count(*) as pcount FROM proto_log WHERE event_type = 1 ***OR event_type = 2*** GROUP BY period ORDER BY pcount DESC LIMIT 2 ;
The multiple ORs should give you the additional results and the LIMIT increase will add the additional MODES to the results. (Otherwise it will still only show the top 1 result)
Results:
EVENT_TYPE | PERIOD | PCOUNT
--------------------------------------
1 | 3 | 3
2 | 1 | 3
I am not 100% sure this is doing exactly what I think it is doing, or if it will work in all situations, so please let me know if I am on or off track here.

Finding a users maximum score and the associated details

I have a table in which users store scores and other information about said score (for example notes on score, or time taken etc). I want a mysql query that finds each users personal best score and it's associated notes and time etc.
What I have tried to use is something like this:
SELECT *, MAX(score) FROM table GROUP BY (user)
The problem with this is that whilst you can extra the users personal best from that query [MAX(score)], the returned notes and times etc are not associated with the maximum score, but a different score (specifically the one contained in *). Is there a way I can write a query that selects what I want? Or will I have to do it manually in PhP?
I'm assuming that you only want one result per player, even if they have scored the same maximum score more than once. I am also assuming that you want each player's first time that they got their personal best in the case that there are repeats.
There's a few ways of doing this. Here's a way that is MySQL specific:
SELECT user, scoredate, score, notes FROM (
SELECT *, #prev <> user AS is_best, #prev := user
FROM table1, (SELECT #prev := -1) AS vars
ORDER BY user, score DESC, scoredate
) AS T1
WHERE is_best
Here's a more general way that uses ordinary SQL:
SELECT T3.* FROM table1 AS T3
JOIN (
SELECT T1.user, T1.score, MIN(scoredate) AS scoredate
FROM table1 AS T1
JOIN (SELECT user, MAX(score) AS score FROM table1 GROUP BY user) AS T2
ON T1.user = T2.user AND T1.score = T2.score
GROUP BY T1.user
) AS T4
ON T3.user = T4.user AND T3.score = T4.score AND T3.scoredate = T4.scoredate
Result:
1, '2010-01-01 17:00:00', 50, 'Much better'
2, '2010-01-01 14:00:00', 100, 'Perfect score'
Test data I used to test this:
CREATE TABLE table1 (user INT NOT NULL, scoredate DATETIME NOT NULL, score INT NOT NULL, notes NVARCHAR(100) NOT NULL);
INSERT INTO table1 (user, scoredate, score, notes) VALUES
(1, '2010-01-01 12:00:00', 10, 'First attempt'),
(1, '2010-01-01 17:00:00', 50, 'Much better'),
(1, '2010-01-01 22:00:00', 30, 'Time for bed'),
(2, '2010-01-01 14:00:00', 100, 'Perfect score'),
(2, '2010-01-01 16:00:00', 100, 'This is too easy');
You can join with a sub query, as in the following example:
SELECT t.*,
sub_t.max_score
FROM table t
JOIN (SELECT MAX(score) as max_score,
user
FROM table
GROUP BY user) sub_t ON (sub_t.user = t.user AND
sub_t.max_score = t.score);
The above query can be explained as follows. It starts with:
SELECT t.* FROM table t;
... This by itself will obviously list all the contents of the table. The goal is to keep only the rows that represent a maximum score of a particular user. Therefore if we had the data below:
+------------------------+
| user | score | notes |
+------+-------+---------+
| 1 | 10 | note a |
| 1 | 15 | note b |
| 1 | 20 | note c |
| 2 | 8 | note d |
| 2 | 12 | note e |
| 2 | 5 | note f |
+------+-------+---------+
...We would have wanted to keep just the "note c" and "note e" rows.
To find the rows that we want to keep, we can simply use:
SELECT MAX(score), user FROM table GROUP BY user;
Note that we cannot get the notes attribute from the above query, because as you had already noticed, you would not get the expected results for fields not aggregated with an aggregate function, like MAX() or not part of the GROUP BY clause. For further reading on this topic, you may want to check:
Debunking GROUP BY Myths
How does MySQL decide which id to return in group by clause?
Why does MySql allow “group by” queries WITHOUT aggregate functions?
Now we only need to keep the rows from the first query that match the second query. We can do this with an INNER JOIN:
...
JOIN (SELECT MAX(score) as max_score,
user
FROM table
GROUP BY user) sub_t ON (sub_t.user = t.user AND
sub_t.max_score = t.score);
The sub query is given the name sub_t. It is the set of all the users with the personal best score. The ON clause of the JOIN applies the restriction to the relevant fields. Remember that we only want to keep rows that are part of this subquery.
SELECT *
FROM table t
ORDER BY t.score DESC
GROUP BY t.user
LIMIT 1
Side note: It is better to specify the fields than use SELECT *