-- 005_task_indexes.sql — index the lookups the control loops actually do. -- -- Postgres does NOT auto-index the referencing side of a foreign key. `tasks.instance_id` -- had no index, and three hot paths look rows up by it: -- -- * ReconcileRepo._has_unfinished — the idempotency guard on every enqueue, so once per -- candidate per 60s tick. -- * due_for_deprovision's `not exists` correlated subquery — once per instance per tick. -- * the ON DELETE CASCADE itself, on every instance delete. -- -- `tasks` is append-only in practice (done/failed rows are never pruned by the app; the -- runbook prunes them by hand), so a sequential scan there gets slower every day the -- system runs. This is the index that stops a 60-second control loop degrading into a -- full table scan of all history. -- -- Composite on (instance_id, kind, state) because _has_unfinished filters all three. create index tasks_by_instance on tasks (instance_id, kind, state); -- `instances.state` is filtered by ready_instances, instance_counts, due_for_deprovision -- and list_upgradable — every reconciler check. Partial would not help: the sweeps look -- for different states, so the whole column earns its index. create index instances_by_state on instances (state);