Get growth rate per month from transaction table - mysql

I have a transaction history like this:
date
revenue
balance
2021-05-03
0
1000
2021-05-21
500
1500
2021-05-23
-250
1250
2021-06-02
-500
750
and I would like to get a result like this:
date
growth
2021-5
0.25
2021-6
-0.4
The formula is:
balance (end of month) - balance (start of month) / balance (start of month)
e.g.: 1250-1000/1000=0.25
and: 750-1250/1250=-0.4
I would very much appreciate a hint for a MYSQL query that is as simple as possible.

You need to pull in the balance from the previous month, if it exists. For that, you can combine lag() with conditional aggregation:
select year(date), month(date),
(-1 +
(max(case when seqnum_desc = 1 then balance end) /
max(case when seqnum_asc = 1 then coalesce(prev_balance, balance) end)
)
) as growth
from (select t.*,
row_number() over (partition by year(date), month(date) order by date) as seqnum_asc,
row_number() over (partition by year(date), month(date) order by date desc) as seqnum_desc,
lag(balance) over (order by date) as prev_balance
from t
) t
group by year(date), month(date);
Here is a db<>fiddle.

#MBauerDC ... Thank You. In my case this was the right direction as I work with MYSQL 5.7. However, a few changes were still necessary to get to the final result:
SELECT
t0.month,
(t2.balance - (t1.balance - t1.revenue)) / (t1.balance - t1.revenue) AS growth
FROM
(SELECT
DATE_FORMAT(date, '%Y-%m') AS 'month',
MIN(date) AS 'min_date',
MAX(date) AS 'max_date'
FROM
t
GROUP BY month) AS t0
JOIN
t AS t1 ON (t1.date = min_date)
JOIN
t AS t2 ON (t2.date = max_date)

The logic of this calculation isn't consistent by your example.
Let's take "start of month" and "end of month" as the record with the first date in the month and the record with the last date in the month.
By this calculation, you do arrive at "0.25" for month 05, but for month 06, there is only one record, so its both the first and last record of the month, and the calculation is (750-750)/750, which is zero.
In your example calculation, you take the end of the last month to calculate the value for month 06, but take the end and start of month 05 for the calculation of the growth in month 05. These are two different calculations! You'll have to decide which one to use.
If you want to use the first and last record in a given month (as you do for month 05), you can use this:
SELECT
`t0`.`month`,
IF(`t1`.balance = 0,
NULL,
((`t2`.balance - `t1`.balance) / `t1`.balance)) AS `growth`
FROM
(SELECT
DATE_FORMAT(`date`, '%Y-%m') AS 'month',
MIN(`date`) AS 'min_date',
MAX(`date`) AS 'max_date'
FROM
`your_table`
GROUP BY MONTH(`date`)) AS `t0`
JOIN
`your_table` AS `t1` ON (`t1`.date = `min_date`)
JOIN
`your_table` AS `t2` ON (`t2`.date = `max_date`);
Note the "IF" because you have to guard against dividing by zero - a growth from zero to any positive value is always "infinity percent", which makes no sense to use - so you have to know how the things you're trying to build should work in these cases.

Provided that the DB version is 8.0, then you can use analytic functions as in the following query
SELECT month,
(bal_end - COALESCE(LAG(bal_end) OVER(ORDER BY month), bal_start)) /
COALESCE(LAG(bal_end) OVER(ORDER BY month), bal_start) AS growth
FROM (SELECT month,
MAX(CASE
WHEN m_end = 1 THEN
sum_balance
END) AS bal_end,
MAX(CASE
WHEN m_start = 1 THEN
sum_balance
END) bal_start
FROM (SELECT month,
SUM(COALESCE(CASE
WHEN bal_start = 1 THEN
balance
END,
0) + COALESCE(revenue, 0)) OVER(ORDER BY date) AS sum_balance,
m_end,
m_start,
date
FROM (SELECT DATE_FORMAT(date, '%Y-%m') AS month,
t.*,
ROW_NUMBER() OVER(PARTITION BY DATE_FORMAT(date, '%Y-%m') ORDER BY date) AS m_start,
ROW_NUMBER() OVER(PARTITION BY DATE_FORMAT(date, '%Y-%m') ORDER BY date DESC) AS m_end,
ROW_NUMBER() OVER(ORDER BY date) AS bal_start
FROM t) AS t0) AS t1
GROUP BY month) AS t2;
month
growth
2021-05
0.2500
2021-06
-0.4000
Demo

Related

SQL: Getting null values on comparing current values to previous period

I have copied this SQL query from another website and I'm not actually sure why it returns null values for the previous period and the month comparing it with. What should I adjust in the query to get the desired result?
SQL Query:
WITH monthly_metrics AS (
SELECT EXTRACT(year from day) as year,
EXTRACT(month from day) as month,
SUM(revenue) as revenue
FROM daily_metrics
GROUP BY 1,2
)
SELECT year AS current_year,
month AS current_month,
revenue AS revenue_current_month,
LAG(year,12) OVER ( ORDER BY year, month) AS previous_year,
LAG(month,12) OVER ( ORDER BY year, month) AS month_comparing_with,
LAG(revenue,12) OVER ( ORDER BY year, month) AS revenue_12_months_ago,
revenue - LAG(revenue,12) OVER (ORDER BY year, month) AS month_to_month_difference
FROM monthly_metrics
ORDER BY 1,2;
Query Result:
dbfiddle

Is their an SQL function that can give me both a negative & positive value using the query below?

So effectively what im trying to do is display a value by counting all new users from the current month minus new users from last month then divide the increase by last months number then times by 100. This will get the percentage increase but never displays a negative number, I realise that using abs() converts negative numbers to positive, is their a function that would allow me to do this?
Thanks.
select round(abs
((select count(id) from users where
month(created_at) = month(current_date())
and
YEAR(created_at) = year(current_date()))
-
(select count(id) from users where
month(created_at) = month(current_date - interval 1 MONTH)
and
YEAR(created_at) = year(current_date - interval 1 MONTH)))
/
(select count(id) from users where
month(created_at) = month(current_date())
and
YEAR(created_at) = year(current_date()))
*100, 0)
as abs_diff
from users
limit 1
;
You can get the number of the new users of the current month with:
count(case when last_day(created_at) = last_day(current_date) then 1 end)
and the number of the new users of the previous month with:
count(case when last_day(created_at) = last_day(current_date - interval 1 month) then 1 end)
So divide these numbers and subtract 1 before you multiply by 100:
select
100 * (
count(case when last_day(created_at) = last_day(current_date) then 1 end) /
nullif(count(case when last_day(created_at) = last_day(current_date - interval 1 month) then 1 end), 0)
- 1
) abs_diff
from users
where date(created_at) > last_day(current_date - interval 2 month)
The function nullif() will return null in case the number of the new users of the previous month is 0 to avoid division by 0.
See a simplified demo.
I am thinking something like this:
select ym.*,
(this_month - last_month) * 100.0 / last_month
from (select year(created_at) as yyyy, month(created_at) as mm, count(*) as this_month,
lag(count(*)) over (order by min(created_at)) as prev_month
from users
group by yyyy, mm
) ym;
Window functions are much simpler method to do month-over-month comparisons.
If you only want this for the current month, you can add:
order by yyyy desc, mm desc
limit 1

MySQL calculate percentage of two other calculated sums including a group by month

Say i have 3 tables with a model like this
The result i want to have now looks like this
I want to calculate turnovers and profits made by all employees per month and compare it to the last years SAME month and calculate the difference in percentage of the profits. It should include the last 12 months with the INTERVAL function.
select
bookings.b_emp_id as "Employee",
MONTH(bookings.b_date) as Month,
#turnover1 := sum(bookings.b_turnover) as '2017-turnover',
#turnover2 := (select sum(lx.b_turnover)
from bookings as lx
where lx.b_date = date_add(bookings.b_date, INTERVAL -1 YEAR)
GROUP BY
MONTH(bookings.b_date),
YEAR(bookings.b_date),
bookings.b_emp_id
) as '2016-turnover',
sum(b_profit) as '2017-profit',
#profit1 := (select sum(lx.umsatz_fees)
from bookings as lx
where lx.b_date = date_add(bookings.b_date,INTERVAL -1 YEAR)
GROUP BY
MONTH(bookings.b_date),
YEAR(bookings.b_date),
bookings.b_emp_id
) as '2016-profit'
from bookings
where bookings.b_date > '2017-01-01'
and bookings.b_emp_id = ´SA´
GROUP BY MONTH(bookings.b_date)
order by bookings.b_date desc
Use conditional aggregation. It is not clear if you want to look at the last 12 / 24 months, or at the months of 2017 and the same months in 2016. Neither do I understand how you want to calculate a percentage. I divide this year's profits by last year's in below query. Adjust this so it meets your needs.
select
b_emp_id,
month,
turnover_this_year,
profit_this_year,
turnover_last_year,
profit_last_year,
profit_this_year / profit_last_year * 100 as diff
from
(
select
b_emp_id,
month(b_date) as month,
sum(case when year(b_date) = year(curdate()) then b_turnover end) as turnover_this_year,
sum(case when year(b_date) = year(curdate()) then b_profit end) as profit_this_year,
sum(case when year(b_date) < year(curdate()) then b_turnover end) as turnover_last_year,
sum(case when year(b_date) < year(curdate()) then b_profit end) as profit_last_year
from bookings
where year(b_date) in (year(curdate()), year(curdate()) - 1)
and month(b_date) <= month(curdate())
group by b_emp_id, month(b_date)
) figures
order by b_emp_id, month;

Combining database queries

How can these SQL-queries to extract statistics from my database be combined for better performance?
$total= mysql_query("SELECT COUNT(*) as number, SUM(order_total) as sum FROM history");
$month = mysql_query("SELECT COUNT(*) as number, SUM(order_total) as sum FROM history WHERE date >= UNIX_TIMESTAMP(DATE_ADD(CURDATE(),INTERVAL -30 DAY))");
$day = mysql_query("SELECT COUNT(*) as number, SUM(order_total) as sum FROM history WHERE date >= UNIX_TIMESTAMP(CURDATE())");
If you want to all the data in a single query, you have two choices:
Use a UNION query (as sugested by bishop in his answer)
Tweak a query to get what you need in a single row
I'll show option 2 (option 1 has been already covered).
Note: I'm using user variables (that stuff in the init subquery) to avoid writing the expressions again and again. Also, to filter the aggregate data, I'm using case ... end expressions.
select
-- Your first query:
count(*) as number, sum(order_total) as `sum`
-- Your second query:
, sum(case when `date` <= #prev_date then 1 else 0 end) as number_prev
, sum(case when `date` <= #prev_date then order_total else 0 end) as sum_prev
-- Your third query:
, sum(case when `date` <= #cur_date then 1 else 0 end) as number_cur
, sum(case when `date` <= #cur_date then order_total else 0 end) as sum_cur
from (
select #cur_date := unix_timestamp(curdate())
, #prev_date := unix_timestamp(date_add(curdate(), interval -30 day))
) as init
, history;
Hope this helps
Since the queries have the same column structure, you can ask MySQL to combine them with the UNION operation:
(SELECT 'total' AS kind, COUNT(*) as number, SUM(order_total) as sum FROM history~
UNION
(SELECT 'by-month' AS kind, COUNT(*) as number, SUM(order_total) as sum FROM history WHERE date <= UNIX_TIMESTAMP(DATE_ADD(CURDATE(),INTERVAL -30 DAY)))
UNION
(SELECT 'by-day' AS kind, COUNT(*) as number, SUM(order_total) as sum FROM history WHERE date <= UNIX_TIMESTAMP(CURDATE()))

MySQL month over month totals compared to previous month

I'd like to sum the amount column for a given month, as well as the total for the previous month. Right now the first month's total works, but every month thereafter is too high by at least 2 order of magnitude.
SELECT month, year, total, previous_month, previous_year, previous_total FROM (
SELECT MONTH(p1.start_date) AS month,
YEAR(p1.start_date) AS year,
SUM(p1.amount) AS total,
SUM(p2.amount) AS previous_total,
MONTH(DATE_SUB(p1.start_date, INTERVAL 1 MONTH)) AS previous_month,
YEAR(DATE_SUB(p1.start_date, INTERVAL 1 MONTH)) AS previous_year
FROM trackings p1
LEFT JOIN trackings p2 ON EXTRACT(YEAR_MONTH FROM DATE_SUB(p1.start_date, INTERVAL 1 MONTH)) = EXTRACT(YEAR_MONTH FROM p2.start_date)
GROUP BY EXTRACT(YEAR_MONTH FROM p1.start_date)
) AS p3;
Related question: I'm doing the same DATE_SUB 3 times. Is there a way to clean up the date related parts to be more efficient?
Sample: http://sqlfiddle.com/#!2/3013a/1
Think the problem is you are getting rows counted multiple times on the join. Ie, you have a sum of all the amounts for a month, but each row from that month is being joined with all rows for the previous month.
Solution using a couple of subselect:-
SELECT ThisMonth.MONTH, ThisMonth.YEAR, ThisMonth.TOTAL, PrevMonth.MONTH AS PREVIOUS_MONTH, PrevMonth.YEAR AS PREVIOUS_YEAR, PrevMonth.TOTAL AS PREVIOUS_TOTAL
FROM
(
SELECT MONTH(start_date) AS `month`,
YEAR(start_date) AS `year`,
EXTRACT(YEAR_MONTH FROM start_date) AS YearMonth,
SUM(amount) AS total
FROM trackings
GROUP BY `month`, `year`, YearMonth
) ThisMonth
LEFT OUTER JOIN
(
SELECT MONTH(start_date) AS `month`,
YEAR(start_date) AS `year`,
EXTRACT(YEAR_MONTH FROM DATE_ADD(start_date, INTERVAL 1 MONTH)) AS YearMonth,
SUM(amount) AS total
FROM trackings
GROUP BY `month`, `year`, YearMonth
) PrevMonth
ON ThisMonth.YearMonth = PrevMonth.YearMonth