Search code examples
mysqljoincountfiscal

MySQL - count by month (including missing records)


I have this SELECT:

SELECT
  DATE_FORMAT(`created`, '%Y-%m') as byMonth,
  COUNT(*) AS Total 
FROM 
  `qualitaet`
WHERE
  `created` >= MAKEDATE(year(now()-interval 1 year),1) + interval 5 month
AND
  `status`=1
GROUP BY 
  YEAR(`created`), MONTH(`created`)
ORDER BY 
  YEAR(`created`) ASC

and get this result:

| byMonth | Total |
| 2015-06 |   2   |
| 2015-09 |  12   |
| 2015-10 |   3   |
| 2015-12 |   8   |
| 2016-01 |   1   |

see SQL-Fiddle here

The WHERE clause is important because i need it as current fiscal year starting on June, 1 in my example.

As you can see, i have no records for Jul, Aug and Nov. But i need this records with zero in Total.

So my result should look like this:

| byMonth | Total |
| 2015-06 |   2   |
| 2015-07 |   0   |
| 2015-08 |   0   |
| 2015-09 |  12   |
| 2015-10 |   3   |
| 2015-11 |   0   |
| 2015-12 |   8   |
| 2016-01 |   1   |

is there a way to get this result?


Solution

  • You need to generate all the wanted dates, and then left join your data to the dates. Note also that it is important to put some predicates in the left join's ON clause, and others in the WHERE clause:

    SELECT
      CONCAT(y, '-', LPAD(m, 2, '0')) as byMonth,
      COUNT(`created`) AS Total 
    FROM (
      SELECT year(now())     AS y UNION ALL
      SELECT year(now()) - 1 AS y 
    ) `years`
    CROSS JOIN (
      SELECT  1 AS m UNION ALL
      SELECT  2 AS m UNION ALL
      SELECT  3 AS m UNION ALL
      SELECT  4 AS m UNION ALL
      SELECT  5 AS m UNION ALL
      SELECT  6 AS m UNION ALL
      SELECT  7 AS m UNION ALL
      SELECT  8 AS m UNION ALL
      SELECT  9 AS m UNION ALL
      SELECT 10 AS m UNION ALL
      SELECT 11 AS m UNION ALL
      SELECT 12 AS m
    ) `months`
    LEFT JOIN `qualitaet` q
    ON YEAR(`created`) = y 
      AND MONTH(`created`) = m
      AND `status` = 1
    WHERE STR_TO_DATE(CONCAT(y, '-', m, '-01'), '%Y-%m-%d') 
        >= MAKEDATE(year(now()-interval 1 year),1) + interval 5 month
      AND STR_TO_DATE(CONCAT(y, '-', m, '-01'), '%Y-%m-%d') 
        <= now()
    GROUP BY y, m
    ORDER BY y, m
    

    How does the above work?

    • CROSS JOIN creates a cartesian product between all available years and all available months. This is what you want, you want all year-month combinations with no gaps.
    • LEFT JOIN adds all the qualitaet records to the result (if they exist) and joins them to the year-month cartesian product from before. It is important to put prediactes like the status = 1 predicate here.
    • COUNT(created) counts only non-NULL values of created, i.e. when the LEFT JOIN produces no rows for any given year-month, we want 0 as a result, not 1, i.e. we don't want to count the NULL value.

    A note on performance

    The above makes heavy use of string operations and date time arithmetic in your ON and WHERE predicates. This isn't going to perform for lots of data. In that case, you should better pre-truncate and index your year-months in the qualitaet table, and operate only on those values.