Here is your original query from the question
SELECT e.*, MAX(m.datetime) AS unread_last, COUNT(m.id) AS unread
FROM TAB_EVENT e
LEFT JOIN TAB_MESSAGE m ON e.id=m.event_id
WHERE ( m.`read` IS NULL OR m.`read` = 0)
GROUP BY e.id
ORDER BY m.datetime DESC, e.id ASC
LIMIT 10;
Maybe try refactoring the query in such a way that in executes in this sequence
- only collect necessary columns from TAB_MESSAGE
- apply LIMIT 10 against the collected rows from TAB_MESSAGE
- run the JOIN
- apply the MAX() and COUNT() last
Here is what I am proposing
SELECT e.*, MAX(m.datetime) AS unread_last, COUNT(m.id) AS unread
FROM
(
SELECT * FROM
(SELECT id,event_id,datetime FROM TAB_MESSAGE
WHERE read IS NULL OR read = 0
ORDER BY datetime DESC) mm
LIMIT 10
) m
LEFT JOIN TAB_EVENT e
ON e.id=m.event_id
ORDER BY m.datetime DESC, e.id ASC;
Give it a Try !!!
UPDATE 2012-02-21 17:06 EDT
SELECT e.*, MAX(m.datetime) AS unread_last, COUNT(m.id) AS unread
FROM
TAB_EVENT e LEFT JOIN
(
SELECT * FROM
(SELECT id,event_id,datetime FROM TAB_MESSAGE
WHERE read IS NULL OR read = 0
ORDER BY datetime DESC) mm
LIMIT 10
) m
ON e.id=m.event_id
ORDER BY m.datetime DESC, e.id ASC;
@Sebastian, I put the query back in the original join order. Please try this as well !!!
UPDATE 2012-02-21 17:11 EDT
Make sure the datetime field is indexed
ALTER TABLE TAB_MESSAGE ADD INDEX read_datetime_ndx (read,datetime);
Why are you determining a member's absence through another member's presence?
I guess there must be an events
table in your schema. And I would expect it to have a column called something like event_date
. It's the event_date
column that you should use to determine which events a member could visit (members.registration_date < events.event_date
).
After getting the list of members and their respective valid events, you can anti-join it to the attendance
table to see which member missed which (or how many) events.
Here's an example query illustrating what I just said. It implements the anti-join using LEFT JOIN
+ WHERE IS NULL
check:
SELECT
m.member_id,
e.event_id
FROM members m
INNER JOIN events e ON m.registration_date < e.event_date
LEFT JOIN attendance a ON m.member_id = a.member_id
AND e.event_id = a.event_id
WHERE a.member_id IS NULL
AND ... /* some other conditions to filter members and/or events, if necessary */
Or here's another example showing how you could count total events available to a member and those actually attended by him/her:
SELECT
m.member_id,
COUNT(e.event_id) AS total_events,
COUNT(a.event_id) AS attended_events,
FROM members m
INNER JOIN events e ON m.registration_date < e.event_date
LEFT JOIN attendance a ON m.member_id = a.member_id
AND e.event_id = a.event_id
WHERE /* some conditions as necessary */
Note that in the last example there's no IS NULL
check. It's because that query needs to get all the events per member. Missed events are just not counted by the COUNT()
function (because the corresponding a.event_id
value contains NULL in those cases).
UPDATE
Since, as per your comment, the events are recurring (and thus have no fixed dates), I would suggest adding and using an event_calendar
table, populated beforehand either manually or programmatically. Naturally, the events
table in the above examples would be replaced with event_calendar
.
Alternatively, if maintaining an event calendar is not an option, you could replace events
in your queries with
(
SELECT DISTINCT
event_id,
attendance_date AS event_date
/* or, if they are not purely dates:
CAST(attendance_date AS date) AS event_date
*/
FROM attendance
) e
(basically, @ypercube's suggestion with dates thrown in).
Best Answer
This is essentially a gaps-and-islands problem. And when I have my SQL Server hat on, I often solve this kind of problem with two
ROW_NUMBER()
calls. Sadly, MySQL, unlike many other major SQL products, does not supportROW_NUMBER()
, nor any other ranking function. To make up for that, however, you can use variable assignment in SELECTs, which MySQL does support (unlike many other major SQL products).Below is a solution followed by an explanation:
Basically, you start with joining
members
andevent_dates
to get all the event occurrences the members could have attended based on their membership dates. Then you throw in theattendance
table (via a left join) to flag the missed occurrences. Here's an example of what you get by this time:At this point, the resulting set needs to be sorted by
member_id, event_id
, because that is crucial to the subsequent rankings calculation.Two different rankings are produced for every row. One is a ranking within the row's partition of
(member_id, event_id)
(it is reset as soon as a new event or a new member is encountered). The other is a ranking within the specific group of consecutive event occurrences, either attended or missed, that the row belongs to (this ranking, in addition to being reset upon coming across a new member or event, also gets reset whenever the other group is encountered). And so you get something like this:As you may have noticed, the difference between the two ranking numbers is constant throughout the particular group of consecutive event occurrences of the same kind ("attended" or "missed") and is also unique for that group within its partition. Therefore, every such group can be identified by
member_id
,event_id
and the just mentioned difference.And it now remains simply to filter the events leaving just the missed ones, group the rows and get the necessary aggregated data, like the number of rows and, possibly, as in the query above, the dates of the first and the last event occurrence in the group. The number of rows is also used in an additional filter condition to omit groups with fewer rows than required.