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 (<entity>_client_id, <entity>_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.
This commit is contained in:
johnnyq
2026-08-27 22:59:14 -04:00
parent 877474b7f0
commit 0838ed07ec
2 changed files with 135 additions and 23 deletions

View File

@@ -0,0 +1,88 @@
<?php
/*
* ITFlow - Database update to version 2.7.5 (from 2.7.4)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// Index the client-scoped lookups.
//
// Every one of these columns was unindexed, which is why the client
// context was slow: agent/includes/inc_all_client.php runs a count per
// table to fill in the side nav badges, and with no index each one is a
// full table scan before the page renders a byte. The same columns are
// what clientScopeSql() filters on, so every client-scoped list page in
// the app was paying for it too.
//
// Shape: (<entity>_client_id, <entity>_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);

70
db.sql
View File

@@ -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