fix: add missing foreign key indexes for performance and referential integrity (#167)

Missing indexes detected on 7 foreign key columns:
- on_call_overrides.user_id
- on_call_layer_members.user_id
- site_outages.site_id
- escalation_targets.user_id
- notification_digests.organization_id
- on_call_notifications.user_id
- alerts.site_outage_id (already existed, skipped)

Without indexes, PostgreSQL must perform sequential scans for:
- Referential integrity checks on DELETE/UPDATE of parent rows
- JOIN operations on these columns
- Can cause lock contention and slow queries

Indexes created with CONCURRENTLY option to avoid blocking production
tables during deployment.

Migration uses @disable_ddl_transaction and @disable_migration_lock
to ensure indexes are built without holding locks.

Reviewed-on: graham/towerops-web#167
This commit is contained in:
Graham McIntire 2026-03-25 13:04:48 -05:00 committed by graham
parent dba9a286d3
commit d75eeb5389

View file

@ -0,0 +1,44 @@
defmodule Towerops.Repo.Migrations.AddMissingFkIndexes do
use Ecto.Migration
@disable_ddl_transaction true
@disable_migration_lock true
def up do
# Create indexes concurrently to avoid locking tables in production
# These foreign keys were missing indexes, causing slow DELETEs/UPDATEs
# on parent tables and slow JOINs
# on_call_overrides.user_id -> users.id
create_if_not_exists index(:on_call_overrides, [:user_id], concurrently: true)
# on_call_layer_members.user_id -> users.id
create_if_not_exists index(:on_call_layer_members, [:user_id], concurrently: true)
# site_outages.site_id -> sites.id
create_if_not_exists index(:site_outages, [:site_id], concurrently: true)
# escalation_targets.user_id -> users.id
create_if_not_exists index(:escalation_targets, [:user_id], concurrently: true)
# notification_digests.organization_id -> organizations.id
create_if_not_exists index(:notification_digests, [:organization_id], concurrently: true)
# on_call_notifications.user_id -> users.id
create_if_not_exists index(:on_call_notifications, [:user_id], concurrently: true)
# alerts.site_outage_id -> site_outages.id
create_if_not_exists index(:alerts, [:site_outage_id], concurrently: true)
end
def down do
# Drop indexes in reverse order
drop_if_exists index(:alerts, [:site_outage_id], concurrently: true)
drop_if_exists index(:on_call_notifications, [:user_id], concurrently: true)
drop_if_exists index(:notification_digests, [:organization_id], concurrently: true)
drop_if_exists index(:escalation_targets, [:user_id], concurrently: true)
drop_if_exists index(:site_outages, [:site_id], concurrently: true)
drop_if_exists index(:on_call_layer_members, [:user_id], concurrently: true)
drop_if_exists index(:on_call_overrides, [:user_id], concurrently: true)
end
end