MySQL Group By Hours

Modifying on @eggyal answer, as another good use case would be to display day as well along with hours.

Suppose you need COUNT of records of past 7 days which is of 24 hour each.

SELECT
   dayname(date_sub(curdate(), INTERVAL 1 DAY)) AS Day,
   CONCAT(Hour, ':00-', Hour+1, ':00') AS Hour,
   COUNT(created) AS `usage`
FROM
   history
   RIGHT JOIN
      (
         SELECT  0 AS Hour
         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
         UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
         UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15
         UNION ALL SELECT 16 UNION ALL SELECT 17 UNION ALL SELECT 18
         UNION ALL SELECT 19 UNION ALL SELECT 20 UNION ALL SELECT 21
         UNION ALL SELECT 22 UNION ALL SELECT 23
      )
      AS totalhours
      ON HOUR(created) = HOUR
      AND DATE(created) = date_sub(curdate(), INTERVAL 1 DAY)
      OR created IS NULL
GROUP BY
   Hour
ORDER BY
   Hour;

SQL Result

Modifying this in the business logic of the backend code or in a stored procedure,

INTERVAL 1 DAY

can yield query results of past 7 days along with the name of the day.


Your existing query can be reduced to:

SELECT   CONCAT(HOUR(created), ':00-', HOUR(created)+1, ':00') AS Hours
  ,      COUNT(*) AS `usage`
FROM     history
WHERE    created BETWEEN '2012-02-07' AND NOW()
GROUP BY HOUR(created)

To display every hour, including those for which there is no data, you need to outer join with a table containing all the hours for which you want data. You can build such a table in your query using UNION:

SELECT   CONCAT(Hour, ':00-', Hour+1, ':00') AS Hours
  ,      COUNT(created) AS `usage`
FROM     history
  RIGHT JOIN (
                   SELECT  0 AS Hour
         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
         UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
         UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15
         UNION ALL SELECT 16 UNION ALL SELECT 17 UNION ALL SELECT 18
         UNION ALL SELECT 19 UNION ALL SELECT 20 UNION ALL SELECT 21
         UNION ALL SELECT 22 UNION ALL SELECT 23
  )      AS AllHours ON HOUR(created) = Hour
WHERE    created BETWEEN '2012-02-07' AND NOW() OR created IS NULL
GROUP BY Hour
ORDER BY Hour

However, the treatment of groups for which no data exists is really a matter for business logic that's best placed in your data access layer rather than in the database itself: indeed it should be trivial for your application to use a zero value whenever an hour is absent.


Given a table Log with columns ts and value, the following will give the hourly average for the last 24 hours (assuming each hour has at least one row).

SELECT 
    FROM_UNIXTIME( TRUNCATE( UNIX_TIMESTAMP( `ts` )/3600, 0 ) * 3600 ) as h,
    AVG( `value` ) as v,
    COUNT( * ) as q
FROM 
    Log
GROUP BY 
   h
ORDER BY 
   h desc
LIMIT
    24

The column ts can be a timestamp or datetime column while value is anything that AVG() will accept.