diff --git a/docs/plans/2026-11-13-auditlogentries-index-usage.md b/docs/plans/2026-11-13-auditlogentries-index-usage.md new file mode 100644 index 00000000..f419fa90 --- /dev/null +++ b/docs/plans/2026-11-13-auditlogentries-index-usage.md @@ -0,0 +1,72 @@ +# AuditLogEntries index usage: first measurement (2026-11-13) + +Tracks: Gitea **ScadaBridge#70** (deferred-work register row 135). +Question: are the single-column `IX_AuditLogEntries_Timestamp`, `_User` and `_EntityType` indexes +dead weight now that `ReviewRemediationIndexes` added `(EntityType, Timestamp DESC)`, +`(User, Timestamp DESC)` and `(Timestamp DESC, Id DESC)`? + +**Decision: no migration yet. The measurement window is too short on both tiers, and no index on the +table was read at all, the new composites included. Re-measure on or after 2026-11-27.** + +## Method + +Read-only `SELECT`s over a direct SQL login (`sqlcmd -C`), no remote exec, run 2026-11-13 ~02:10 server time: +`sys.dm_db_index_usage_stats` LEFT JOIN `sys.indexes` for `dbo.AuditLogEntries`, plus +`sys.dm_db_index_operational_stats`, `sys.partitions`/`sys.allocation_units` for rows and size, and +`sys.dm_os_sys_info.sqlserver_start_time` (the DMVs reset when SQL Server restarts). + +## Measurement window + +| Tier | SQL host / DB | `sqlserver_start_time` | Window | Latest migration | +|---|---|---|---|---| +| DEV | `wonder-app-vd03` / `ScadaBridge` | 2026-11-10 06:42 | **~3 days** | `20261110120000_AuditMaintenanceGrants` (64) | +| QA | `wonder-sql-vq03` / `ScadaBridge` | 2026-11-03 07:09 | **~10 days** | `20261022133907_ReviewRemediationIndexes` (58) | + +Both are under the two weeks needed. The migration went in on 2026-10-22, but SQL restarts since then +have reset the counters. + +## Results + +Table size: DEV 471 rows (Timestamp 2026-05-29 to 2026-10-28), QA 567 rows (to 2026-11-06). + +| Index | Keys | DEV seeks/scans/lookups | DEV updates | QA seeks/scans/lookups | QA updates | Size | +|---|---|---|---|---|---|---| +| PK_AuditLogEntries (clustered) | Id | 0/0/0 | 0 | 0/0/0 | 10 | 1.4 MB / 4.3 MB | +| IX_AuditLogEntries_Action | Action | 0/0/0 | 0 | 0/0/0 | 10 | 72 KB | +| IX_AuditLogEntries_EntityId | EntityId | 0/0/0 | 0 | 0/0/0 | 10 | 72 KB | +| **IX_AuditLogEntries_EntityType** | EntityType | 0/0/0 | 0 | 0/0/0 | 10 | 72 KB | +| **IX_AuditLogEntries_Timestamp** | Timestamp | 0/0/0 | 0 | 0/0/0 | 10 | 72 KB | +| **IX_AuditLogEntries_User** | User | 0/0/0 | 0 | 0/0/0 | 10 | 72 KB | +| IX_AuditLogEntries_BundleImportId | BundleImportId | 0/0/0 | 0 | 0/0/0 | 10 | 72 KB | +| IX_AuditLogEntries_EntityType_Timestamp | EntityType, Timestamp DESC | 0/0/0 | 0 | 0/0/0 | 10 | 200 KB | +| IX_AuditLogEntries_Timestamp_Id | Timestamp DESC, Id DESC | 0/0/0 | 0 | 0/0/0 | 10 | 200 KB | +| IX_AuditLogEntries_User_Timestamp | User, Timestamp DESC | 0/0/0 | 0 | 0/0/0 | 10 | 200 KB | +| IX_AuditLogEntries_EntityName (DEV only, migration 59+) | EntityName | 0/0/0 | 0 | — | — | 200 KB | + +`sys.dm_db_index_operational_stats` agrees: zero range scans and singleton lookups on every index on both +tiers, with 10 leaf inserts per index on QA and none on DEV. + +## Reading + +- The data does not settle the question. Every index reads zero, including the composites that are + supposed to replace the single-column ones. Nobody used the config-audit page or the CLI query on + either tier during the window, and the purge job did not touch the table. A zero on an old index + only means something if the composite that replaces it shows reads. +- On write cost: the table is tiny (about 500 rows) and takes about 1 write a day on QA. The three + single-column indexes cost about 216 KB and 3 extra index writes per insert. Nothing is urgent. +- Static coverage (from code, not from the measurement): each single-column index is a left prefix of + a composite. The filters in `CentralUiRepository` (`User ==`, `EntityType ==`, `Timestamp` range, + ordered by `Timestamp DESC, Id DESC`) and the purge in `ConfigAuditPurgeService` + (`Timestamp < cutoff ORDER BY Timestamp`, which can use a backward scan of `IX_AuditLogEntries_Timestamp_Id`) + are all served by the composites. So the case for dropping the three indexes is sound in principle. + The register's rule is "measured, not assumed", and this run measured nothing useful. + +## Next step + +Re-run the same query on or after **2026-11-27**, which is two weeks after both current +`sqlserver_start_time`s. Check `sqlserver_start_time` again first. If the composites show reads and +the single-column indexes still show zero, drop `IX_AuditLogEntries_Timestamp`, `_User` and +`_EntityType` in one migration whose `Down()` recreates them. Leave `Action`, `EntityId`, +`BundleImportId` and `EntityName` alone, because no composite covers them. If every index still reads +zero, the owner has to decide whether to drop on static grounds or to exercise the config-audit page +on QA first and then measure.