Files
zoneminder/web/includes
Isaac Connor 022410c299 fix: stop abandoned events matching every DateTime window, and index the query
A DateTime lower bound was written as

  COALESCE(E.EndDateTime, '9999-12-31 23:59:59') >= T1

reading "an event that has not ended yet never ends". EndDateTime is NULL for
an event that is still recording, but also for one zmc was killed part way
through, and that stays NULL forever - so every abandoned event matched every
window from then on. Wrapping the column in a function also meant no index
could range over it: with the only MonitorId key being MonitorId alone, every
montage review request read all of that monitor's events and filtered them in
memory, however narrow the window.

Length tells the two cases apart. It is flushed during recording, so a live
event's effective end keeps advancing while an abandoned one's is frozen at
whatever was recorded. Use the same CASE the SELECT list in ajax/events.php
already uses, and bound it below by StartDateTime.

The lower bound now emits three conjuncts:

  E.StartDateTime >= DATE_SUB(T1, INTERVAL 1 DAY)
  AND (E.EndDateTime IS NULL OR E.EndDateTime >= T1)
  AND <effective end> >= T1

The floor bounds the scan for a window in the past and drops events abandoned
more than a day earlier - events do not outlive the nightly logrotate SIGHUP,
which stops and restarts them, so a day is comfortably beyond any real event.
The middle conjunct is implied by the third and exists only to give the
optimiser a second indexable handle: it is narrow exactly when the floor is
wide, so between them a window at either end of the retention period has
something cheap to range over. Verified the optimiser picks correctly for both,
unprompted. The third is the residual that gets the semantics right.

Add the two composite keys those conjuncts need. Two range columns cannot both
narrow one B-tree, and these are mirror images - EndDateTime >= T1 is
open-ended upwards, StartDateTime <= T2 downwards:

  Events_MonitorId_StartDateTime_idx (MonitorId, StartDateTime)
  Events_EndDateTime_MonitorId_idx   (EndDateTime, MonitorId)

MonitorId leads the first because it is the equality; past a range column later
columns can no longer narrow the scan, and (StartDateTime, MonitorId) measured
3.5x slower for the same rows. EndDateTime leads the second so it also covers
zmaudit's hunt for events that were never closed, which has no monitor to scope
it - 9 rows with a key against a 22,948 row table scan without.

Both replacements are added before the keys they supersede are dropped:

  Events_MonitorId_idx (MonitorId) is now a leftmost prefix of the new key.
  Events_EndDateTime_DiskSpace (EndDateTime, DiskSpace) existed for the scan
  that hunted events with no DiskSpace set; DiskSpace is set when the event is
  finalised in C++, so nothing scans for it any more.

Net index count on Events is unchanged.

Measured on a 7,167 event monitor. Default one hour window: 7,055 rows
examined / 25.4ms -> 173 rows / 4ms. Window scrubbed a week back, which
persists in the zmFilter_StartDateTime cookie: 6,995 rows / 28ms -> 19 rows /
0.15ms.

Adds migration db/zm_update-1.39.22.sql. Extends t/filter_sql.t; the PHP and Perl were
checked to emit identical SQL, and the semantics checked against a table of
seven event shapes - abandoned long ago, abandoned recently, overlapping,
inside, before, still recording, and still recording for 17 hours.
2026-08-19 22:06:21 -05:00
..