From 0838ed07ec470b57755b5bf7da55b2e91bd43f5e Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 27 Aug 2026 22:59:14 -0400 Subject: [PATCH] Add indexes on the client-scoped columns (db 2.7.5) None of the *_client_id columns carried an index, so agent/includes/inc_all_client.php was doing a full table scan per badge count before the page rendered a byte - 27 of them. The main agent context runs 8 counts and the admin context none, which is why the client side nav was the only one that felt slow. 24 indexes, shaped (_client_id, _archived_at) where the query pairs the two and the client column alone otherwise. The archived column is always trailing and never an index of its own - on a healthy instance nearly every row has it NULL, so it cannot lead. recurring_tickets, services, invoices, recurring_invoices and calendar_events get a single column because their queries carry no archived filter; invoices is in that group because the money total there spans archived rows on purpose. payments.payment_invoice_id and transfers.transfer_revenue_id are join keys rather than client scoping but sit on the same page's critical path. Migration checks information_schema.STATISTICS before each ALTER, since MySQL has no ADD INDEX IF NOT EXISTS and re-adding is an error - safe to re-run and safe where an index was added by hand. db.sql carries the same keys so fresh installs skip the update step. Wider than the side nav: these are the columns clientScopeSql() filters on, so every client-scoped list page was paying for their absence. Expect the ALTERs to be the slow part of the upgrade on a mature instance. --- admin/database_updates/2.7.5.php | 88 ++++++++++++++++++++++++++++++++ db.sql | 70 ++++++++++++++++--------- 2 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 admin/database_updates/2.7.5.php diff --git a/admin/database_updates/2.7.5.php b/admin/database_updates/2.7.5.php new file mode 100644 index 000000000..0ee9fd4f9 --- /dev/null +++ b/admin/database_updates/2.7.5.php @@ -0,0 +1,88 @@ +_client_id, _archived_at) where the query pairs + // the two, otherwise the client column alone. The archived column is + // deliberately the TRAILING part and never an index of its own - on a + // healthy instance almost every row has it NULL, so it is not selective + // enough to lead and an index on it alone would be ignored. + // + // Tables whose queries carry no archived filter get a single-column index: + // recurring_tickets, services, invoices, recurring_invoices, + // calendar_events. invoices is in that group because the money total there + // spans archived rows as well, so the scan cannot be narrowed by it. + // + // payments.payment_invoice_id and transfers.transfer_revenue_id are join + // keys rather than client scoping, but they are on the same page's + // critical path (amount paid, and the income count's transfer exclusion). + // + // InnoDB builds these online, but on a mature instance this is the slowest + // step of the upgrade - it is proportional to table size, not row count + // returned. + + $itflow_indexes = [ + ['contacts', 'contact_client_id', ['contact_client_id', 'contact_archived_at']], + ['locations', 'location_client_id', ['location_client_id', 'location_archived_at']], + ['assets', 'asset_client_id', ['asset_client_id', 'asset_archived_at']], + ['tickets', 'ticket_client_id', ['ticket_client_id', 'ticket_archived_at']], + ['recurring_tickets', 'recurring_ticket_client_id', ['recurring_ticket_client_id']], + ['projects', 'project_client_id', ['project_client_id', 'project_archived_at']], + ['services', 'service_client_id', ['service_client_id']], + ['vendors', 'vendor_client_id', ['vendor_client_id', 'vendor_archived_at']], + ['credentials', 'credential_client_id', ['credential_client_id', 'credential_archived_at']], + ['networks', 'network_client_id', ['network_client_id', 'network_archived_at']], + ['racks', 'rack_client_id', ['rack_client_id', 'rack_archived_at']], + ['domains', 'domain_client_id', ['domain_client_id', 'domain_archived_at']], + ['certificates', 'certificate_client_id', ['certificate_client_id', 'certificate_archived_at']], + ['software', 'software_client_id', ['software_client_id', 'software_archived_at']], + ['invoices', 'invoice_client_id', ['invoice_client_id']], + ['quotes', 'quote_client_id', ['quote_client_id', 'quote_archived_at']], + ['recurring_invoices', 'recurring_invoice_client_id', ['recurring_invoice_client_id']], + ['revenues', 'revenue_client_id', ['revenue_client_id', 'revenue_archived_at']], + ['files', 'file_client_id', ['file_client_id', 'file_archived_at']], + ['documents', 'document_client_id', ['document_client_id', 'document_archived_at']], + ['calendar_events', 'event_client_id', ['event_client_id']], + ['trips', 'trip_client_id', ['trip_client_id', 'trip_archived_at']], + ['payments', 'payment_invoice_id', ['payment_invoice_id']], + ['transfers', 'transfer_revenue_id', ['transfer_revenue_id']], + ]; + + foreach ($itflow_indexes as $itflow_index) { + list($itflow_index_table, $itflow_index_name, $itflow_index_columns) = $itflow_index; + + // MySQL has no ADD INDEX IF NOT EXISTS, and re-adding one is an error + // rather than a no-op, so check first. This also makes the migration + // safe on an instance where someone added the index by hand. + $itflow_index_exists = mysqli_query($mysqli, "SELECT 1 FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '$itflow_index_table' + AND INDEX_NAME = '$itflow_index_name' + LIMIT 1"); + + if ($itflow_index_exists && mysqli_num_rows($itflow_index_exists) > 0) { + continue; + } + + $itflow_index_column_list = '`' . implode('`, `', $itflow_index_columns) . '`'; + + mysqli_query($mysqli, "ALTER TABLE `$itflow_index_table` + ADD KEY `$itflow_index_name` ($itflow_index_column_list)"); + } + + unset($itflow_indexes, $itflow_index, $itflow_index_table, $itflow_index_name, + $itflow_index_columns, $itflow_index_exists, $itflow_index_column_list); diff --git a/db.sql b/db.sql index 12fbc2c06..f5661aa56 100644 --- a/db.sql +++ b/db.sql @@ -329,7 +329,8 @@ CREATE TABLE `assets` ( `asset_location_id` int(11) NOT NULL DEFAULT 0, `asset_contact_id` int(11) NOT NULL DEFAULT 0, `asset_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`asset_id`) + PRIMARY KEY (`asset_id`), + KEY `asset_client_id` (`asset_client_id`,`asset_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -461,6 +462,7 @@ CREATE TABLE `calendar_events` ( `event_calendar_id` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`event_id`), KEY `event_calendar_id` (`event_calendar_id`), + KEY `event_client_id` (`event_client_id`), CONSTRAINT `calendar_events_ibfk_1` FOREIGN KEY (`event_calendar_id`) REFERENCES `calendars` (`calendar_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -574,7 +576,8 @@ CREATE TABLE `certificates` ( `certificate_accessed_at` datetime DEFAULT NULL, `certificate_domain_id` int(11) NOT NULL DEFAULT 0, `certificate_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`certificate_id`) + PRIMARY KEY (`certificate_id`), + KEY `certificate_client_id` (`certificate_client_id`,`certificate_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -859,7 +862,8 @@ CREATE TABLE `contacts` ( `contact_user_id` int(11) NOT NULL DEFAULT 0, `contact_department` varchar(200) DEFAULT NULL, `contact_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`contact_id`) + PRIMARY KEY (`contact_id`), + KEY `contact_client_id` (`contact_client_id`,`contact_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -985,7 +989,8 @@ CREATE TABLE `credentials` ( `credential_contact_id` int(11) NOT NULL DEFAULT 0, `credential_asset_id` int(11) NOT NULL DEFAULT 0, `credential_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`credential_id`) + PRIMARY KEY (`credential_id`), + KEY `credential_client_id` (`credential_client_id`,`credential_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1197,6 +1202,7 @@ CREATE TABLE `documents` ( `document_updated_by` int(11) NOT NULL DEFAULT 0, `document_client_id` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`document_id`), + KEY `document_client_id` (`document_client_id`,`document_archived_at`), FULLTEXT KEY `document_content_raw` (`document_content_raw`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1249,7 +1255,8 @@ CREATE TABLE `domains` ( `domain_dnshost` int(11) NOT NULL DEFAULT 0, `domain_mailhost` int(11) NOT NULL DEFAULT 0, `domain_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`domain_id`) + PRIMARY KEY (`domain_id`), + KEY `domain_client_id` (`domain_client_id`,`domain_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1329,7 +1336,8 @@ CREATE TABLE `files` ( `file_created_by` int(11) NOT NULL DEFAULT 0, `file_folder_id` int(11) NOT NULL DEFAULT 0, `file_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`file_id`) + PRIMARY KEY (`file_id`), + KEY `file_client_id` (`file_client_id`,`file_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1423,7 +1431,8 @@ CREATE TABLE `invoices` ( `invoice_category_id` int(11) NOT NULL, `invoice_recurring_invoice_id` int(11) NOT NULL DEFAULT 0, `invoice_client_id` int(11) NOT NULL, - PRIMARY KEY (`invoice_id`) + PRIMARY KEY (`invoice_id`), + KEY `invoice_client_id` (`invoice_client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1476,7 +1485,8 @@ CREATE TABLE `locations` ( `location_accessed_at` datetime DEFAULT NULL, `location_contact_id` int(11) NOT NULL DEFAULT 0, `location_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`location_id`) + PRIMARY KEY (`location_id`), + KEY `location_client_id` (`location_client_id`,`location_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1565,7 +1575,8 @@ CREATE TABLE `networks` ( `network_accessed_at` datetime DEFAULT NULL, `network_location_id` int(11) NOT NULL DEFAULT 0, `network_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`network_id`) + PRIMARY KEY (`network_id`), + KEY `network_client_id` (`network_client_id`,`network_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1651,7 +1662,8 @@ CREATE TABLE `payments` ( `payment_archived_at` datetime DEFAULT NULL, `payment_account_id` int(11) NOT NULL, `payment_invoice_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`payment_id`) + PRIMARY KEY (`payment_id`), + KEY `payment_invoice_id` (`payment_invoice_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1752,7 +1764,8 @@ CREATE TABLE `projects` ( `project_completed_at` datetime DEFAULT NULL, `project_archived_at` datetime DEFAULT NULL, `project_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`project_id`) + PRIMARY KEY (`project_id`), + KEY `project_client_id` (`project_client_id`,`project_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1825,7 +1838,8 @@ CREATE TABLE `quotes` ( `quote_archived_at` datetime DEFAULT NULL, `quote_category_id` int(11) NOT NULL, `quote_client_id` int(11) NOT NULL, - PRIMARY KEY (`quote_id`) + PRIMARY KEY (`quote_id`), + KEY `quote_client_id` (`quote_client_id`,`quote_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1876,7 +1890,8 @@ CREATE TABLE `racks` ( `rack_archived_at` datetime DEFAULT NULL, `rack_location_id` int(11) DEFAULT NULL, `rack_client_id` int(11) NOT NULL, - PRIMARY KEY (`rack_id`) + PRIMARY KEY (`rack_id`), + KEY `rack_client_id` (`rack_client_id`,`rack_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1985,7 +2000,8 @@ CREATE TABLE `recurring_invoices` ( `recurring_invoice_archived_at` datetime DEFAULT NULL, `recurring_invoice_category_id` int(11) NOT NULL, `recurring_invoice_client_id` int(11) NOT NULL, - PRIMARY KEY (`recurring_invoice_id`) + PRIMARY KEY (`recurring_invoice_id`), + KEY `recurring_invoice_client_id` (`recurring_invoice_client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2074,7 +2090,8 @@ CREATE TABLE `recurring_tickets` ( `recurring_ticket_contact_id` int(11) NOT NULL DEFAULT 0, `recurring_ticket_asset_id` int(11) NOT NULL DEFAULT 0, `recurring_ticket_ticket_template_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`recurring_ticket_id`) + PRIMARY KEY (`recurring_ticket_id`), + KEY `recurring_ticket_client_id` (`recurring_ticket_client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2115,7 +2132,8 @@ CREATE TABLE `revenues` ( `revenue_category_id` int(11) NOT NULL DEFAULT 0, `revenue_account_id` int(11) NOT NULL, `revenue_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`revenue_id`) + PRIMARY KEY (`revenue_id`), + KEY `revenue_client_id` (`revenue_client_id`,`revenue_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2257,7 +2275,8 @@ CREATE TABLE `services` ( `service_accessed_at` datetime DEFAULT NULL, `service_review_due` date DEFAULT NULL, `service_client_id` int(11) NOT NULL, - PRIMARY KEY (`service_id`) + PRIMARY KEY (`service_id`), + KEY `service_client_id` (`service_client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2480,7 +2499,8 @@ CREATE TABLE `software` ( `software_accessed_at` datetime DEFAULT NULL, `software_vendor_id` int(11) DEFAULT 0, `software_client_id` int(11) NOT NULL, - PRIMARY KEY (`software_id`) + PRIMARY KEY (`software_id`), + KEY `software_client_id` (`software_client_id`,`software_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2942,7 +2962,8 @@ CREATE TABLE `tickets` ( `ticket_order` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`ticket_id`), KEY `ticket_response_due_at` (`ticket_response_due_at`), - KEY `ticket_resolution_due_at` (`ticket_resolution_due_at`) + KEY `ticket_resolution_due_at` (`ticket_resolution_due_at`), + KEY `ticket_client_id` (`ticket_client_id`,`ticket_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2962,7 +2983,8 @@ CREATE TABLE `transfers` ( `transfer_archived_at` datetime DEFAULT NULL, `transfer_expense_id` int(11) NOT NULL, `transfer_revenue_id` int(11) NOT NULL, - PRIMARY KEY (`transfer_id`) + PRIMARY KEY (`transfer_id`), + KEY `transfer_revenue_id` (`transfer_revenue_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2988,7 +3010,8 @@ CREATE TABLE `trips` ( `trip_archived_at` datetime DEFAULT NULL, `trip_user_id` int(11) NOT NULL DEFAULT 0, `trip_client_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`trip_id`) + PRIMARY KEY (`trip_id`), + KEY `trip_client_id` (`trip_client_id`,`trip_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -3200,7 +3223,8 @@ CREATE TABLE `vendors` ( `vendor_accessed_at` datetime DEFAULT NULL, `vendor_client_id` int(11) NOT NULL DEFAULT 0, `vendor_template_id` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`vendor_id`) + PRIMARY KEY (`vendor_id`), + KEY `vendor_client_id` (`vendor_client_id`,`vendor_archived_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -3213,4 +3237,4 @@ CREATE TABLE `vendors` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2026-08-27 17:57:04 +-- Dump completed on 2026-08-27 22:58:59