How to Drop Indexes in PostgreSQL

How to Drop Indexes in PostgreSQL

Indexes are one of those things that get added enthusiastically and removed reluctantly. I’ve inherited databases with dozens of indexes on a single table, half of them redundant, some of them never used even once according to PostgreSQL’s own statistics, all of them quietly costing write performance and disk space. Learning to drop indexes confidently — backed by actual data rather than guesswork — is just as important a skill as knowing how to create them in the first place.

Why Would You Drop an Index?

Before the syntax, it’s worth being clear on why you’d remove an index at all, since indexes generally speed up reads:

Basic Syntax

DROP INDEX [IF EXISTS] index_name [CASCADE | RESTRICT];

Basic Example

DROP INDEX idx_employees_department;

Safer, idempotent version:

DROP INDEX IF EXISTS idx_employees_department;

Dropping Multiple Indexes

DROP INDEX idx_employees_department, idx_employees_hire_date;

Dropping an Index in a Specific Schema

If you’re not relying on search_path, qualify the index with its schema:

DROP INDEX reporting.idx_sales_region;

Dropping Indexes Without Blocking Writes: CONCURRENTLY

Here’s the detail that catches people off guard in production: a plain DROP INDEX takes an ACCESS EXCLUSIVE lock on the table, which blocks all reads and writes for the (usually brief, but not always) duration of the drop. On a busy production table, even a “quick” operation can cause a noticeable stall if it has to wait for existing queries to finish first, or blocks new ones from starting.

DROP INDEX CONCURRENTLY idx_employees_department;

CONCURRENTLY avoids taking that exclusive lock, allowing normal reads and writes to continue while the index is dropped. The trade-offs:

For any index drop on a live production table, I default to CONCURRENTLY unless I have a specific reason not to (like doing it during a maintenance window where blocking briefly genuinely doesn’t matter).

Dropping an Index That Backs a Constraint

If an index was automatically created to support a UNIQUE or PRIMARY KEY constraint, you generally can’t drop it directly with DROP INDEX — you need to drop the constraint instead, which removes the backing index along with it:

ALTER TABLE employees DROP CONSTRAINT employees_email_key;

Trying to DROP INDEX on a constraint-backed index directly will typically raise an error telling you to drop the constraint instead.

Finding Indexes to Drop

This is really the most important part of the whole topic — dropping the right indexes. A few queries I use regularly:

Finding Unused Indexes

SELECT
    schemaname,
    relname AS table_name,
    indexrelname AS index_name,
    idx_scan AS times_used,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

idx_scan = 0 means the index has never been used by the planner since statistics were last reset. Be cautious with this on a database that hasn’t been running long, or where statistics were recently reset (pg_stat_reset()) — a low-traffic monthly report query might just not have run yet.

Finding Duplicate Indexes

SELECT
    indrelid::regclass AS table_name,
    array_agg(indexrelid::regclass) AS duplicate_indexes
FROM pg_index
GROUP BY indrelid, indkey
HAVING COUNT(*) > 1;

This groups indexes by table and the exact set of columns they cover — any group with more than one index is a candidate for consolidation.

Checking Index Size

SELECT
    indexrelname,
    pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE relname = 'employees'
ORDER BY pg_relation_size(indexrelid) DESC;

A Safe Workflow for Dropping Indexes in Production

I follow roughly this process whenever I’m cleaning up indexes on a live database:

  1. Query pg_stat_user_indexes to identify unused or redundant indexes, over a representative time window (ideally covering weekly and monthly query patterns, not just a few days).
  2. Cross-check against any known reporting jobs or batch processes that might run infrequently and wouldn’t show recent usage.
  3. Rather than dropping outright, consider renaming the index first, or in newer PostgreSQL versions, marking it invisible to the planner if that feature is available, to catch any unexpected usage before committing to removal — otherwise, monitor closely after the drop for query performance regressions.
  4. Drop with CONCURRENTLY during a period of lower traffic, even though it doesn’t block, just to minimize any incidental overhead.
  5. Monitor query performance and pg_stat_statements after the drop to confirm nothing regressed.
  6. Keep the CREATE INDEX statement handy so you can quickly recreate it if something unexpected shows up.

Common Use Cases

Troubleshooting Common Issues

“cannot drop index because constraint depends on it” — the index backs a UNIQUE or PRIMARY KEY constraint. Drop the constraint with ALTER TABLE ... DROP CONSTRAINT instead.

Drop hangs indefinitely — a plain (non-concurrent) DROP INDEX is waiting to acquire an exclusive lock, likely blocked by a long-running query or transaction on the same table. Check pg_stat_activity for blocking sessions, or switch to DROP INDEX CONCURRENTLY.

“DROP INDEX CONCURRENTLY cannot run inside a transaction block” — remove any surrounding BEGIN/COMMIT, or if you’re running this from a migration tool, check whether it wraps every statement in a transaction automatically and needs a specific flag to exclude this one.

Index drop left an invalid index behind — this can happen if a CONCURRENTLY operation is interrupted. Check for it with:

SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;

Drop the invalid index (a plain DROP INDEX works fine on an already-invalid index) and, if you still need it, recreate it with CREATE INDEX CONCURRENTLY.

Query performance regressed after dropping an index — this is exactly why keeping the original CREATE INDEX statement handy matters. Recreate it (ideally with CONCURRENTLY) and revisit your usage analysis — the index may have been supporting a query pattern your monitoring window missed.

Best Practices

Wrapping Up

Dropping an index is a simple command, but doing it responsibly means backing the decision with real usage data, understanding the locking implications on a live table, and being ready to reverse the decision quickly if it turns out you were wrong. Get comfortable with pg_stat_user_indexes, default to CONCURRENTLY in production, and treat index cleanup as an ongoing part of database maintenance rather than a one-time cleanup project.

Exit mobile version