diff --git a/db/CMakeLists.txt b/db/CMakeLists.txt index 236049b62c0..f0499e3a501 100644 --- a/db/CMakeLists.txt +++ b/db/CMakeLists.txt @@ -8,6 +8,7 @@ configure_file(zm_update-1.37.4.sql.in "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1. configure_file(zm_update-1.37.69.sql.in "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.37.69.sql" @ONLY) configure_file(zm_update-1.37.74.sql.in "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.37.74.sql" @ONLY) configure_file(zm_update-1.37.77.sql.in "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.37.77.sql" @ONLY) +configure_file(zm_update-1.37.78.sql.in "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.37.78.sql" @ONLY) # install zm_create.sql install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zm_create.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") @@ -27,6 +28,8 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.37.69.sql" DESTINATION "$ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.37.74.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") # install zm_update-1.37.77.sql install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.37.77.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") +# install zm_update-1.37.78.sql +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.37.78.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") # Install the database upgrade scripts install(FILES ${dbfileslist} DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") diff --git a/db/triggers.sql b/db/triggers.sql index b6e64ee11b4..ff0157b5082 100644 --- a/db/triggers.sql +++ b/db/triggers.sql @@ -1,220 +1,29 @@ +DELIMITER // -delimiter // DROP TRIGGER IF EXISTS Events_Hour_delete_trigger// -CREATE TRIGGER Events_Hour_delete_trigger BEFORE DELETE ON Events_Hour -FOR EACH ROW BEGIN - UPDATE Event_Summaries SET - HourEvents = GREATEST(COALESCE(HourEvents,1)-1,0), - HourEventDiskSpace=GREATEST(COALESCE(HourEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0),0) - WHERE Event_Summaries.MonitorId=OLD.MonitorId; -END; -// DROP TRIGGER IF EXISTS Events_Hour_update_trigger// -CREATE TRIGGER Events_Hour_update_trigger AFTER UPDATE ON Events_Hour -FOR EACH ROW - BEGIN - declare diff BIGINT default 0; - - set diff = COALESCE(NEW.DiskSpace,0) - COALESCE(OLD.DiskSpace,0); - IF ( diff ) THEN - IF ( NEW.MonitorID != OLD.MonitorID ) THEN - UPDATE Event_Summaries SET HourEventDiskSpace=GREATEST(COALESCE(HourEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0),0) WHERE Event_Summaries.MonitorId=OLD.MonitorId; - UPDATE Event_Summaries SET HourEventDiskSpace=COALESCE(HourEventDiskSpace,0)+COALESCE(NEW.DiskSpace,0) WHERE Event_Summaries.MonitorId=NEW.MonitorId; - ELSE - UPDATE Event_Summaries SET HourEventDiskSpace=COALESCE(HourEventDiskSpace,0)+diff WHERE Event_Summaries.MonitorId=NEW.MonitorId; - END IF; - END IF; - END; -// - DROP TRIGGER IF EXISTS Events_Day_delete_trigger// -CREATE TRIGGER Events_Day_delete_trigger BEFORE DELETE ON Events_Day -FOR EACH ROW BEGIN - UPDATE Event_Summaries SET - DayEvents = GREATEST(COALESCE(DayEvents,1)-1,0), - DayEventDiskSpace=GREATEST(COALESCE(DayEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0),0) - WHERE Event_Summaries.MonitorId=OLD.MonitorId; -END; -// - -DROP TRIGGER IF EXISTS Events_Day_update_trigger; -CREATE TRIGGER Events_Day_update_trigger AFTER UPDATE ON Events_Day -FOR EACH ROW - BEGIN - declare diff BIGINT default 0; - - set diff = COALESCE(NEW.DiskSpace,0) - COALESCE(OLD.DiskSpace,0); - IF ( diff ) THEN - IF ( NEW.MonitorID != OLD.MonitorID ) THEN - UPDATE Event_Summaries SET DayEventDiskSpace=GREATEST(COALESCE(DayEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0),0) WHERE Event_Summaries.MonitorId=OLD.MonitorId; - UPDATE Event_Summaries SET DayEventDiskSpace=COALESCE(DayEventDiskSpace,0)+COALESCE(NEW.DiskSpace,0) WHERE Event_Summaries.MonitorId=NEW.MonitorId; - ELSE - UPDATE Event_Summaries SET DayEventDiskSpace=GREATEST(COALESCE(DayEventDiskSpace,0)+diff,0) WHERE Event_Summaries.MonitorId=NEW.MonitorId; - END IF; - END IF; - END; - // +DROP TRIGGER IF EXISTS Events_Day_update_trigger// DROP TRIGGER IF EXISTS Events_Week_delete_trigger// -CREATE TRIGGER Events_Week_delete_trigger BEFORE DELETE ON Events_Week -FOR EACH ROW BEGIN - UPDATE Event_Summaries SET - WeekEvents = GREATEST(COALESCE(WeekEvents,1)-1,0), - WeekEventDiskSpace=GREATEST(COALESCE(WeekEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0),0) - WHERE Event_Summaries.MonitorId=OLD.MonitorId; -END; -// -DROP TRIGGER IF EXISTS Events_Week_update_trigger; -CREATE TRIGGER Events_Week_update_trigger AFTER UPDATE ON Events_Week -FOR EACH ROW - BEGIN - declare diff BIGINT default 0; - - set diff = COALESCE(NEW.DiskSpace,0) - COALESCE(OLD.DiskSpace,0); - IF ( diff ) THEN - IF ( NEW.MonitorID != OLD.MonitorID ) THEN - UPDATE Event_Summaries SET WeekEventDiskSpace=GREATEST(COALESCE(WeekEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0),0) WHERE Event_Summaries.MonitorId=OLD.MonitorId; - UPDATE Event_Summaries SET WeekEventDiskSpace=COALESCE(WeekEventDiskSpace,0)+COALESCE(NEW.DiskSpace,0) WHERE Event_Summaries.MonitorId=NEW.MonitorId; - ELSE - UPDATE Event_Summaries SET WeekEventDiskSpace=GREATEST(COALESCE(WeekEventDiskSpace,0)+diff,0) WHERE Event_Summaries.MonitorId=NEW.MonitorId; - END IF; - END IF; - END; - // +DROP TRIGGER IF EXISTS Events_Week_update_trigger// DROP TRIGGER IF EXISTS Events_Month_delete_trigger// -CREATE TRIGGER Events_Month_delete_trigger BEFORE DELETE ON Events_Month -FOR EACH ROW BEGIN - UPDATE Event_Summaries SET - MonthEvents = GREATEST(COALESCE(MonthEvents,1)-1,0), - MonthEventDiskSpace=GREATEST(COALESCE(MonthEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0),0) - WHERE Event_Summaries.MonitorId=OLD.MonitorId; -END; -// - -DROP TRIGGER IF EXISTS Events_Month_update_trigger; -CREATE TRIGGER Events_Month_update_trigger AFTER UPDATE ON Events_Month -FOR EACH ROW - BEGIN - declare diff BIGINT default 0; - - set diff = COALESCE(NEW.DiskSpace,0) - COALESCE(OLD.DiskSpace,0); - IF ( diff ) THEN - IF ( NEW.MonitorID != OLD.MonitorID ) THEN - UPDATE Event_Summaries SET MonthEventDiskSpace=GREATEST(COALESCE(MonthEventDiskSpace,0)-COALESCE(OLD.DiskSpace),0) WHERE Event_Summaries.MonitorId=OLD.MonitorId; - UPDATE Event_Summaries SET MonthEventDiskSpace=COALESCE(MonthEventDiskSpace,0)+COALESCE(NEW.DiskSpace) WHERE Event_Summaries.MonitorId=NEW.MonitorId; - ELSE - UPDATE Event_Summaries SET MonthEventDiskSpace=GREATEST(COALESCE(MonthEventDiskSpace,0)+diff,0) WHERE Event_Summaries.MonitorId=NEW.MonitorId; - END IF; - END IF; - END; - // - -drop procedure if exists update_storage_stats// -drop trigger if exists event_update_trigger// - -CREATE TRIGGER event_update_trigger AFTER UPDATE ON Events -FOR EACH ROW -BEGIN - declare diff BIGINT default 0; - set diff = COALESCE(NEW.DiskSpace,0) - COALESCE(OLD.DiskSpace,0); +DROP TRIGGER IF EXISTS Events_Month_update_trigger// - IF ( diff ) THEN - UPDATE Events_Hour SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; - UPDATE Events_Day SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; - UPDATE Events_Week SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; - UPDATE Events_Month SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; - UPDATE Event_Summaries - SET - TotalEventDiskSpace = GREATEST(COALESCE(TotalEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0) + COALESCE(NEW.DiskSpace,0),0) - WHERE Event_Summaries.MonitorId=OLD.MonitorId; - END IF; +DROP PROCEDURE IF EXISTS update_storage_stats// - IF ( NEW.Archived != OLD.Archived ) THEN - IF ( NEW.Archived ) THEN - INSERT INTO Events_Archived (EventId,MonitorId,DiskSpace) VALUES (NEW.Id,NEW.MonitorId,NEW.DiskSpace); - INSERT INTO Event_Summaries (MonitorId,ArchivedEvents,ArchivedEventDiskSpace) VALUES (NEW.MonitorId,1,NEW.DiskSpace) ON DUPLICATE KEY - UPDATE ArchivedEvents = COALESCE(ArchivedEvents,0)+1, ArchivedEventDiskSpace = COALESCE(ArchivedEventDiskSpace,0) + COALESCE(NEW.DiskSpace,0); - ELSEIF ( OLD.Archived ) THEN - DELETE FROM Events_Archived WHERE EventId=OLD.Id; - UPDATE Event_Summaries - SET - ArchivedEvents = GREATEST(COALESCE(ArchivedEvents,0)-1,0), - ArchivedEventDiskSpace = GREATEST(COALESCE(ArchivedEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0),0) - WHERE Event_Summaries.MonitorId=OLD.MonitorId; - ELSE - IF ( OLD.DiskSpace != NEW.DiskSpace ) THEN - UPDATE Events_Archived SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; - UPDATE Event_Summaries SET - ArchivedEventDiskSpace = GREATEST(COALESCE(ArchivedEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0) + COALESCE(NEW.DiskSpace,0),0) - WHERE Event_Summaries.MonitorId=OLD.MonitorId; - END IF; - END IF; - ELSEIF ( NEW.Archived AND diff ) THEN - UPDATE Events_Archived SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; - END IF; - -END; - -// +DROP TRIGGER IF EXISTS event_update_trigger// DROP TRIGGER IF EXISTS event_insert_trigger// -/* The assumption is that when an Event is inserted, it has no size yet, so don't bother updating the DiskSpace, just the count. - * The DiskSpace will get update in the Event Update Trigger - */ -/* -CREATE TRIGGER event_insert_trigger AFTER INSERT ON Events -FOR EACH ROW - BEGIN - - INSERT INTO Events_Hour (EventId,MonitorId,StartDateTime,DiskSpace) VALUES (NEW.Id,NEW.MonitorId,NEW.StartDateTime,0); - INSERT INTO Events_Day (EventId,MonitorId,StartDateTime,DiskSpace) VALUES (NEW.Id,NEW.MonitorId,NEW.StartDateTime,0); - INSERT INTO Events_Week (EventId,MonitorId,StartDateTime,DiskSpace) VALUES (NEW.Id,NEW.MonitorId,NEW.StartDateTime,0); - INSERT INTO Events_Month (EventId,MonitorId,StartDateTime,DiskSpace) VALUES (NEW.Id,NEW.MonitorId,NEW.StartDateTime,0); - INSERT INTO Event_Summaries (MonitorId,HourEvents,DayEvents,WeekEvents,MonthEvents,TotalEvents) VALUES (NEW.MonitorId,1,1,1,1,1) ON DUPLICATE KEY - UPDATE - HourEvents = COALESCE(HourEvents,0)+1, - DayEvents = COALESCE(DayEvents,0)+1, - WeekEvents = COALESCE(WeekEvents,0)+1, - MonthEvents = COALESCE(MonthEvents,0)+1, - TotalEvents = COALESCE(TotalEvents,0)+1; -END; -// -*/ - DROP TRIGGER IF EXISTS event_delete_trigger// -CREATE TRIGGER event_delete_trigger BEFORE DELETE ON Events -FOR EACH ROW -BEGIN - DELETE FROM Events_Hour WHERE EventId=OLD.Id; - DELETE FROM Events_Day WHERE EventId=OLD.Id; - DELETE FROM Events_Week WHERE EventId=OLD.Id; - DELETE FROM Events_Month WHERE EventId=OLD.Id; - IF ( OLD.Archived ) THEN - DELETE FROM Events_Archived WHERE EventId=OLD.Id; - UPDATE Event_Summaries SET - ArchivedEvents = GREATEST(COALESCE(ArchivedEvents,1) - 1,0), - ArchivedEventDiskSpace = GREATEST(COALESCE(ArchivedEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0),0), - TotalEvents = GREATEST(COALESCE(TotalEvents,1) - 1,0), - TotalEventDiskSpace = GREATEST(COALESCE(TotalEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0),0) - WHERE Event_Summaries.MonitorId=OLD.MonitorId; - ELSE - UPDATE Event_Summaries SET - TotalEvents = GREATEST(COALESCE(TotalEvents,1)-1,0), - TotalEventDiskSpace=GREATEST(COALESCE(TotalEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0),0) - WHERE Event_Summaries.MonitorId=OLD.MonitorId; - END IF; -END; - -// - DROP TRIGGER IF EXISTS Zone_Insert_Trigger// CREATE TRIGGER Zone_Insert_Trigger AFTER INSERT ON Zones FOR EACH ROW @@ -222,6 +31,7 @@ FOR EACH ROW UPDATE Monitors SET ZoneCount=(SELECT COUNT(*) FROM Zones WHERE MonitorId=NEW.MonitorId) WHERE Monitors.Id=NEW.MonitorID; END // + DROP TRIGGER IF EXISTS Zone_Delete_Trigger// CREATE TRIGGER Zone_Delete_Trigger AFTER DELETE ON Zones FOR EACH ROW diff --git a/db/views.sql b/db/views.sql new file mode 100644 index 00000000000..84be8d7f79a --- /dev/null +++ b/db/views.sql @@ -0,0 +1,169 @@ +DELIMITER // + +-- This index covers MonitorId grouping, StartDateTime ranges, +-- Archived status filtering, and includes DiskSpace for summation. +CREATE INDEX IF NOT EXISTS Events_Summaries_Performance_idx +ON Events (MonitorId, StartDateTime, Archived, DiskSpace)// + +-- Clean up a previous typoed version of the index +DROP INDEX IF EXISTS Events_Summaries_Perfomance_idx ON Events// + +-- Hourly Events View +CREATE OR REPLACE VIEW Events_Hour AS +SELECT + Id AS EventId, + MonitorId, + DiskSpace, + StartDateTime +FROM Events +WHERE StartDateTime >= (NOW() - INTERVAL 1 HOUR)// + +-- Daily Events View +CREATE OR REPLACE VIEW Events_Day AS +SELECT + Id AS EventId, + MonitorId, + DiskSpace, + StartDateTime +FROM Events +WHERE StartDateTime >= (NOW() - INTERVAL 1 DAY)// + +-- Weekly Events View +CREATE OR REPLACE VIEW Events_Week AS +SELECT + Id AS EventId, + MonitorId, + DiskSpace, + StartDateTime +FROM Events +WHERE StartDateTime >= (NOW() - INTERVAL 7 DAY)// + +-- Monthly Events View +CREATE OR REPLACE VIEW Events_Month AS +SELECT + Id AS EventId, + MonitorId, + DiskSpace, + StartDateTime +FROM Events +WHERE StartDateTime >= (NOW() - INTERVAL 1 MONTH)// + +-- Archived Events View +CREATE OR REPLACE VIEW Events_Archived AS +SELECT + Id AS EventId, + MonitorId, + DiskSpace +FROM Events +WHERE Archived = 1// + +-- Event Summaries Source View (used by the refresh procedure) +CREATE OR REPLACE VIEW VIEW_Event_Summaries AS +SELECT + MonitorId, + -- Hour Stats + COUNT(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 HOUR) THEN 1 END) AS HourEvents, + COALESCE(SUM(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 HOUR) THEN DiskSpace ELSE 0 END), 0) AS HourEventDiskSpace, + + -- Day Stats + COUNT(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 DAY) THEN 1 END) AS DayEvents, + COALESCE(SUM(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 DAY) THEN DiskSpace ELSE 0 END), 0) AS DayEventDiskSpace, + + -- Week Stats + COUNT(CASE WHEN StartDateTime >= (NOW() - INTERVAL 7 DAY) THEN 1 END) AS WeekEvents, + COALESCE(SUM(CASE WHEN StartDateTime >= (NOW() - INTERVAL 7 DAY) THEN DiskSpace ELSE 0 END), 0) AS WeekEventDiskSpace, + + -- Month Stats + COUNT(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 MONTH) THEN 1 END) AS MonthEvents, + COALESCE(SUM(CASE WHEN StartDateTime >= (NOW() - INTERVAL 1 MONTH) THEN DiskSpace ELSE 0 END), 0) AS MonthEventDiskSpace, + + -- Archive Stats + COUNT(CASE WHEN Archived = 1 THEN 1 END) AS ArchivedEvents, + COALESCE(SUM(CASE WHEN Archived = 1 THEN DiskSpace ELSE 0 END), 0) AS ArchivedEventDiskSpace, + + -- Totals + COUNT(Id) AS TotalEvents, + COALESCE(SUM(DiskSpace), 0) AS TotalEventDiskSpace +FROM Events +GROUP BY MonitorId// + +-- Event Summaries snapshot table (SWR pattern) +CREATE TABLE IF NOT EXISTS `Event_Summaries` ( + `MonitorId` int(10) unsigned NOT NULL, + `HourEvents` int(10) DEFAULT 0, + `HourEventDiskSpace` bigint DEFAULT 0, + `DayEvents` int(10) DEFAULT 0, + `DayEventDiskSpace` bigint DEFAULT 0, + `WeekEvents` int(10) DEFAULT 0, + `WeekEventDiskSpace` bigint DEFAULT 0, + `MonthEvents` int(10) DEFAULT 0, + `MonthEventDiskSpace` bigint DEFAULT 0, + `ArchivedEvents` int(10) DEFAULT 0, + `ArchivedEventDiskSpace` bigint DEFAULT 0, + `TotalEvents` int(10) DEFAULT 0, + `TotalEventDiskSpace` bigint DEFAULT 0, + PRIMARY KEY (`MonitorId`) +) ENGINE=InnoDB// + +-- Metadata table for SWR staleness tracking +CREATE TABLE IF NOT EXISTS `Event_Summaries_Metadata` ( + `table_name` VARCHAR(64) NOT NULL, + `last_updated` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00', + PRIMARY KEY (`table_name`) +) ENGINE=InnoDB// + +INSERT IGNORE INTO `Event_Summaries_Metadata` + (`table_name`, `last_updated`) VALUES ('Event_Summaries', '1970-01-01 00:00:00')// + +-- Stored procedure: atomic refresh of Event_Summaries with GET_LOCK to prevent thundering herd +DROP PROCEDURE IF EXISTS `Refresh_Summaries_SWR`// + +CREATE PROCEDURE `Refresh_Summaries_SWR`() +proc: BEGIN + DECLARE v_lock_result INT DEFAULT 0; + DECLARE v_last DATETIME; + + -- Non-blocking lock: skip if another process is already refreshing + SET v_lock_result = GET_LOCK('refresh_summaries_lock', 0); + IF v_lock_result != 1 THEN + -- Another process holds the lock; return immediately (stale read is fine) + LEAVE proc; + END IF; + + -- Double-check staleness inside lock + SELECT `last_updated` INTO v_last + FROM `Event_Summaries_Metadata` + WHERE `table_name` = 'Event_Summaries'; + + IF v_last IS NOT NULL AND TIMESTAMPDIFF(SECOND, v_last, NOW()) < 60 THEN + DO RELEASE_LOCK('refresh_summaries_lock'); + LEAVE proc; + END IF; + + -- Atomic rename pattern: build new table, swap, drop old + DROP TABLE IF EXISTS `Event_Summaries_New`; + CREATE TABLE `Event_Summaries_New` LIKE `Event_Summaries`; + INSERT INTO `Event_Summaries_New` SELECT * FROM `VIEW_Event_Summaries`; + + DROP TABLE IF EXISTS `Event_Summaries_Old`; + RENAME TABLE `Event_Summaries` TO `Event_Summaries_Old`, + `Event_Summaries_New` TO `Event_Summaries`; + DROP TABLE IF EXISTS `Event_Summaries_Old`; + + -- Update metadata timestamp + UPDATE `Event_Summaries_Metadata` + SET `last_updated` = NOW() + WHERE `table_name` = 'Event_Summaries'; + + DO RELEASE_LOCK('refresh_summaries_lock'); +END proc// + +-- MySQL EVENT for background refresh every 600 seconds (10 minutes) +-- Note: Requires event_scheduler=ON in my.cnf or SET GLOBAL event_scheduler = ON; +DROP EVENT IF EXISTS `Event_Summaries_Refresh_Event`// + +CREATE EVENT IF NOT EXISTS `Event_Summaries_Refresh_Event` + ON SCHEDULE EVERY 600 SECOND + DO CALL `Refresh_Summaries_SWR`()// + +DELIMITER ; diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index abe60df6fcd..25478bfb755 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -225,60 +225,6 @@ CREATE TABLE `Events` ( KEY `Events_EndDateTime_DiskSpace` (`EndDateTime`,`DiskSpace`) ) ENGINE=@ZM_MYSQL_ENGINE@; -DROP TABLE IF EXISTS `Events_Hour`; -CREATE TABLE `Events_Hour` ( - `EventId` BIGINT unsigned NOT NULL, - `MonitorId` int(10) unsigned NOT NULL, - `StartDateTime` datetime default NULL, - `DiskSpace` bigint default NULL, - PRIMARY KEY (`EventId`), - KEY `Events_Hour_MonitorId_idx` (`MonitorId`), - KEY `Events_Hour_StartDateTime_idx` (`StartDateTime`) -) ENGINE=@ZM_MYSQL_ENGINE@; - -DROP TABLE IF EXISTS `Events_Day`; -CREATE TABLE `Events_Day` ( - `EventId` BIGINT unsigned NOT NULL, - `MonitorId` int(10) unsigned NOT NULL, - `StartDateTime` datetime default NULL, - `DiskSpace` bigint default NULL, - PRIMARY KEY (`EventId`), - KEY `Events_Day_MonitorId_idx` (`MonitorId`), - KEY `Events_Day_StartDateTime_idx` (`StartDateTime`) -) ENGINE=@ZM_MYSQL_ENGINE@; - -DROP TABLE IF EXISTS `Events_Week`; -CREATE TABLE `Events_Week` ( - `EventId` BIGINT unsigned NOT NULL, - `MonitorId` int(10) unsigned NOT NULL, - `StartDateTime` datetime default NULL, - `DiskSpace` bigint default NULL, - PRIMARY KEY (`EventId`), - KEY `Events_Week_MonitorId_idx` (`MonitorId`), - KEY `Events_Week_StartDateTime_idx` (`StartDateTime`) -) ENGINE=@ZM_MYSQL_ENGINE@; - -DROP TABLE IF EXISTS `Events_Month`; -CREATE TABLE `Events_Month` ( - `EventId` BIGINT unsigned NOT NULL, - `MonitorId` int(10) unsigned NOT NULL, - `StartDateTime` datetime default NULL, - `DiskSpace` bigint default NULL, - PRIMARY KEY (`EventId`), - KEY `Events_Month_MonitorId_idx` (`MonitorId`), - KEY `Events_Month_StartDateTime_idx` (`StartDateTime`) -) ENGINE=@ZM_MYSQL_ENGINE@; - - -DROP TABLE IF EXISTS `Events_Archived`; -CREATE TABLE `Events_Archived` ( - `EventId` BIGINT unsigned NOT NULL, - `MonitorId` int(10) unsigned NOT NULL, - `DiskSpace` bigint default NULL, - PRIMARY KEY (`EventId`), - KEY `Events_Archived_MonitorId_idx` (`MonitorId`) -) ENGINE=@ZM_MYSQL_ENGINE@; - DROP TABLE IF EXISTS `Event_Data`; CREATE TABLE `Event_Data` ( `Id` BIGINT unsigned NOT NULL auto_increment, @@ -646,24 +592,6 @@ CREATE TABLE `Monitor_Status` ( ) ENGINE=@ZM_MYSQL_ENGINE@; CREATE INDEX Monitor_Status_UpdatedOn_idx on Monitor_Status(UpdatedOn); -DROP TABLE IF EXISTS `Event_Summaries`; -CREATE TABLE `Event_Summaries` ( - `MonitorId` int(10) unsigned NOT NULL, - `TotalEvents` int(10) default NULL, - `TotalEventDiskSpace` bigint default NULL, - `HourEvents` int(10) default NULL, - `HourEventDiskSpace` bigint default NULL, - `DayEvents` int(10) default NULL, - `DayEventDiskSpace` bigint default NULL, - `WeekEvents` int(10) default NULL, - `WeekEventDiskSpace` bigint default NULL, - `MonthEvents` int(10) default NULL, - `MonthEventDiskSpace` bigint default NULL, - `ArchivedEvents` int(10) default NULL, - `ArchivedEventDiskSpace` bigint default NULL, - PRIMARY KEY (`MonitorId`) -) ENGINE=@ZM_MYSQL_ENGINE@; - -- -- Table structure for table `States` -- PP - Added IsActive to track custom run states @@ -1272,6 +1200,8 @@ CREATE TABLE `Events_Tags` ( source @PKGDATADIR@/db/Object_Types.sql -- We generally don't alter triggers, we drop and re-create them, so let's keep them in a separate file that we can just source in update scripts. source @PKGDATADIR@/db/triggers.sql +-- Views replace the old Events_Hour, Events_Day, Events_Week, Events_Month, Events_Archived, and Event_Summaries tables. +source @PKGDATADIR@/db/views.sql source @PKGDATADIR@/db/manufacturers.sql source @PKGDATADIR@/db/models.sql diff --git a/db/zm_update-1.37.78.sql.in b/db/zm_update-1.37.78.sql.in new file mode 100644 index 00000000000..226474c17cf --- /dev/null +++ b/db/zm_update-1.37.78.sql.in @@ -0,0 +1,24 @@ +-- +-- Replace Events_Hour, Events_Day, Events_Week, Events_Month, +-- Events_Archived, and Event_Summaries tables with views. +-- These views query the Events table directly, eliminating the need +-- for triggers and periodic reconciliation in zmaudit/zmstats. +-- + +-- Step 1: Drop triggers that maintained the old tables +source @PKGDATADIR@/db/triggers.sql + +-- Step 2: Drop the old denormalized tables (and any prior Event_Summaries view) +DROP TABLE IF EXISTS `Events_Hour`; +DROP TABLE IF EXISTS `Events_Day`; +DROP TABLE IF EXISTS `Events_Week`; +DROP TABLE IF EXISTS `Events_Month`; +DROP TABLE IF EXISTS `Events_Archived`; +DROP VIEW IF EXISTS `Event_Summaries`; +DROP TABLE IF EXISTS `Event_Summaries`; +DROP TABLE IF EXISTS `Event_Summaries_Metadata`; +DROP TABLE IF EXISTS `Event_Summaries_New`; +DROP TABLE IF EXISTS `Event_Summaries_Old`; + +-- Step 3: Create views with the same names and a covering index +source @PKGDATADIR@/db/views.sql diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Event_Summary.pm b/scripts/ZoneMinder/lib/ZoneMinder/Event_Summary.pm index 0086b5bac04..180c96f741d 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Event_Summary.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Event_Summary.pm @@ -68,6 +68,16 @@ $serial = $primary_key = 'MonitorId'; ArchivedEventDiskSpace => undef, ); +my $_last_checked = 0; + +sub ensureSummariesFresh { + return if (time() - $_last_checked) < 60; + $_last_checked = time(); + + require ZoneMinder::Database; + ZoneMinder::Database::zmDbDo("CALL Refresh_Summaries_SWR()"); +} + sub Monitor { return new ZoneMinder::Monitor( $_[0]{MonitorId} ); } # end sub Monitor diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm index 963c1677b69..fdae189f161 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm @@ -393,6 +393,7 @@ sub Event_Summary { my $self = shift; $$self{Event_Summary} = shift if @_; if ( ! $$self{Event_Summary} ) { + ZoneMinder::Event_Summary::ensureSummariesFresh(); $$self{Event_Summary} = ZoneMinder::Event_Summary->find_one(MonitorId=>$$self{Id}); } return $$self{Event_Summary}; diff --git a/scripts/zmaudit.pl.in b/scripts/zmaudit.pl.in index 2ad29a7a5eb..d5617fd9d54 100644 --- a/scripts/zmaudit.pl.in +++ b/scripts/zmaudit.pl.in @@ -888,62 +888,6 @@ FROM `Frames` WHERE `EventId`=?'; } # end if ZM_LOG_DATABASE_LIMIT $loop = $continuous; - my $eventcounts_sql = ' - UPDATE `Event_Summaries` SET - `TotalEvents`=(SELECT COUNT(`Id`) FROM `Events` WHERE `MonitorId`=`Event_Summaries`.`MonitorId`), - `TotalEventDiskSpace`=(SELECT SUM(`DiskSpace`) FROM `Events` WHERE `MonitorId`=`Event_Summaries`.`MonitorId` AND `DiskSpace` IS NOT NULL), - `ArchivedEvents`=(SELECT COUNT(`Id`) FROM `Events` WHERE `MonitorId`=`Event_Summaries`.`MonitorId` AND `Archived`=1), - `ArchivedEventDiskSpace`=(SELECT SUM(`DiskSpace`) FROM `Events` WHERE `MonitorId`=`Event_Summaries`.`MonitorId` AND `Archived`=1 AND `DiskSpace` IS NOT NULL) - '; - - ZoneMinder::Database::zmDbDo($eventcounts_sql); - aud_print('Finished updating TotalEvents, ArchivedEvents'); - - my $eventcounts_hour_sql = ' - UPDATE `Event_Summaries` INNER JOIN ( - SELECT `MonitorId`, COUNT(*) AS `HourEvents`, SUM(COALESCE(`DiskSpace`,0)) AS `HourEventDiskSpace` - FROM `Events_Hour` GROUP BY `MonitorId` - ) AS `E` ON `E`.`MonitorId`=`Event_Summaries`.`MonitorId` SET - `Event_Summaries`.`HourEvents` = `E`.`HourEvents`, - `Event_Summaries`.`HourEventDiskSpace` = `E`.`HourEventDiskSpace` - '; - ZoneMinder::Database::zmDbDo($eventcounts_hour_sql); - aud_print("Finished updating HourEvents"); - - - my $eventcounts_day_sql = ' - UPDATE `Event_Summaries` INNER JOIN ( - SELECT `MonitorId`, COUNT(*) AS `DayEvents`, SUM(COALESCE(`DiskSpace`,0)) AS `DayEventDiskSpace` - FROM `Events_Day` GROUP BY `MonitorId` - ) AS `E` ON `E`.`MonitorId`=`Event_Summaries`.`MonitorId` SET - `Event_Summaries`.`DayEvents` = `E`.`DayEvents`, - `Event_Summaries`.`DayEventDiskSpace` = `E`.`DayEventDiskSpace` - '; - ZoneMinder::Database::zmDbDo($eventcounts_day_sql); - aud_print("Finished updating DayEvents"); - - my $eventcounts_week_sql = ' - UPDATE `Event_Summaries` INNER JOIN ( - SELECT `MonitorId`, COUNT(*) AS `WeekEvents`, SUM(COALESCE(`DiskSpace`,0)) AS `WeekEventDiskSpace` - FROM `Events_Week` GROUP BY `MonitorId` - ) AS `E` ON `E`.`MonitorId`=`Event_Summaries`.`MonitorId` SET - `Event_Summaries`.`WeekEvents` = `E`.`WeekEvents`, - `Event_Summaries`.`WeekEventDiskSpace` = `E`.`WeekEventDiskSpace` - '; - ZoneMinder::Database::zmDbDo($eventcounts_week_sql); - aud_print("Finished updating WeekEvents"); - - my $eventcounts_month_sql = ' - UPDATE `Event_Summaries` INNER JOIN ( - SELECT `MonitorId`, COUNT(*) AS `MonthEvents`, SUM(COALESCE(`DiskSpace`,0)) AS `MonthEventDiskSpace` - FROM `Events_Month` GROUP BY `MonitorId` - ) AS `E` ON `E`.`MonitorId`=`Event_Summaries`.`MonitorId` SET - `Event_Summaries`.`MonthEvents` = `E`.`MonthEvents`, - `Event_Summaries`.`MonthEventDiskSpace` = `E`.`MonthEventDiskSpace` - '; - ZoneMinder::Database::zmDbDo($eventcounts_month_sql); - aud_print("Finished updating MonthEvents"); - ZoneMinder::Database::zmDbDo('UPDATE Storage SET DiskSpace=(SELECT SUM(DiskSpace) FROM Events WHERE StorageId=Storage.Id)'); aud_print("Finished updating Storage DiskSpace"); diff --git a/scripts/zmstats.pl.in b/scripts/zmstats.pl.in index 282adb20b74..0cb1b7aa796 100644 --- a/scripts/zmstats.pl.in +++ b/scripts/zmstats.pl.in @@ -86,11 +86,6 @@ while (!$zm_terminate) { # Clear out statuses for Monitors that aren't updating themselves. zmDbDo('DELETE FROM Monitor_Status WHERE UpdatedOn < timestamp(DATE_SUB(NOW(), INTERVAL 1 MINUTE))'); - zmDbDo('DELETE FROM Events_Hour WHERE StartDateTime < DATE_SUB(NOW(), INTERVAL 1 hour)'); - zmDbDo('DELETE FROM Events_Day WHERE StartDateTime < DATE_SUB(NOW(), INTERVAL 1 day)'); - zmDbDo('DELETE FROM Events_Week WHERE StartDateTime < DATE_SUB(NOW(), INTERVAL 1 week)'); - zmDbDo('DELETE FROM Events_Month WHERE StartDateTime < DATE_SUB(NOW(), INTERVAL 1 month)'); - # Prune the Logs table if required if ( $Config{ZM_LOG_DATABASE_LIMIT} ) { if ( $Config{ZM_LOG_DATABASE_LIMIT} =~ /^\d+$/ ) { diff --git a/src/zm_event.cpp b/src/zm_event.cpp index 634b03a2f11..272912ac339 100644 --- a/src/zm_event.cpp +++ b/src/zm_event.cpp @@ -149,40 +149,6 @@ Event::Event( id = zmDbDoInsert(sql); } while (!id and !zm_terminate); - /* None of these are crucial, and are simple when done as individual transactions */ - sql = stringtf("INSERT INTO `Events_Hour` (EventId,MonitorId,StartDateTime,DiskSpace)" - " VALUES (%" PRId64 ",%u,'%s',NULL)", - id, monitor->Id(), now_str.c_str()); - dbQueue.push(std::move(sql)); - sql = stringtf("INSERT INTO `Events_Day` (EventId,MonitorId,StartDateTime,DiskSpace)" - " VALUES (%" PRId64 ",%u,'%s',NULL)", - id, monitor->Id(), now_str.c_str()); - dbQueue.push(std::move(sql)); - sql = stringtf("INSERT INTO `Events_Week` (EventId,MonitorId,StartDateTime,DiskSpace)" - " VALUES (%" PRId64 ",%u,'%s',NULL)", - id, monitor->Id(), now_str.c_str()); - dbQueue.push(std::move(sql)); - sql = stringtf("INSERT INTO `Events_Month` (EventId,MonitorId,StartDateTime,DiskSpace)" - " VALUES (%" PRId64 ",%u,'%s',NULL)", - id, monitor->Id(), now_str.c_str()); - dbQueue.push(std::move(sql)); - /* - sql = stringtf("INSERT INTO `Events_Year` (EventId,MonitorId,StartDateTime,DiskSpace)" - " VALUES (%" PRId64 ",%u,'%s',NULL)", - id, monitor->Id(), now_str.c_str()); - dbQueue.push(std::move(sql)); - */ - sql = stringtf("INSERT INTO Event_Summaries " - "(MonitorId,HourEvents,DayEvents,WeekEvents,MonthEvents,TotalEvents)" - " VALUES (%u,1,1,1,1,1) ON DUPLICATE KEY UPDATE" - " HourEvents = COALESCE(HourEvents,0)+1," - " DayEvents = COALESCE(DayEvents,0)+1," - " WeekEvents = COALESCE(WeekEvents,0)+1," - " MonthEvents = COALESCE(MonthEvents,0)+1," - " TotalEvents = COALESCE(TotalEvents,0)+1", - monitor->Id()); - dbQueue.push(std::move(sql)); - thread_ = std::thread(&Event::Run, this); } diff --git a/web/ajax/console.php b/web/ajax/console.php index c7aded61f54..4171c9b4bf0 100644 --- a/web/ajax/console.php +++ b/web/ajax/console.php @@ -144,11 +144,14 @@ function queryRequest() { } } + // Ensure Event_Summaries snapshot is fresh before querying + ensureSummariesFresh(); + // Build SQL query $sql = 'SELECT M.*, S.*, E.*, (SELECT Name FROM Manufacturers WHERE Manufacturers.Id=M.ManufacturerId) AS Manufacturer, (SELECT Name FROM Models where Models.Id=M.ModelId) AS Model FROM Monitors AS M - LEFT JOIN Monitor_Status AS S ON S.MonitorId=M.Id - LEFT JOIN Event_Summaries AS E ON E.MonitorId=M.Id + LEFT JOIN Monitor_Status AS S ON S.MonitorId=M.Id + LEFT JOIN Event_Summaries AS E ON E.MonitorId=M.Id WHERE M.`Deleted`=false'; if (count($conditions)) { diff --git a/web/api/app/Model/Event_Summary.php b/web/api/app/Model/Event_Summary.php index b9395ab1ba0..00c82274254 100644 --- a/web/api/app/Model/Event_Summary.php +++ b/web/api/app/Model/Event_Summary.php @@ -46,4 +46,21 @@ class Event_Summary extends AppModel { ), ), ); + + public function beforeFind($queryData) { + $db = $this->getDataSource(); + $result = $db->fetchAll( + "SELECT last_updated FROM Event_Summaries_Metadata WHERE table_name='Event_Summaries'" + ); + if ($result) { + $row = $result[0]; + $last_updated = isset($row['Event_Summaries_Metadata']) + ? $row['Event_Summaries_Metadata']['last_updated'] + : (isset($row[0]) ? $row[0]['last_updated'] : null); + if ($last_updated && (time() - strtotime($last_updated)) >= 60) { + $db->rawQuery("CALL Refresh_Summaries_SWR()"); + } + } + return $queryData; + } } diff --git a/web/includes/Monitor.php b/web/includes/Monitor.php index ccb6c865188..ce4ceb8ae09 100644 --- a/web/includes/Monitor.php +++ b/web/includes/Monitor.php @@ -516,6 +516,7 @@ public function __call($fn, array $args) { } # end if this->Id return null; } else if (array_key_exists($fn, $this->summary_fields)) { + ensureSummariesFresh(); $sql = 'SELECT * FROM `Event_Summaries` WHERE `MonitorId`=?'; $row = dbFetchOne($sql, NULL, array($this->{'Id'})); if (!$row) { @@ -733,7 +734,6 @@ public function destroy() { if ( ZM_OPT_X10 ) dbQuery('DELETE FROM TriggersX10 WHERE MonitorId=?', array($this->{'Id'})); dbQuery('DELETE FROM Monitor_Status WHERE MonitorId = ?', array($this->{'Id'})); - dbQuery('DELETE FROM Event_Summaries WHERE MonitorId = ?', array($this->{'Id'})); dbQuery('DELETE FROM Monitors WHERE Id = ?', array($this->{'Id'})); } // end function destroy diff --git a/web/includes/database.php b/web/includes/database.php index b0c76612d86..2863d692b54 100644 --- a/web/includes/database.php +++ b/web/includes/database.php @@ -380,6 +380,20 @@ function getTableDescription( $table, $asString=1 ) { return $columns; } +function ensureSummariesFresh($max_age_seconds = 60) { + static $checked = false; + if ($checked) return; + $checked = true; + + $row = dbFetchOne( + "SELECT last_updated FROM Event_Summaries_Metadata WHERE table_name='Event_Summaries'" + ); + if ($row && (time() - strtotime($row['last_updated'])) < $max_age_seconds) { + return; + } + dbQuery("CALL Refresh_Summaries_SWR()"); +} + function db_version() { return dbFetchOne('SELECT VERSION()', 'VERSION()'); } diff --git a/web/skins/classic/views/_monitor_filters.php b/web/skins/classic/views/_monitor_filters.php index 584f5e74a02..86312abc5ec 100644 --- a/web/skins/classic/views/_monitor_filters.php +++ b/web/skins/classic/views/_monitor_filters.php @@ -217,10 +217,12 @@ function buildMonitorsFilters() { $html .= ''; $html .= ''; + ensureSummariesFresh(); + $sqlAll = 'SELECT M.*, S.*, E.* FROM Monitors AS M - LEFT JOIN Monitor_Status AS S ON S.MonitorId=M.Id - LEFT JOIN Event_Summaries AS E ON E.MonitorId=M.Id + LEFT JOIN Monitor_Status AS S ON S.MonitorId=M.Id + LEFT JOIN Event_Summaries AS E ON E.MonitorId=M.Id WHERE M.`Deleted`=false'; $sqlSelected = $sqlAll . ( count($conditions) ? ' AND ' . implode(' AND ', $conditions) : '' ).' ORDER BY Sequence ASC'; $monitors = dbFetchAll($sqlSelected, null, $values);