From d75eeb538950bf75712d994978c873b7e423c962 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 25 Mar 2026 13:04:48 -0500 Subject: [PATCH] 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: https://git.mcintire.me/graham/towerops-web/pulls/167 --- .../20260325180211_add_missing_fk_indexes.exs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 priv/repo/migrations/20260325180211_add_missing_fk_indexes.exs diff --git a/priv/repo/migrations/20260325180211_add_missing_fk_indexes.exs b/priv/repo/migrations/20260325180211_add_missing_fk_indexes.exs new file mode 100644 index 00000000..4c34b6dd --- /dev/null +++ b/priv/repo/migrations/20260325180211_add_missing_fk_indexes.exs @@ -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