Files
zoneminder/db/zm_update-1.39.28.sql
T
Isaac ConnorandClaude Opus 5 11bade086e perf: drop unused and duplicate secondary indexes
An audit of every table in zm_create.sql.in against the queries that read it
turned up seven indexes that are either never used or duplicate another index.

Logs.TimeKey duplicates Logs_TimeKey_idx, which zm_create.sql.in creates three
lines below it and zm_update-1.31.11.sql adds to upgraded installs. Both have
existed on every install since, and every log INSERT maintains both. Logs is
the second highest insert-rate table in the schema.

Stats.MonitorId and Stats.ZoneId are never read. Every query against Stats is
anchored on EventId: the per-frame zone stats view in skins functions.php and
ajax/stats.php, the ZoneId filter term in FilterTerm.php and Filter.pm, the
deletes in Event.php, Event.pm and zmaudit.pl, and the orphan scan. EventId_ZoneId
serves all of them. A Stats row is written per frame per zone when
ZM_RECORD_EVENT_STATS is on, which is the same insert-rate argument as the
Frames indexes in the previous commit.

EncoderTemplates.Encoder is the leftmost prefix of Encoder_Name, and the RoleId
indexes on Role_Groups_Permissions and Role_Monitors_Permissions are the
leftmost prefix of the UNIQUE index beside them. Those tables are small and
rarely written, so this is tidying rather than a saving.

Monitor_Status_UpdatedOn_idx is dropped by zm_update-1.37.76.sql but was left in
zm_create.sql.in, so fresh installs have carried it and upgraded installs have
not. Removed from zm_create.sql.in and repeated in the migration so the two
agree from here on.

Stats.MonitorId and Stats.ZoneId are the only two with no other index covering
their column, so their drop is guarded on there being no foreign key on the
column. 1.37.31 drops the Stats foreign keys only when they are named
Stats_ibfk_1..4; on an install where they survived under another name the drop
would fail with errno 150 and abort the upgrade, so it skips with a message
instead. Verified by building that case and confirming the migration completes.

Tested against MySQL 8.4: the schema built from master's zm_create.sql.in, with
1.39.27 and 1.39.28 applied, is index for index identical to a fresh install
from the updated zm_create.sql.in - 123 indexes across 55 tables. Both
migrations verified idempotent, and all 29 foreign keys survive.

Not changed: AI_Detections carries three single-column indexes and has no
consumer anywhere in the tree yet, so there is nothing to judge them against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:51:49 -04:00

70 lines
4.8 KiB
SQL

--
-- This updates a 1.39.27 database to 1.39.28
--
-- Drop secondary indexes that no query uses, or that duplicate another index.
--
-- Logs.TimeKey duplicates Logs_TimeKey_idx, created immediately below it in
-- zm_create.sql.in since 1.31.11. Both exist on every install, fresh and
-- upgraded, and every log INSERT has maintained both ever since.
--
-- Stats.MonitorId and Stats.ZoneId are never read. Every query against Stats is
-- anchored on EventId - the per-frame zone stats view, the ZoneId filter term
-- in FilterTerm.php and Filter.pm, the deletes in Event.php and Event.pm - and
-- EventId_ZoneId serves all of them. A Stats row is written per frame per zone
-- when ZM_RECORD_EVENT_STATS is on, so this is the same insert-rate argument as
-- the Frames indexes in 1.39.27.
--
-- EncoderTemplates.Encoder is the leftmost prefix of Encoder_Name, and the
-- RoleId indexes on the two role permission tables are the leftmost prefix of
-- the UNIQUE index beside them. Those tables are small and rarely written, so
-- this is tidying rather than a saving, folded in while we are here.
--
-- Monitor_Status_UpdatedOn_idx is dropped by zm_update-1.37.76.sql but was left
-- in zm_create.sql.in, so fresh installs have had it and upgraded installs have
-- not. This drop is repeated here only so that the two agree from 1.39.28 on;
-- installs that already ran 1.37.76 will report it as already removed.
--
--
-- Stats.MonitorId and Stats.ZoneId are the only two here with no other index
-- covering their column, so on an install still carrying a foreign key on those
-- columns - 1.37.31 drops them only when they are named Stats_ibfk_1..4 - the
-- drop would fail and abort the upgrade. Those two are guarded to skip instead.
--
set @exist := (select count(*) from information_schema.statistics where table_schema = database() and table_name = 'Logs' and index_name = 'TimeKey');
set @sqlstmt := if( @exist > 0, 'DROP INDEX `TimeKey` ON `Logs`', "SELECT 'TimeKey INDEX is already removed from Logs.'");
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
set @exist := (select count(*) from information_schema.statistics where table_schema = database() and table_name = 'Stats' and index_name = 'MonitorId');
set @fk := (select count(*) from information_schema.key_column_usage where table_schema = database() and table_name = 'Stats' and column_name = 'MonitorId' and referenced_table_name is not null);
set @sqlstmt := if( @exist = 0, "SELECT 'MonitorId INDEX is already removed from Stats.'", if( @fk > 0, "SELECT 'Keeping Stats.MonitorId: a FOREIGN KEY still needs it.'", 'DROP INDEX `MonitorId` ON `Stats`' ));
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
set @exist := (select count(*) from information_schema.statistics where table_schema = database() and table_name = 'Stats' and index_name = 'ZoneId');
set @fk := (select count(*) from information_schema.key_column_usage where table_schema = database() and table_name = 'Stats' and column_name = 'ZoneId' and referenced_table_name is not null);
set @sqlstmt := if( @exist = 0, "SELECT 'ZoneId INDEX is already removed from Stats.'", if( @fk > 0, "SELECT 'Keeping Stats.ZoneId: a FOREIGN KEY still needs it.'", 'DROP INDEX `ZoneId` ON `Stats`' ));
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
set @exist := (select count(*) from information_schema.statistics where table_schema = database() and table_name = 'EncoderTemplates' and index_name = 'Encoder');
set @sqlstmt := if( @exist > 0, 'DROP INDEX `Encoder` ON `EncoderTemplates`', "SELECT 'Encoder INDEX is already removed from EncoderTemplates.'");
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
set @exist := (select count(*) from information_schema.statistics where table_schema = database() and table_name = 'Role_Groups_Permissions' and index_name = 'Role_Groups_Permissions_RoleId_idx');
set @sqlstmt := if( @exist > 0, 'DROP INDEX `Role_Groups_Permissions_RoleId_idx` ON `Role_Groups_Permissions`', "SELECT 'Role_Groups_Permissions_RoleId_idx INDEX is already removed from Role_Groups_Permissions.'");
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
set @exist := (select count(*) from information_schema.statistics where table_schema = database() and table_name = 'Role_Monitors_Permissions' and index_name = 'Role_Monitors_Permissions_RoleId_idx');
set @sqlstmt := if( @exist > 0, 'DROP INDEX `Role_Monitors_Permissions_RoleId_idx` ON `Role_Monitors_Permissions`', "SELECT 'Role_Monitors_Permissions_RoleId_idx INDEX is already removed from Role_Monitors_Permissions.'");
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
set @exist := (select count(*) from information_schema.statistics where table_schema = database() and table_name = 'Monitor_Status' and index_name = 'Monitor_Status_UpdatedOn_idx');
set @sqlstmt := if( @exist > 0, 'DROP INDEX `Monitor_Status_UpdatedOn_idx` ON `Monitor_Status`', "SELECT 'Monitor_Status_UpdatedOn_idx INDEX is already removed from Monitor_Status.'");
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;