Count by month with only two date fields - IN and OUT - mysql

Haven't been able to find an answer to this specific issue. Need a total count of inventory grouped by month on different products. Source data has date fields, one for IN and one for OUT. Total count for a specific month would include an aggregate sum of all rows with an IN date prior to specific month as long as the out date is null or a date after the specific month.
Obviously I can get a count for any given month by writing a query for count(distinct productID) with a WHERE clause stating that the IN Date be before the month I'm interested in (IE September 2012) AND the Out Date is null or after 9/2012:
Where ((in_date <= '2012-09-30') AND (out_date >= '2012-09-01' or out_date is null))
If the product was even part of inventory for one day in September I want it to count which is why out date above 9/1/12. Sample data below. Instead of querying for a specific month, how can I turn this:
Raw Data - Each Row Is Individual Item
InDate OutDate ProductAttr ProductID
2008-04-05 NULL Blue 101
2008-06-04 NULL Red 125
2008-01-01 2012-06-01 Blue 134
2008-12-10 2012-10-09 Red 129
2009-10-15 2012-11-01 Blue 153
2012-10-01 2013-06-01 Red 149
Into this?:
Date ProductAttr Count
2008-04 Blue 503
2008-04 Red 1002
2008-05 Blue 94
2008-05 Red 3004
2008-06 Blue 2000
2008-06 Red 322
Through grouping I can get the raw data into this format grouped by months:
InDate OutDate Value Count
2008-05 2012-05 Blue 119
2008-05 2008-06 Red 333
2008-05 2012-10 Blue 4
2008-05 NULL Red 17488
2008-06 2012-11 Blue 711
2008-06 2013-02 Red 34
If you wanted to know how many products were 'IN' as of Oct. 2012- you would sum the counts of all rows except for 2. Group on Value to keep blue and red separate. Row 2 is ruled out because OutDate is before Oct. 2012.
Thank in advance.
EDIT:
Gordon Linoff's solution works just like I need it to. The only issue I am having now is the size and efficiency of the query, because the part I left out above is that the product attribute is actually located in a different table then the IN/OUT dates and I also need to join a third table to limit to a certain type of product (ForSale for example). I have tried two different approaches and they both work and return the same data, but both take far too long to automate this report:
select months.mon, count(distinct d.productID), d.ProductAttr
from (select '2008-10' as mon union all
select '2008-11' union all
select '2008-12' union all
select '2009-01'
) months left outer join
t
on months.mon >= date_format(t.Indate, '%Y-%m') and
(months.mon <= date_format(t.OutDate, '%Y-%m') or t.OutDate is NULL)
join x on x.product_id = t.product_id and x.type = 'ForSale'
join d on d.product_id = x.product_id and d.type = 'Attribute'
group by months.mon, d.ProductAttr;
Also tried the above without the last two joins by adding subqueries for the product attribute and where/exclusion - this seems to run about the same or a bit slower:
select months.mon, count(distinct t.productID), (select ProductAttr from d where productid = t.productID and type = 'attribute' limit 1)
from (select '2008-10' as mon union all
select '2008-11' union all
select '2008-12' union all
select '2009-01'
) months left outer join
t
on months.mon >= date_format(t.Indate, '%Y-%m') and
(months.mon <= date_format(t.OutDate, '%Y-%m') or t.OutDate is NULL)
WHERE exists (select 1 from x where x.productid = t.productID and x.type = 'ForSale')
group by months.mon, d.ProductAttr;
Any ideas to make this more efficient with the additional data that I need to rely on 3 source tables in total (1 just for exclusion). Thanks in advance.

You can do this by generating a list of the months that you need. The easiest way is to do this manually in MySQL (although generating the code in Excel can make this easier).
Then use a left join and aggregation to get the information you want:
select months.mon, t.ProductAttr, count(distinct t.productID)
from (select '2008-10' as mon union all
select '2008-11' union all
select '2008-12' union all
select '2009-01'
) months left outer join
t
on months.mon >= date_format(t.Indate, '%Y-%m') and
(months.mon <= date_format(t.OutDate, '%Y-%m) or t.OutDate is NULL)
group by t months.mon, t.ProductAttr;
This version does all the comparisons as strings. You are working at the granularity of "month" and the format YYYY-MM does a good job for comparisons.
EDIT:
You do need every month that you want in the output. If you have products coming in every month, then you could do:
select months.mon, t.ProductAttr, count(distinct t.productID)
from (select distinct date_format(t.InDate, '%Y-%m') as mon
from t
) months left outer join
t
on months.mon >= date_format(t.InDate, '%Y-%m') and
(months.mon <= date_format(t.OutDate, '%Y-%m) or t.OutDate is NULL)
group by t months.mon, t.ProductAttr;
This pulls the months from the data.

Related

How to show months if it has no record and force it to zero if null on MySQL

i have an orders table, and i need to fetch the orders record by month. but i have terms if there is no data in a month it should still show the data but forcing to zero like this:
what i have done is using my query:
select sum(total) as total_orders, DATE_FORMAT(created_at, "%M") as date
from orders
where is_active = 1
AND tenant_id = 2
AND created_at like '%2021%'
group by DATE_FORMAT(created_at, "%m")
but the result is only fetched the existed data:
can anyone here help me to create the exactly query?
Thank you so much
Whenever you're trying to use a value that doesn't exist in the table, one option is to use a reference; whether it's from a table or a query-generated value.
I'm guessing that in terms of date data, the column created_at in table orders may have a complete list all the 12 months in a year regardless of which year.
Let's assume that the table data for orders spans from 2019 to present date. With that you can simply create a 12 months reference table for a LEFT JOIN operation. So:
SELECT MONTHNAME(created_at) mnt FROM orders GROUP BY MONTHNAME(created_at);
You can append that into your query like:
SELECT IFNULL(SUM(total),0) as total_orders, mnt
from (SELECT MONTHNAME(created_at) mnt FROM orders GROUP BY MONTHNAME(created_at)) mn
LEFT JOIN orders o
ON mn.mnt=MONTHNAME(created_at)
AND is_active = 1
AND tenant_id = 2
AND created_at like '%2021%'
GROUP BY mnt;
Apart from adding the 12 months sub-query and a LEFT JOIN, there are 3 other changes from your original query:
IFNULL() is added to the SUM() operation in SELECT to return 0 if the value is non-existent.
All the WHERE conditions has been switched to ON since remaining it as WHERE will make the LEFT JOIN becoming a normal JOIN.
GROUP BY is using the sub-query generated month (mnt) value instead.
Taking consideration of table orders might not have the full 12 months, you can generate it from query. There are a lot of ways of doing it but here I'm only going to show the UNION method that works with most MySQL version.
SELECT MONTHNAME(CONCAT_WS('-',YEAR(NOW()),mnt,'01')) dt
FROM
(SELECT 1 AS mnt UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION
SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION
SELECT 9 UNION SELECT 10 UNION SELECT 11 UNION SELECT 12) mn
If you're using MariaDB version that supports SEQUENCE ENGINE, the same query above is much shorter:
SELECT MONTHNAME(CONCAT_WS('-',YEAR(NOW()),mnt,'01'))
FROM (SELECT seq AS mnt FROM seq_1_to_12) mn
I'm using MariaDB 10.5 in this demo fiddle however it seems like the month name ordering is based on the name value rather than the month itself so it looks un-ordered. It's in the correct order if it's in MySQL 8.0 fiddle though.
Thanks all for the answers & comments i really appreciate it.
i solved it by create table helper for static months then use union and aliasing, since i need the months in indonesia, i create case-when function too.
so, the query is like this:
SELECT total_orders,
(CASE date WHEN 01 THEN 'Januari'
WHEN 02 THEN 'Februari'
WHEN 03 THEN 'Maret'
WHEN 04 THEN 'April'
WHEN 05 THEN 'Mei'
WHEN 06 THEN 'Juni'
WHEN 07 THEN 'Juli'
WHEN 08 THEN 'Agustus'
WHEN 09 THEN 'September'
WHEN 10 THEN 'Oktober'
WHEN 11 THEN 'November'
WHEN 12 THEN 'Desember'
ELSE date END ) AS date
FROM (SELECT SUM(total) AS total_orders,
DATE_FORMAT(created_at, "%m") AS date
FROM orders
WHERE is_active = 1
AND tenant_id = 2
AND created_at like '%2021%'
GROUP BY DATE_FORMAT(created_at, "%m")
UNION
SELECT 0 AS total_orders,
code AS date
FROM quantum_default_months ) as Q
GROUP BY date
I still don't know if this query is fully correct or not, but I get my exact result.
cmiiw.
thanks all

Group By rows where value equals 0 or non-existent rows in mysql?

I have a simple piece of SQL code where I am trying to get the monthly averages of numbers. But the problem I am running into is if any number within any given month is 0 then the average returned is 0 or if there are any rows that don't exist with any given month then there are no values returned at all for that month. Hopefully, someone can give me some insight as to what I am doing wrong.
GROUP BY 1 = a.metric and GROUP BY 2 = a.report_dt within the subquery
I have tried inserting the missing rows with a value of 0, but as I said it will return the averaged value as 0 as well.
SELECT a.report_dt - INTERVAL 1 DAY AS 'Date',
a.metric,
a.num
FROM (SELECT *
FROM reporting.tbl_bo_daily_levels b
WHERE b.report_dt = reporting.func_first_day(b.report_dt)
AND b.report_dt > DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY 1,2
)a;
My expected results are to get the average numbers of each month even if there are non-existent rows within the specified date range or even if there zeroes as values.
You need a relation of all the months you want to span. This can be made ad hoc with UNION ALL. Then left join the data on the months GROUP BY the month and the metric and get avg(num).
SELECT m.year,
m.month,
l.metric,
coalesce(avg(l.num), 0)
FROM (SELECT 2017 year,
12 month
UNION ALL
SELECT 2018 year,
1 month
UNION ALL
SELECT 2018 year,
2 month
...
SELECT 2018 year,
11 month
UNION ALL
SELECT 2018 year,
12 month) months m
LEFT JOIN reporting.tbl_bo_daily_levels l
ON year(l.report_dt) = m.year
AND month(l.report_dt) = m.month;
GROUP BY m.year,
m.month,
l.metric;
(Change the second parameter to coalesce if you want any other number than 0 if there are no numbers for a month. Or don't use coalesce() at all if you want NULL in such cases.)

MySQL avg count per month for current year (include months with no data)

I am trying to create a query for a bar-chart which displays a monthly overview of the number of orders.
The query I am using is correctly providing me with the breakdown per month but when I skipped a month, it is not providing a 0 for that month, just doesnt add it at all.
Since this chart is expecting 12 numbers, everything after the missing month would be off an inaccurate.
Current Attempt:
select Month(dateCreated) as monthID,
Monthname(dateCreated) as monthName,
count(dateCreated) as totalRewards
from reward
where Year(dateCreated) = '2018'
GROUP BY monthID
If we were to assume that it is currently May 2018, I would like to see Jan - May, current counts even if the month had no orders (April = 0).
Whats the best way to include all months that have happened so far in the provided year and then their appropriate count?
You can mock a months table, then LEFT JOIN the reward table against it. To ensure you only get valid results, it's best to use a SUM() where not null rather than a COUNT() aggregate:
SELECT
months.id as monthID,
MONTHNAME(CONCAT('2018-',months.id,'-01')) as monthName,
SUM(CASE WHEN dateCreated IS NULL THEN 0 ELSE 1 END) as totalRewards
FROM
(
SELECT 1 AS id
UNION SELECT 2
UNION SELECT 3
UNION SELECT 4
UNION SELECT 5
UNION SELECT 6
UNION SELECT 7
UNION SELECT 8
UNION SELECT 9
UNION SELECT 10
UNION SELECT 11
UNION SELECT 12
) as months
LEFT JOIN reward
ON MONTH(reward.dateCreated) = months.id
AND YEAR(dateCreated) = '2018'
GROUP BY monthID, monthName
ORDER BY monthID;
SQL Fiddle

MySQL Couting Distinct values when grouping by dates

I do have two tables, one is a calendar table with a DATE column, and the other contains ID's and three DATES for each ID.
Calendar Table:
dt
2016-01-01
2016-01-02
2016-01-03
2016-01-04
...
Data Table:
ID d_created d_forwarded d_solved
1 2016-01-01 2016-01-02 2016-01-03
2 2016-01-01 2016-01-02 2016-01-03
3 2016-01-02 2016-01-02 2016-01-04
4 2016-01-03 2016-01-04 2016-01-05
...
The Data Table does in reality contain a multitude of other fields, but I think that is irrelevant for my question. I have a query which selects the DATE field from the calendar table for a given range, let's say a month, and then I do a LEFT JOIN with the Data Table using all three DATE fields combined with OR, because I need to count multiple things from the Data Table, some depending on the d_created, some depending on the d_forwarded and others on d_solved:
SELECT
tc.dt AS dt,
COUNT(DISTINCT(CASE
WHEN td.id != 0
AND DATE(td.d_solved) >= '2016-01-01'
AND DATE(td.d_solved) <= '2016-01-31'
THEN td.id
ELSE NULL
END)) AS result1
.... more stmts ...
FROM calendar_table tc
LEFT JOIN data_table td ON tc.dt = DATE(td.d_created) OR tc.dt = DATE(td.d_solved) OR tc.dt = DATE(td.d_forwarded)
Now here's my problem: The query delivers the correct output, when I do not group my results by tc.dt, but as soon as I group it by tc.dt, the results are incorrect. I am by no means an SQL expert, but as far as I understand it, td.id will occur more than once due to the JOIN, and as long as I have a single result row, the DISTINCT prevents an ID from being counted twice.
I need to be able to count all ID's which have been created, solved or forwarded within my date range, and I also need the calendar table join because I would like to display each day in the range, even though there might be no matching dates in my data table for a particular day, if that makes sense.
Is there any way I can make sure that no ID is counted more than one time when grouping by days ?
I hope I could make clear what the exact problem is, if not, please let me know and I try to elaborate in more detail.
UPDATE
I tried using SarathChandra example which looks quite promising and it does indeed deliver results, however as soon as I add more criteria to my CASE WHEN statement, it does not work the way it should. I forked and modified SarathChandra's ideaone fiddle HERE
So it should return 1 for the 2016-01-02 date but it shows a 0 ?
UPDATE 2
Unfortunately, none of the provided answers was able to solve the underlying problem. While both suggestions were appreciated a lot, I ended up splitting the query into three queries, each time joining the calendar table with the same range of dates, and then combining the arrays in PHP to a single result set.
I have made some assumptions regarding the data in arriving at the following solution:
d_created shall precede d_forwarded, which in turn shall precede d_solved.
In order to remove duplicate counts, that is, count each record only once, I am joining on the basis of the least of the three dates.
The below query seems to be working fine for me.
SELECT
tc.dt AS dt,
COUNT(
CASE WHEN DATE(td.d_created) BETWEEN '2016-01-01' AND '2016-01-31' THEN td.id
ELSE NULL END) AS `Count`
FROM calendar_table tc
LEFT JOIN data_table td ON
(tc.dt = LEAST(DATE(td.d_created), DATE(td.d_solved), DATE(td.d_forwarded)))
GROUP BY tc.dt;
UPDATE: Working example code here.
The problem here is that you join on multiple columns, so when you group on date you'll for example get the ID 1 for the '2016-01-01' (created), '2016-01-02' (solved) and '2016-01-03' (forwarded).
You could try to join to the same table 3 times and count the results in 3 columns. The sum of each column should then match the number of records.
SQL Fiddle Example
Query:
SELECT tc.dt AS dt,
COUNT(DISTINCT(CASE WHEN td_solved.id != 0
AND DATE(td_solved.d_solved) >= '2016-01-01'
AND DATE(td_solved.d_solved) <= '2016-01-31' THEN td_solved.id ELSE NULL END)) AS solved,
COUNT(DISTINCT(CASE WHEN td_created.id != 0
AND DATE(td_created.d_created) >= '2016-01-01'
AND DATE(td_created.d_created) <= '2016-01-31' THEN td_created.id ELSE NULL END)) AS created,
COUNT(DISTINCT(CASE WHEN td_forwarded.id != 0
AND DATE(td_forwarded.d_forwarded) >= '2016-01-01'
AND DATE(td_forwarded.d_forwarded) <= '2016-01-31' THEN td_forwarded.id ELSE NULL END)) AS forwarded
FROM calendar_table tc
LEFT JOIN data_table td_created ON tc.dt = DATE(td_created.d_created)
LEFT JOIN data_table td_solved ON tc.dt = DATE(td_solved.d_solved)
LEFT JOIN data_table td_forwarded ON tc.dt = DATE(td_forwarded.d_forwarded)
GROUP BY 1 WITH ROLLUP

MySQL get all the months of the year stats

I have to do a SQL query for getting the incomes of a company on all the months of the year, but some months dont have records in the table.
I have used this query:
SELECT COUNT(wp_dgl_stats.datetime_stamp)*wp_dgl_ads.price as incomes, MONTHNAME(wp_dgl_stats.datetime_stamp) as month
FROM wp_dgl_ads
INNER JOIN wp_dgl_stats ON wp_dgl_stats.id_ad = wp_dgl_ads.id
WHERE YEAR(wp_dgl_stats.datetime_stamp) = 2015
GROUP BY MONTHNAME(wp_dgl_stats.datetime_stamp)
I have to say that wp_dgl_stats contains a record for every click made by an user in certain spaces of the web (the ads showed) with a reference to the ad and a datetime stamp.
This query returns exactly months with incomes and the exact amount. But I need to get also the rest of the months with a 0.
How could this be done?
After a lot of tests I got a proper solution. I will post it here if someone needs for it with an explanation.
SELECT meses.month, CAST(COUNT(stats.id)*ads.precio AS UNSIGNED) as precio
FROM
(
SELECT 1 AS MONTH
UNION SELECT 2 AS MONTH
UNION SELECT 3 AS MONTH
UNION SELECT 4 AS MONTH
UNION SELECT 5 AS MONTH
UNION SELECT 6 AS MONTH
UNION SELECT 7 AS MONTH
UNION SELECT 8 AS MONTH
UNION SELECT 9 AS MONTH
UNION SELECT 10 AS MONTH
UNION SELECT 11 AS MONTH
UNION SELECT 12 AS MONTH
) as meses
LEFT JOIN wp_dgl_stats stats ON meses.month = MONTH(stats.datetime_stamp)
LEFT JOIN wp_dgl_ads ads ON stats.id_ad = ads.id AND YEAR(stats.datetime_stamp) = '2015'
GROUP BY meses.month
Because I am a spanish developer and I need to have spanish month names I selected the month number and with an PHP array we can convert the month number to his spanish name.
If someone have some question, do it, I will be glad to answer.
You could LEFT/RIGHT join on the months:
WITH monthnames AS (SELECT <something that returns a list of the month names> as monthname)
SELECT COALESCE(COUNT(wp_dgl_stats.datetime_stamp)*wp_dgl_ads.price, 0) as incomes, MONTHNAME(wp_dgl_stats.datetime_stamp) as month
FROM wp_dgl_ads
INNER JOIN wp_dgl_stats ON wp_dgl_stats.id_ad = wp_dgl_ads.id
RIGHT JOIN monthnames ON month = monthname
WHERE YEAR(wp_dgl_stats.datetime_stamp) = 2015
GROUP BY MONTHNAME(wp_dgl_stats.datetime_stamp)