mysql skips certain months - mysql

Been trying to sort this one out for a while. I'd really appreciate any help.
I've got this table where I'm getting 2 columns with date and int values respectively. The problem is that mysql skips the date values wherever the int value is null.
Here the sql statement
SELECT DATE_FORMAT(sales_date_sold, '%b \'%y')
AS sale_date, sales_amount_sold
AS sale_amt
FROM yearly_sales
WHERE sales_date_sold BETWEEN DATE_SUB(SYSDATE(), INTERVAL 2 YEAR) AND SYSDATE()
GROUP BY YEAR(sales_date_sold), MONTH(sales_date_sold)
ORDER BY YEAR(sales_date_sold), MONTH(sales_date_sold) ASC;
There aren't any values for feb 2011 so that month gets skipped, along with a few others. Coalesce and if_null don't work too.

You need a row source that provides values for all of the months in the dimension, and then left join your yearly_sales table to that.
You are doing a GROUP BY, you most likely want an aggregate on your measure (sales_amount_sold), or you don't want a GROUP BY. (The query in your question is going to return a value from sales_amount_sold for only one row in a given month. That may be what you want, but its a very odd resultset to return.)
One approach is to have a "calendar_month" table that contains DATE values all of the months you want returned. (There are other ways to generate this, existing answers to questions elsewhere on stackoverflow)
SELECT m.month AS sale_date
, IFNULL(SUM(s.sales_amount_sold),0) AS sale_amt
FROM calendar_months m
LEFT
JOIN yearly_sales s
ON s.sales_date_sold >= m.month
AND s.sales_date_sold < DATE_ADD(m.month,INTERVAL 1 MONTH)
WHERE m.month BETWEEN DATE_SUB(SYSDATE(), INTERVAL 2 YEAR) AND SYSDATE()
GROUP BY m.month
ORDER BY m.month
This query returns a slightly different result, you are only going to get rows in groups of "whole months", rather than including partial months, as in your original query, because the WHERE clause on sale_date references two years before the current date and time, rather than the "first of the month" two years before.
A calendar_months table is not necessarily required; this could be replaced with a query that returns the row source. In that case, the predicate on the month value could be moved from the outer query into the subquery.
Addendum: if you use a calendar_month table as a rowsource, you'll need to populate it with every possible "month" value you want to return.
CREATE TABLE calendar_month
(`month` DATE NOT NULL PRIMARY KEY COMMENT 'one row for first day of each month');
INSERT INTO calendar_month(`month`) VALUES ('2011-01-01'),('2011-02-01'),('2011-03-01')
As an alternative, you can specify a dynamically generated rowsource, as an inline view, rather than a table reference. (You could use a similar query to quickly populate a calendar_months table.)
You can wrap this query in parenthesis, and paste it between FROM and calendar_months in the previous query I provided.
SELECT DATE_ADD('1990-01-01',INTERVAL 1000*thousands.digit + 100*hundreds.digit + 10*tens.digit + ones.digit MONTH) AS `month`
FROM ( SELECT 0 AS digit UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 ) ones
JOIN ( SELECT 0 AS digit UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 ) tens
JOIN ( SELECT 0 AS digit UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 ) hundreds
JOIN ( SELECT 0 AS digit UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 ) thousands

The problem is not that the value is NULL, the problem is that you are selecting data off your Database. If you don't have data for a specific month, MySQL has no way of selecting data that is not there.
The only way to solve this completely in MySQL is already answered in a very similar question

I have had this problem before with timestamps. The solution I used was to create a reference table with all of your months. This could be a table with just the numbers 1-12 (12 rows) or you could go one step further and put the month names. Then you can left join your yearly_sales table to the 1_through_12 table to get every month.

Why don't you just use 0 instead of NULL?

Related

MySQL query to count zero value using group by in the same table

Here's my "customers" table:
To get number of enquiries per for a particular month and year, I'm using following query:
SELECT YEAR(customer_date) AS Year, MONTH(customer_date) AS Month, COUNT(customer_id) AS Count FROM customers WHERE customer_product = 6 GROUP BY YEAR(customer_date), MONTH(customer_date)
I get following result:
You can see that as there is no enquery in the April month, so no row fetched for month number 4. But I want 0 value in Count column if there is no record found in that particular month and year.
This is what I want:
One option uses a calendar table to represent all months and years, even those which do not appear in your data set:
SELECT
t1.year,
t2.month,
COUNT(c.customer_id) AS Count
FROM
(
SELECT 2017 AS year UNION ALL
SELECT 2018
) t1
CROSS JOIN
(
SELECT 1 AS month UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
SELECT 5 UNION ALL
SELECT 6 UNION ALL
SELECT 7 UNION ALL
SELECT 8 UNION ALL
SELECT 9 UNION ALL
SELECT 10 UNION ALL
SELECT 11 UNION ALL
SELECT 12
) t2
LEFT JOIN customers c
ON t1.year = YEAR(c.customer_date) AND
t2.month = MONTH(c.customer_date)
WHERE
c.customer_product = 6
GROUP BY
t1.year,
t2.month
ORDER BY
t1.year,
t2.month;
Note: The above query can probably be made faster by actually creating dedicated calendar tables in your MySQL schema.
The following index on the customers table might help:
CREATE INDEX idx ON customers(customer_product, customer_id);
This might make the join between the calendar tables and customers faster, assuming that the customer_product = 6 condition is restrictive.

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

How to find daily average over a time period in mysql?

I've a table where there's two column:
MARKS
CREAT_TS
I want to daily average marks for between two date range (e.g. startDate & endDate)
I've made the following query:
select SUM(MARKS)/ COUNT(date(CREAT_TS)) AS DAILY_AVG_MARKS,
date(CREAT_TS) AS DATE
from TABLENAME
group by date(CREAT_TS)
With this query I can get the daily average only if there's a row in the database for the date. But my requirement is that even if there's no row, I want to show 0 for that date.
I mean I want the query to return X rows if there are X days between (startDate, endDate)
Can anyone help me. :(
You need to create a set of integers that you can add to the dates. The following will give you an idea:
select thedate, avg(Marks) as DAILY_AVG_MARKS
from (select startdate+ interval num day as thedate
from (select d1.d + 10 * d2.d + 100*d3.d as num
from (select 0 as d union select 1 union select 2 union select 3 union select 4 union
select 5 union select 6 union select 7 union select 8 union select 9
) d1 cross join
(select 0 as d union select 1 union select 2 union select 3 union select 4 union
select 5 union select 6 union select 7 union select 8 union select 9
) d2 cross join
(select 0 as d union select 1 union select 2 union select 3 union select 4 union
select 5 union select 6 union select 7 union select 8 union select 9
) d3
) n cross join
(select XXX as startdate, YYY as enddate) const
where startdate + num <= enddate
) left outer join
tablename t
on date(CREAT_TS) = thedate
group by thedate
All the complication is in creating a set of sequential dates for the report. If you have a numbers table or a calendar table, then the SQL looks much simpler.
How does this work? The first big subquery has two parts. The first just generates the numbers from 0 to 999 by cross joining the digits 0-9 and doing some arithmetic. The second joins this to the two dates, startdate and enddate -- you need to put the correct values in for XXX and YYY. With this table, you have all the dates between the two values. If you need more than 999 days, just add in another cross join.
This is the left joined to your data table. The result is that all dates appear for the group by.
In terms of reporting, there are advantages and disadvantages to doing this in the presentation layer. Basically, the advantage to doing it in SQL is that the report layer is simpler. The advantage to doing it in the reporting layer is that the SQL is simpler. It is hard for an outsider to make that judgement.
My suggestion would be to create a numbers table that you can just use in reports like this. Then the query will look simpler and you won't have to change the reporting layer.

MySQL: Is it possible to 'fill' a SELECT with values without a table?

I need to display the total of 'orders' for each year and month. But for some months there is no data, but I DO want to display that month (with a total value of zero). I could make a helpertable 'months' with 12 records for each year, but is there maybe a way to get a range of months, without introducing a new table?
Something like:
SELECT [all year-month combinations between january 2000 and march 2011]
FROM DUAL AS years_months
Does anybody have an idea how to do this? Can you use SELECT with some kind of formula, to 'create' data on the fly?!
UPDATE:
Found this myself:
generate days from date range
The accepted answer in this question is kind of what I'm looking for. Maybe not the easiest method, but it does what I want: fill a select with data, based on a formula....
To 'create' a table on the fly with all months of the last 10 years:
SELECT CONCAT(MONTHNAME(datetime), ' ' , YEAR(datetime)) AS YearMonth,
MONTH(datetime) AS Month,
YEAR(datetime) AS Year
FROM (
select (curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) MONTH) as datetime
from (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as a
cross join (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as b
cross join (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as c
LIMIT 120
) AS t
ORDER BY datetime ASC
I must admit, this is VERY exotic, but it DOES work...
I can use this select to join it with my 'orders'-table and get the totals for each month, even when there is no data in a certain month.
But using a 'numbers' or 'calendar' table is probably the best option, so I'm going to use that.
If at all possible, try to stay away from generating data on the fly. It makes very simple queries ridiculusly complex, but above all: it confuses the optimizer to no end.
If you need a series of integers, use a static table of integers. If you need a series of dates, months or whatever, use a calendar table. Unless you are dealing with some truly extraordinary requirements, a static table is the way to go.
I gave an example on how to create a table of numbers and a minimal calendar table(only dates) in this answer.
If you have those tables in place, it becomes easy to solve your query.
Aggregate the order data to MONTH.
Right join to the table of months (or distinct MONTH from the table of dates)
You could try something like this
select * from
(select 2000 as year union
select 2001 as year union
select 2009
) as years,
(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
)
AS months
WHERE year between 2001 AND 2008 OR (year=2000 and month>0) OR (year = 2009 AND month < 4)
ORDER by year,month
You could just fill in the missing months after you've done your query in your application logic.
You should most definitely do this in your application rather than the DB layer. Simply create an array of dates for the time range, and merge the actual data with the empty dates you pre-created. See this answer to similar question
I do following query to generate months in a given interval. For my case it generate list of month started from may 2013 until now.
SELECT date_format(#dt:= DATE_ADD( #dt, INTERVAL 1 MONTH),'%M %Y') date_string,
#dt as date_full
FROM (SELECT #dt := DATE_SUB(CAST(DATE_FORMAT('2013-05-01' ,'%Y-%m-01') AS DATE),
INTERVAL 1 MONTH) ) vars,
your_tables
WHERE #dt<NOW()
The concern is, it should be joined with table containing sufficient rows to supply number of month you expected. E.g. if you need to generate all month in a particular year, you will need a tables consisting at least 12 rows.
For me it is a bit straight forward. I joined it with my configuration table, consisting around 370 rows. So it could generate months in a year, or days in a year if I need it. Changing from month interval into days interval would be easy, as I need only to change the interval from MONTH to DAY.
If you're using PostgreSQL, you can combine both date_trunc and generate_series to do some very fun grouping and series generation.
For example, you could use this to generate a table of all dates in the last year:
SELECT current_date - s.a as date
FROM generate_series(0,365,1) as s(a);
Then, you could use date_trunc to grab the months and group by that date_trunc'ed field:
SELECT date(date_trunc('month', series.date)) as month, COUNT(*) as days
FROM (SELECT current_date - s.a as date
FROM generate_series(0,365,1) as s(a)) series
GROUP BY month;
Create a table (e.g. tblMonths) that includes all 12 months and use a LEFT JOIN (or RIGHT JOIN) on it and your partial source data.
Check out the reference and this tutorial for how this works.
I would do something like this:
SELECT COUNT(Order.OrderID)
FROM Orders
WHERE YEAR(Order.DateOrdered) > 2000
GROUP BY MONTH(Order.DateOrdered)
This will give you the number of orders grouped by each month.
Then in you application simply assign a ZERO to the months in which no data was returned
I hope this Helps
Query on static data MySQL.
You can select static data from hardcoded list with table by this query
SELECT *
FROM (
values row('Hamza','23'), row('Ali', '24')
) t1 (name, age);

SQL query to bring months that doesn't exist in a column

I want to count the number of rows with respect to the month that they've written in database. My database has a column named created_date. you can see what i've done bellow :
select month(created_date) as "Month", count(created_date) as "Count" from transactions group by month(created_date)
what this query returns is something like this :
{'Month':1,'Count':10}
this happens because i only have one month in my database, while i need to have all months in results, including months that doesn't exist in database, like this :
{'Month':1,'Count':10}
{'Month':2,'Count':0}
{'Month':3,'Count':0}
{'Month':4,'Count':0}
{'Month':5,'Count':0}
{'Month':6,'Count':0}
{'Month':7,'Count':0}
{'Month':8,'Count':0}
{'Month':9,'Count':0}
{'Month':10,'Count':0}
{'Month':11,'Count':0}
{'Month':12,'Count':0}
how should i do it?
You could JOIN to a list of months so you can get a row even for months that don't exist in your table:
SELECT m.month,
COUNT(t.created_date)
FROM (SELECT 1 AS month UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12) m
LEFT JOIN transactions t ON MONTH(t.created_date) = m.month
GROUP BY m.month
SELECT * from
(
SELECT 1 as month UNION ALL
SELECT 2 as month UNION ALL
..
SELECT 12 as month
) as months
left outer join
(
select month(created_date) as "Month", count(created_date) as "Count" from transactions group by month(created_date)
) as data
on (months.month=data.month)
ORDER BY months.month