This actually turned out to be fairly painful. :) The details of this logic ran though my head most of last night... but in my current sleep-deprived state, I think I have devised a solution where every case is handled, and which does what we need in a reasonably-optimal fashion.
It returns the correct values for all of the test cases.
You need an index on (EventTime) so that the optimizer can turn the date-level tests into constants and use that index to find the matching rows by date range... then the server can filter the date-matched rows based on their time match in the
CASE statement of the where clause.
WHERE clause, the
CASE expression matches a condition in our data, and will yield a boolean value based on the truthiness of the expressions in the
THEN. This value will include a row if
TRUE and exclude the row if
I've used variables, but that's mostly for clarity... assuming there's any clarity to be found in what I've written, below, and to prevent errors since the literals are needed in so many different places.
SET @TargetDateTime = '2013-09-26 00:00:00';
SET @DateRange = 7;
SET @TimeRange = 15;
FROM EventTable E
-- must be at or after midnight the morning of the first valid day
WHERE EventTime >= DATE_SUB(DATE(@TargetDateTime), INTERVAL @DateRange DAY)
-- must be before midnight on the morning after the first valid day
AND EventTime < DATE_ADD(DATE(@TargetDateTime), INTERVAL (@DateRange + 1) DAY)
-- now, hold my beer and watch this...
-- easy first case, the time portions of both values are equal
WHEN TIME(EventTime) = TIME(@TargetDateTime)
-- in each case if the expression in the "THEN" evaluates to something true,
-- then the row will be returned if the other conditions in WHERE are also met
-- next case, if the event appears to be earlier than the target,
-- we match the row IF either...
WHEN TIME(EventTime) < TIME(@TargetDateTime)
-- ...the simple case, difference in seconds is within range
THEN TIME_TO_SEC(@TargetDateTime) - TIME_TO_SEC(EventTime)
BETWEEN 0 AND (@TimeRange * 60)
-- ...the complicated case, when we span midnight
86400 + TIME_TO_SEC(EventTime) - TIME_TO_SEC(@TargetDateTime) <= (@TimeRange * 60)
-- next case, event appears later than target, we match if either...
WHEN TIME(EventTime) > TIME(@TargetDateTime)
-- ... simple subtraction returns a value in range
THEN TIME_TO_SEC(EventTime) - TIME_TO_SEC(@TargetDateTime)
BETWEEN 0 AND (@TimeRange * 60)
-- ... we wrap around midnight
86400 + TIME_TO_SEC(@TargetDateTime) - TIME_TO_SEC(EventTime) <= (@TimeRange * 60)
-- ELSE isn't encountered unless we run into null values, in which case, no match
ELSE FALSE END; /* END of CASE expression */
A working demo of the final version is here: http://sqlfiddle.com/#!2/7b8e5/2
It is usually bad form in a
WHERE clause to use columns as arguments to functions, because this prevents the optimizer from using an available index, since it has to evaluate the data in every row of the table in order to evaluate the function. There's no harm, here, though, since an index can't help us match the times. This is because
DATETIME values are on the left hand side of the field, as stored, and the time values are on the right, and an index stops helping as soon as we encounter irrelevant information from left to right. (That's usually discussed in the context of multi-column indexes, but it also holds true, here.)
It's also often bad form in a
WHERE clause to use a
CASE expression because that too may prevent the optimizer from choosing the optimal path because the
CASE can prevent the optimizer from forming a proper understanding of the logic, but this is acceptable here because we have nothing but
AND in the
WHERE clause and two other conditions that are easily satisfied by an index.
Update: This code and the answer went through several iterations before arriving at its current (working) form, as you can see in the comments.
Essentially, what we are doing is considering the three cases than can be identified when trying to compare wall-clock time of day: the times are equal, A is earlier than B, or B is earlier than A. It seems like "absolute value" might come into play, but the problem we encounter is that when a time value has no date component, earlier and later aren't meaningful concepts when the times might span midnight. Without a date component, 00:05:00 and 23:55:00 are 23 hours and 50 minutes apart. With a date component, they could also be "n days and 10 minutes" apart. When the date component is missing but uninteresting (or already handled, in this case), we first check the simple case, subtracting the "earlier" from the "later" to see if they're within range, but when they're not, we add 86400 seconds to the "earlier" component, essentially making it "the same time, the following day"... then the subtraction can be done to see if the times are close enough together when they're calculated as having spanned midnight.