ADR 014: Database Index Optimization
Status
Accepted
Date
2026-01-24
Context
Supabase's performance advisor flagged 33 potential performance issues in the database:
- Unindexed Foreign Keys (4 warnings): Foreign key columns without covering indexes, which can impact JOIN performance.
- Unused Indexes (29 warnings): Indexes that Postgres has never used according to
pg_stat_user_indexes, potentially wasting storage and slowing writes.
Before taking action, a comprehensive codebase analysis was performed to determine:
- Which foreign keys are actually queried in JOINs or WHERE clauses
- Which indexes are genuinely unused vs. false positives due to low traffic
- Whether audit/provenance fields require indexes
Decision
We adopted a data-driven approach to index management:
1. Audit-Only Foreign Keys: No Indexes
The following foreign keys are write-only audit fields never used in queries:
audit_logs.performed_bynotification_audit.sent_byorganization_invitations.accepted_byorganization_invitations.invited_byorganization_members.invited_byservices.deleted_byservices.reviewed_bypartner_terms_acceptance.service_id
Decision: Leave these unindexed. Adding indexes would slow down INSERT/UPDATE operations with zero query benefit.
2. Genuinely Unused Indexes: Remove
Through codebase analysis, 18 indexes were confirmed as genuinely unused:
Migration 1 (20260124000001_optimize_indexes.sql):
idx_org_invitations_email- Email never filteredidx_org_invitations_token- Token lookup only in DB functionsidx_services_scope- Filtering happens client-sideidx_services_authority_tier- Scoring done in-memoryidx_audit_logs_record- Write-only tableidx_audit_logs_performed_by- Write-only tableidx_audit_logs_performed_at- Write-only tableidx_notification_audit_sent_by- Write-only tableidx_notification_audit_sent_at- Write-only tableidx_notification_audit_onesignal_id- Write-only tableidx_org_members_invited_by- Never queriedidx_feedback_status- Client-side filtering only
Migration 2 (20260124000002_remove_unused_indexes.sql):
idx_push_subscriptions_categories- GIN index unused (only filters by endpoint)idx_submissions_status- Table never queriedidx_submissions_created- Table never queriedidx_partner_terms_service_id- Table never queriedidx_admin_actions_service- Write-only audit tableidx_admin_actions_action_type- Write-only audit table
3. Missing Critical Index: Add
Dashboard queries for service_update_requests were performing table scans:
Decision: Added composite index idx_service_update_requests_requested_by_status on (requested_by, status).
4. False Positives: Keep
15 indexes flagged as "unused" are actively used but not detected due to:
- Low query volume (development/staging environment)
- Queries going through views/functions rather than direct table access
- Postgres statistics not yet accumulated
These indexes are retained:
idx_mat_feedback_agg_service- Used in analytics viewsidx_analytics_service_id- Used in analytics APIidx_analytics_created_at- Time-based analyticsidx_feedback_service- Feedback lookupsidx_feedback_created- Ordering feedback by dateidx_update_requests_service- Dashboard queriesidx_update_requests_status- Filter by pendingidx_update_requests_requested_by- User-specific queriesidx_org_members_org- Heavy usage in member managementidx_notifications_read- Unread notification countsidx_org_invitations_org- Invitation lookupsidx_org_invitations_expires- Pending invitation cleanupidx_services_deleted_at- Export filteringidx_reindex_progress_status_started- Admin reindex status
Consequences
Positive
- Performance: Removed 18 unused indexes, reducing storage overhead and improving write performance
- Query Optimization: Added 1 critical missing index for dashboard queries
- Code-First Validation: Every index decision was validated against actual application code, not just static analysis
- Clear Documentation: Future developers can reference this ADR to understand why certain foreign keys are intentionally unindexed
Negative
- Ongoing Warnings: Supabase will continue to flag the 8 unindexed foreign keys and 15 "unused" indexes as INFO-level warnings
- Manual Validation: Requires periodic review of warnings as the application evolves
Neutral
- Postgres Statistics: As production traffic increases, Postgres will recognize more of the "unused" indexes as actively used
Implementation
Two migrations were created:
- 20260124000001_optimize_indexes.sql: Removed 12 unused indexes, added 1 critical index
- 20260124000002_remove_unused_indexes.sql: Removed 6 additional unused indexes
Both migrations use IF EXISTS guards to ensure idempotency.
Future Considerations
- Run
ANALYZEon tables periodically to help Postgres recognize index usage - Re-evaluate warnings after production traffic accumulates for 2-4 weeks
- Consider removing
partner_terms_acceptance,service_submissions, andadmin_actionstables entirely if they remain unused