Getting "filler" empty values for a GROUP BY - mysql

Assuming I want to get 'weekly' results from a date range, and the date range contains four separate weeks, but my database only has a record for one week, how can I get filler values for the remaining three weeks?
For example, given this date range: 6/2018 - 7/2018
I run this query:
SELECT
DATE_FORMAT(period, '%m %d') || ' - ' || DATE_FORMAT(period, '%m %d') AS period,
SUM(clicks) AS clicks
FROM tablename
WHERE period >= ? AND period <= ?
GROUP BY YEAR(period), WEEK(period)
With these table rows in the database:
| period | clicks |
| 07/01/2018T:00:00:00Z | 1000 |
And I get these query results:
| period | clicks |
| Jul 1 - Jul 5 | 1000 |
But I want to get these query results to cover default values for the empty weeks so I can use them to populate a d3 chart:
| period | clicks |
| Jul 1 - Jul 5 | 1000 |
| Jun 25 - Jun 23 | 0 |
| Jun 18 - Jun 24 | 0 |
| Jun 12 - Jun 18 | 0 |
Any ideas? This sort of "filler" phenomena should be generic enough to work with other intervals, such as daily, monthly, yearly, hourly.

rewrite your query as
ifnull( SUM(clicks),0) AS clicks

You can use this query to generate the weeks and then left join with your current query to get what you want.
DECLARE #StartingFromDate DATETIME = '2018-12-01';
DECLARE #EndingAtDate DATETIME = '2018-12-31';
WITH CTE_DateRange (DateRange)
AS (SELECT DATEADD(WEEK, DATEDIFF(WEEK, 0, #EndingAtDate) - DATEDIFF(WEEK, #StartingFromDate, #EndingAtDate), 0)
UNION ALL
SELECT DATEADD(WEEK, 1, DateRange)
FROM CTE_DateRange
WHERE DATEADD(WEEK, 1, DateRange) < #EndingAtDate)
SELECT CTE_DateRange.DateRange
FROM CTE_DateRange
WHERE CTE_DateRange.DateRange BETWEEN #StartingFromDate AND #EndingAtDate

Related

MySQL record between fixed date range

I have an small application which was build with CodeIgniter 3 and need to perform a report which will be converted to Chart.js. The report should be in yearly basis but at given specific date every month. The requirement are all data count must be from 4th to 3rd monthly. Like this:
For example January Report would be from 4th January to 3rd February, 4th February to 3rd March,... and so on.
I have created a MySQL query but I'm stuck on how to get the date too date. My Query are as follows:
SELECT DATE_FORMAT(odd_date_created, '%Y') as 'year',
DATE_FORMAT(odd_date_created, '%m') as 'month',
COUNT(odd_id) as 'total', status
FROM odd_data
WHERE status = $id and
GROUP BY DATE_FORMAT(odd_date_created, '%Y%m'), status
I'm new to MySQl. Could somebody help me on this. I'm stuck where should I put the date to date query.
Firstly I want to caution you not to use "between" with the following when you come to join your data, use this method instead data.date >= r.period_start_dt and data.date < r.period_end_dt
Secondly I am assuming your data does have dates or timestamps and that will fall between the calculated ranges that follow:
set #year :=2017;
select
*
from (
select
start_dt + INTERVAL m.n MONTH period_start_dt
, start_dt + INTERVAL m.n + 1 MONTH period_end_dt
from (
select str_to_date(concat(#year,'-01-04'),'%Y-%m-%d') start_dt ) seed
cross join (select 0 n 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
) m
) r
## LEFT JOIN YOUR DATA
## ON data.date >= r.period_start_dt and data.date < r.period_end_dt
Example ranges: (produce you own at this demo: http://rextester.com/CHTKSA95303 )
nb dd.mm.yyyy (.de format)
+----+---------------------+---------------------+
| | period_start_dt | period_end_dt |
+----+---------------------+---------------------+
| 1 | 04.01.2017 00:00:00 | 04.02.2017 00:00:00 |
| 2 | 04.02.2017 00:00:00 | 04.03.2017 00:00:00 |
| 3 | 04.03.2017 00:00:00 | 04.04.2017 00:00:00 |
| 4 | 04.04.2017 00:00:00 | 04.05.2017 00:00:00 |
| 5 | 04.05.2017 00:00:00 | 04.06.2017 00:00:00 |
| 6 | 04.06.2017 00:00:00 | 04.07.2017 00:00:00 |
| 7 | 04.07.2017 00:00:00 | 04.08.2017 00:00:00 |
| 8 | 04.08.2017 00:00:00 | 04.09.2017 00:00:00 |
| 9 | 04.09.2017 00:00:00 | 04.10.2017 00:00:00 |
| 10 | 04.10.2017 00:00:00 | 04.11.2017 00:00:00 |
| 11 | 04.11.2017 00:00:00 | 04.12.2017 00:00:00 |
| 12 | 04.12.2017 00:00:00 | 04.01.2018 00:00:00 |
+----+---------------------+---------------------+
Given the specification, I think I would tempted to cheat it... subtract 3 days from the date. Doing that, Jan 4 backs up to Jan 1, Feb 3 backs up to Jan 31... so those all end up as January.
SELECT DATE_FORMAT(odd_date_created + INTERVAL -3 DAY, '%Y') AS `year`
, DATE_FORMAT(odd_date_created + INTERVAL -3 DAY, '%m') AS `month`
, ...
FROM ...
GROUP
BY DATE_FORMAT(odd_date_created + INTERVAL -3 DAY, '%Y')
, DATE_FORMAT(odd_date_created + INTERVAL -3 DAY, '%m')
This falls apart if there's oddball ranges... if it's not always the 4th and 3rd.

Finding MAX and MIN values for each same start and end week

There is a query I am trying to implement in which I am not having much success with in trying to find the MAX and MIN for each week.
I have 2 Tables:
SYMBOL_DATA (contains open,high,low,close, and volume)
WEEKLY_LOOKUP (contains a list of weeks(no weekends) with a WEEK_START and WEEK_END)
**SYMBOL_DATA Example:**
OPEN, HIGH, LOW, CLOSE, VOLUME
23.22 26.99 21.45 22.49 34324995
WEEKLY_LOOKUP (contains a list of weeks(no weekends) with a WEEK_START and WEEK_END)
**WEEKLY_LOOKUP Example:**
WEEK_START WEEK_END
2016-01-25 2016-01-29
2016-01-18 2016-01-22
2016-01-11 2016-01-15
2016-01-04 2016-01-08
I am trying to find for each WEEK_START and WEEK_END the high and low for that particular week.
For instance, if the WEEK is WEEK_START=2016-01-11 and WEEK_END=2016-01-15, I would have
5 entries for that particular symbol listed:
DATE HIGH LOW
2016-01-15 96.38 93.54
2016-01-14 98.87 92.45
2016-01-13 100.50 95.21
2016-01-12 99.96 97.55
2016-01-11 98.60 95.39
2016-01-08 100.50 97.03
2016-01-07 101.43 97.30
2016-01-06 103.77 100.90
2016-01-05 103.71 101.67
2016-01-04 102.24 99.76
For each week_ending (2016-01-15) the HIGH is 100.50 on 2016-01-13 and the LOW is 92.45 on 2016-01-14
I attempted to write a query that gives me a list of highs and lows, but when I tried adding a MAX(HIGH), I had only 1 row returned back.
I tried a few more things in which I couldn't get the query to work (some sort of infinite run type). For now, I just have this that gives me a list of highs and lows for every day instead of the roll-up for each week which I am not sure how to do.
select date, t1.high, t1.low
from SYMBOL_DATA t1, WEEKLY_LOOKUP t2
where symbol='ABCDE' and (t1.date>=t2.START_DATE and t1.date<=t2.END_DATE)
and t1.date<=CURDATE()
LIMIT 30;
How can I get for each week (Start and End) the High_Date, MAX(High), and Low_Date, MIN(LOW) found each week? I probably don't need a
full history for a symbol, so a LIMIT of like 30 or (30 week periods) would be sufficient so I can see trending.
If I wanted to know for example each week MAX(High) and MIN(LOW) start week ending 2016-01-15 the result would show
**Result:**
WEEK_ENDING 2016-01-15 100.50 2016-01-13 92.45 2016-01-14
WEEK_ENDING 2016-01-08 103.77 2016-01-06 97.03 2016-01-08
etc
etc
Thanks to all of you with the expertise and knowledge. I greatly appreciate your help very much.
Edit
Once the Week Ending list is returned containing the MAX(HIGH) and MIN(LOW) for each week, is it possible then on how to find the MAX(HIGH) and MIN(LOW) from that result set so it return then only 1 entry from the 30 week periods?
Thank you!
To Piotr
select part1.end_date,part1.min_l,part1.max_h, s1.date, part1.min_l,s2.date from
(
select t2.start_date, t2.end_date, max(t1.high) max_h, min(t1.low) min_l
from SYMBOL_DATA t1, WEEKLY_LOOKUP t2
where symbol='FB'
and t1.date<='2016-01-22'
and (t1.date>=t2.START_DATE and t1.date<=t2.END_DATE)
group by t2.start_date, t2.end_date order by t1.date DESC LIMIT 1;
) part1, symbol_data s1, symbol_data s2
where part1.max_h = s1.high and part1.min_l = s2.low;
You will notice that the MAX and MIN for each week is staying roughly the same and not changing as it should be different for week to week for both the High and Low.
SQL Fiddle
I have abbreviated some of your names in my example.
Getting the high and low for each week is pretty simple; you just have to use GROUP BY:
SELECT s1.symbol, w.week_end, MAX(s1.high) AS weekly_high, MIN(s1.LOW) as weekly_low
FROM weeks AS w
INNER JOIN symdata AS s1 ON s1.zdate BETWEEN w.week_start AND w.week_end
GROUP BY s1.symbol, w.week_end
Results:
| symbol | week_end | weekly_high | weekly_low |
|--------|---------------------------|-------------|------------|
| ABCD | January, 08 2016 00:00:00 | 103.77 | 97.03 |
| ABCD | January, 15 2016 00:00:00 | 100.5 | 92.45 |
Unfortunately, getting the dates of the high and low requires that you re-join to the symbol_data table, based on the symbol, week and values. And even that doesn't do the job; you have to account for the possibility that there might be two days where the same high (or low) was achieved, and decide which one to choose. I arbitrarily chose the first occurrence in the week of the high and low. So to get that second level of choice, you need another GROUP BY. The whole thing winds up looking like this:
SELECT wl.symbol, wl.week_end, wl.weekly_high, MIN(hd.zdate) as high_date, wl.weekly_low, MIN(ld.zdate) as low_date
FROM (
SELECT s1.symbol, w.week_start, w.week_end, MAX(s1.high) AS weekly_high, MIN(s1.low) as weekly_low
FROM weeks AS w
INNER JOIN symdata AS s1 ON s1.zdate BETWEEN w.week_start AND w.week_end
GROUP BY s1.symbol, w.week_end) AS wl
INNER JOIN symdata AS hd
ON hd.zdate BETWEEN wl.week_start AND wl.week_end
AND hd.symbol = wl.symbol
AND hd.high = wl.weekly_high
INNER JOIN symdata AS ld
ON ld.zdate BETWEEN wl.week_start AND wl.week_end
AND ld.symbol = wl.symbol
AND ld.low = wl.weekly_low
GROUP BY wl.symbol, wl.week_start, wl.week_end, wl.weekly_high, wl.weekly_low
Results:
| symbol | week_end | weekly_high | high_date | weekly_low | low_date |
|--------|---------------------------|-------------|---------------------------|------------|---------------------------|
| ABCD | January, 08 2016 00:00:00 | 103.77 | January, 06 2016 00:00:00 | 97.03 | January, 08 2016 00:00:00 |
| ABCD | January, 15 2016 00:00:00 | 100.5 | January, 13 2016 00:00:00 | 92.45 | January, 14 2016 00:00:00 |
To get the global highs and lows, just remove the weekly table from the original query:
SELECT wl.symbol, wl.high, MIN(hd.zdate) as high_date, wl.low, MIN(ld.zdate) as low_date
FROM (
SELECT s1.symbol, MAX(s1.high) AS high, MIN(s1.low) as low
FROM symdata AS s1
GROUP BY s1.symbol) AS wl
INNER JOIN symdata AS hd
ON hd.symbol = wl.symbol
AND hd.high = wl.high
INNER JOIN symdata AS ld
ON ld.symbol = wl.symbol
AND ld.low = wl.low
GROUP BY wl.symbol, wl.high, wl.low
Results:
| symbol | high | high_date | low | low_date |
|--------|--------|---------------------------|-------|---------------------------|
| ABCD | 103.77 | January, 06 2016 00:00:00 | 92.45 | January, 14 2016 00:00:00 |
The week table seems entirely redundant...
SELECT symbol
, WEEK(zdate)
, MIN(low) min
, MAX(high) max_high
FROM symdata
GROUP
BY symbol, WEEK(zdate);
This is a simplified example. In reality, you might use DATE_FORMAT or something like that instead.
http://sqlfiddle.com/#!9/c247f/3
Check if following query produces desired result:
select part1.end_date,part1.min_l,part1.max_h, s1.date, part1.min_l,s2.date from
(
select t2.start_date, t2.end_date, max(t1.high) max_h, min(t1.low) min_l
from SYMBOL_DATA t1, WEEKLY_LOOKUP t2
where symbol='ABCDE'
and (t1.date>=t2.START_DATE and t1.date<=t2.END_DATE)
group by t2.start_date, t2.end_date
) part1, symbol_data s1, symbol_data s2
where part1.max_h = s1.high and part1.min_l = s2.low
and (s1.date >= part1.start_date and part1.end_date)
and (s2.date >= part1.start_date and part1.end_date)

Group and sum data based on a day of the month

I have a reoccurring payment day of 14th of each month and want to group a subset of data by month/year and sum the sent column. For example for the given data:-
Table `Counter`
Id Date Sent
1 10/04/2013 2
2 11/04/2013 4
3 15/04/2013 7
4 10/05/2013 3
5 14/05/2013 5
6 15/05/2013 3
7 16/05/2013 4
The output I want is something like:
From Count
14/03/2013 6
14/04/2013 10
14/05/2013 12
I am not worried how the from column is formatted or if its easier to split into month/year as I can recreated a date from multiple columns in the GUI. So the output could easily just be:
FromMth FromYr Count
03 2013 6
04 2013 10
05 2013 12
or even
toMth toYr Count
04 2013 6
05 2013 10
06 2013 12
If the payment date is for example the 31st then the date comparison would need to be the last date of each month. I am also not worried about missing months in the result-set.
I will also turn this into a Stored procedure so that I can push in the the payment date and other filtered criteria. It is also worth mentioning that we can go across years.
Try this query
select
if(day(STR_TO_DATE(date, "%Y-%d-%m")) >= 14,
concat('14/', month(STR_TO_DATE(date, "%Y-%d-%m")), '/', year(STR_TO_DATE(date, "%Y-%d-%m"))) ,
concat('14/', if ((month(STR_TO_DATE(date, "%Y-%d-%m")) - 1) = 0,
concat('12/', year(STR_TO_DATE(date, "%Y-%d-%m")) - 1),
concat(month(STR_TO_DATE(date, "%Y-%d-%m"))-1,'/',year(STR_TO_DATE(date, "%Y-%d-%m")))
)
)
) as fromDate,
sum(sent)
from tbl
group by fromDate
FIDDLE
| FROMDATE | SUM(SENT) |
--------------------------
| 14/10/2013 | 3 |
| 14/12/2012 | 1 |
| 14/3/2013 | 6 |
| 14/4/2013 | 10 |
| 14/5/2013 | 12 |
| 14/9/2013 | 1 |
Pay date could be grouped by months and year separatedly
select Sum(Sent) as "Count",
Extract(Month from Date - 13) as FromMth,
Extract(Year from Date - 13) as FromYr
from Counter
group by Extract(Year from Date - 13),
Extract(Month from Date - 13)
Be careful, since field's name "Date" coninsides with the keyword "date" in ANSISQL
I think the simplest way to do what you want is to just subtract 14 days rom the date and group by that month:
select date_format(date - 14, '%Y-%m'), sum(sent)
from counter
group by date_format(date - 14, '%Y-%m')

My SQL Finding a span of dates accross rows

I am looking for some help with even knowing where to start. Essentially we have a table for clients that hold employment start dates and end dates. For annual reports we have to calculate "continuous employment" which is defined as earliest start date to last end date as long as there is not more than 21 days between one end date and the next start date.
here is an example
employee | Start Date | End Date
1 | 2012-10-1 | 2012-11-05
1 | 2012-11-08 | 2013-1-25
2 | 2012-10-1 | 2012-11-05
2 | 2012-11-30 | 2013-1-02
in the above, i would like to see employee 1 as continuously employed from 2012-10-1 to 2013-1-25
but employee 2 would have 2 separate employment lines showing continuous employment from 2012-10-1 to 2012-11-05 and a different from 012-11-30 to 2013-1-02
Thanks for the help!
The theory is similar to #mellamokb's answer, but somewhat more concise:
SELECT employee, MIN(start) start, end
FROM (
SELECT #end:=IF(employee<=>#emp AND #stt<=end+INTERVAL 21 DAY,#end,end) end,
#stt:=start start,
#emp:=employee AS employee
FROM my_table, (SELECT #emp:=NULL, #stt:=0, #end:=0) init
ORDER BY employee, start DESC
) t
GROUP BY employee, end
See it on sqlfiddle.
One way to find "continuous groups" among a set of records is to use variables to track the difference between each line and develop groupings that combine continuous ranges together. In the example below, I use three variables to track enough information for generating the groups:
#curEmployee - tracks the current employee from the previous record, and is compared with the employee on the current record to know when we've switched to a different employee, which automatically becomes another grouping
#curEndDate - tracks the last end date from the previous record, so it can be compared with the start date of the current record to see if the current record belongs in the same "group" as the previous record - that is to say, it is part of continuous employment with the previous record
#curGroup - this is the key variable which segregates the rows into separate "groups" that represent continuous employment. The logic is that a row should be considered as continuous with the previous row if and only if the following two conditions are true: the two rows have the same employee number, and the end date of the previous row is less than 21 days from the current row.
NOTE: You may want to validate the edge conditions, i.e., whether exactly 20/21/22 days apart will be considered continuous employment or not, and tweak the logic below.
Here is the sample query that calculates those groups. A couple things to take note of: the order of variable assignment matters, because they are assigned from top to bottom in the select list. We need to assign #curGroup first, so that it still has the values of #curEmployee and #curEndDate from the previous record to draw on. Secondly, the order by clause is very important to ensure that when we are comparing the previous and current record, they are the two records that are the closest to each other. If we looked at the records in a random order, they would likely end up all as separate groups.
select
e.employee, e.`start date`, e.`end date`
,#curGroup :=
case when employee = #curEmployee
and #curEndDate + INTERVAL 21 DAY >= e.`start date`
then #curGroup
else #curGroup + 1
end as curGroup
,#curEmployee := employee as curEmployee
,#curEndDate := e.`end date` as curEndDate
from
employment e
JOIN (SELECT #curEmployee := 0, #curEndDate := NULL, #curGroup := 0) r
order by e.employee, e.`start date`
Sample Result (DEMO) - notice how CURGROUP stays at 1 for the first two lines, because they are within 21 days of each other and represent continuous employment, while the last two lines get identified as separate group numbers:
| EMPLOYEE | START DATE | END DATE | CURGROUP | CUREMPLOYEE | CURENDDATE |
-------------------------------------------------------------------------------------------------------------------------------
| 1 | October, 01 2012 00:00:00+0000 | November, 05 2012 00:00:00+0000 | 1 | 1 | 2012-11-05 00:00:00 |
| 1 | November, 08 2012 00:00:00+0000 | January, 25 2013 00:00:00+0000 | 1 | 1 | 2013-01-25 00:00:00 |
| 2 | October, 01 2012 00:00:00+0000 | November, 05 2012 00:00:00+0000 | 2 | 2 | 2012-11-05 00:00:00 |
| 2 | November, 30 2012 00:00:00+0000 | January, 02 2013 00:00:00+0000 | 3 | 2 | 2013-01-02 00:00:00 |
Now that we've established groups of records that are part of continuous employment, we merely need to group by those group numbers and find the minimum and maximum date range for the output:
select
employee,
min(`start date`) as `start date`,
max(`end date`) as `end date`
from (
select
e.employee, e.`start date`, e.`end date`
,#curGroup :=
case when employee = #curEmployee
and #curEndDate + INTERVAL 21 DAY >= e.`start date`
then #curGroup
else #curGroup + 1
end as curGroup
,#curEmployee := employee as curEmployee
,#curEndDate := e.`end date` as curEndDate
from
employment e
JOIN (SELECT #curEmployee := 0, #curEndDate := NULL, #curGroup := 0) r
order by e.employee, e.`start date`
) as T
group by curGroup
Sample Result (DEMO):
| EMPLOYEE | START DATE | END DATE |
--------------------------------------------------------------------------------
| 1 | October, 01 2012 00:00:00+0000 | January, 25 2013 00:00:00+0000 |
| 2 | October, 01 2012 00:00:00+0000 | November, 05 2012 00:00:00+0000 |
| 2 | November, 30 2012 00:00:00+0000 | January, 02 2013 00:00:00+0000 |

Grouping MySQL datetime into intervals irrespective of timezone

This question has been asked before but I am facing a slightly different problem.
I have a table which logs events and stores their timestamps (as datetime). I need to be able to break up time into chunks and get number of events that occurred in that interval. The interval can be custom (Say from 5 minutes to 1 hour and even beyond).
The obvious solution is to convert the datetime to unix_timestamp divide it by number of seconds in the interval, take its floor function and multiply it back by the number of seconds. Finally convert the unix_timestamp back to the datetime format.
This works fine for small intervals.
select
from_unixtime(floor(unix_timestamp(event.timestamp)/300)*300) as start_time,
count(*) as total
from event
where timestamp>='2012-08-03 00:00:00'
group by start_time;
This gives the correct output
+---------------------+-------+
| start_time | total |
+---------------------+-------+
| 2012-08-03 00:00:00 | 11 |
| 2012-08-03 00:05:00 | 4 |
| 2012-08-03 00:10:00 | 4 |
| 2012-08-03 00:15:00 | 7 |
| 2012-08-03 00:20:00 | 8 |
| 2012-08-03 00:25:00 | 1 |
| 2012-08-03 00:30:00 | 1 |
| 2012-08-03 00:35:00 | 3 |
| 2012-08-03 00:40:00 | 3 |
| 2012-08-03 00:45:00 | 5 |
~~~~~OUTPUT SNIPPED~~~~~~~~~~~~
But if I increase the interval to say 1 hour (3600 sec)
mysql> select from_unixtime(floor(unix_timestamp(event.timestamp)/3600)*3600) as start_time, count(*) as total from event where timestamp>='2012-08-03 00:00:00' group by start_time;
+---------------------+-------+
| start_time | total |
+---------------------+-------+
| 2012-08-02 23:30:00 | 35 |
| 2012-08-03 00:30:00 | 30 |
| 2012-08-03 01:30:00 | 12 |
| 2012-08-03 02:30:00 | 18 |
| 2012-08-03 03:30:00 | 12 |
| 2012-08-03 04:30:00 | 4 |
| 2012-08-03 05:30:00 | 3 |
| 2012-08-03 06:30:00 | 13 |
| 2012-08-03 07:30:00 | 269 |
| 2012-08-03 08:30:00 | 681 |
| 2012-08-03 09:30:00 | 1523 |
| 2012-08-03 10:30:00 | 911 |
+---------------------+-------+
The reason, as far as I could gauge, for the boundaries not being set properly is that unix_timestamp will convert time from my local timezone (GMT + 0530) to UTC and then output the numerical value.
So a value like 2012-08-03 00:00:00 will actually be 2012-08-02 18:30:00. Dividing and using floor will set the minutes part to 00. But when I use from_unixtime, it will convert it back to GMT + 0530 and hence give me intervals that begin at 30 mins.
How do I ensure the query works correctly irrespective of the timezone? I use MySQL 5.1.52 so to_seconds() is not available
EDIT:
The query should also fire correctly irrespective of the interval (can be hours, minutes, days). A generic solution would be appreciated
You can use TIMESTAMPDIFF to group by intervals of time:
For a specified interval of hours, you can use:
SELECT '2012-08-03 00:00:00' +
INTERVAL FLOOR(TIMESTAMPDIFF(HOUR, '2012-08-03 00:00:00', timestamp) / <n>) * <n> HOUR AS start_time,
COUNT(*) AS total
FROM event
WHERE timestamp >= '2012-08-03 00:00:00'
GROUP BY start_time
Replace the occurances of 2012-08-03 00:00:00 with your minimum input date.
<n> is your specified interval in hours (every 2 hours, 3 hours, etc.), and you can do the same for minutes:
SELECT '2012-08-03 00:00:00' +
INTERVAL FLOOR(TIMESTAMPDIFF(MINUTE, '2012-08-03 00:00:00', timestamp) / <n>) * <n> MINUTE AS start_time,
COUNT(*) AS total
FROM event
WHERE timestamp >= '2012-08-03 00:00:00'
GROUP BY start_time
Where <n> is your specified interval in minutes (every 45 minutes, 90 minutes, etc).
Be sure you're passing in your minimum input date (in this example 2012-08-03 00:00:00) as the second parameter to TIMESTAMPDIFF.
EDIT: If you don't want to worry about which interval unit to pick in the TIMESTAMPDIFF function, then of course just do the interval by seconds (300 = 5 minutes, 3600 = 1 hour, 7200 = 2 hours, etc.)
SELECT '2012-08-03 00:00:00' +
INTERVAL FLOOR(TIMESTAMPDIFF(SECOND, '2012-08-03 00:00:00', timestamp) / <n>) * <n> SECOND AS start_time,
COUNT(*) AS total
FROM event
WHERE timestamp >= '2012-08-03 00:00:00'
GROUP BY start_time
EDIT2: To address your comment pertaining to reducing the number of areas in the statement where you have to pass in your minimum parameter date, you can use:
SELECT b.mindate +
INTERVAL FLOOR(TIMESTAMPDIFF(SECOND, b.mindate, timestamp) / <n>) * <n> SECOND AS start_time,
COUNT(*) AS total
FROM event
JOIN (SELECT '2012-08-03 00:00:00' AS mindate) b ON timestamp >= b.mindate
GROUP BY start_time
And simply pass in your minimum datetime parameter once into the join subselect.
You can even make a second column in the join subselect for your seconds interval (e.g. 3600) and name the column something like secinterval... then change the <n>'s to b.secinterval, so you only have to pass in your minimum date parameter AND interval one time each.
SQLFiddle Demo
the easier method would be:
Method1
select date(timestamp) as date_timestamp, hour(timestamp) as hour_timestamp, count(*) as total
from event
where timestamp>='2012-08-03 00:00:00'
group by date_timestamp, hour_timestamp
if you would like to use your original approach.
Method2
select from_unixtime(floor(unix_timestamp(event.timestamp-1800)/3600)*3600+1800) as start_time,
count(*) as total
from event
where timestamp>='2012-08-03 00:00:00'
group by start_time;
EDIT1
for the first method, it also allows user to set different interval.
For example, if user wants the log to group by 15 minutes,
select date(time) as date_timestamp,
hour(time) as hour_timestamp,
floor(minute(time) as minute_timestamp / 15) * 15 as minute_timestamp
count(*) as total
from event
group by date_timestamp, hour_timestamp, minute_timestamp