PostgreSQL migration
Does DROP INDEX lock the table in PostgreSQL?
Yes. A plain DROP INDEX takes an ACCESS EXCLUSIVE lock on the table, not just the index. The drop itself is instant, but it waits for every query using the table, and every new query waits behind it. DROP INDEX CONCURRENTLY takes a weaker lock and lets reads and writes carry on.
Tested on PostgreSQL 14, 15, 16, 17, 18 · Updated 9 October 2026
- Lock
- ACCESS EXCLUSIVE
- Blocks
- Reads and writes
- Rewrites the table
- No
Short answer
DROP INDEX orders_status_idx takes an ACCESS EXCLUSIVE lock on the
table, the strongest lock there is. Removing the index takes a millisecond, but the lock can’t be
granted while any query is using the table, and while it waits, every new SELECT and write on the
table queues behind it.
DROP INDEX CONCURRENTLY orders_status_idx avoids that. It takes a
SHARE UPDATE EXCLUSIVE lock, marks the index unusable, waits for
queries that might still be using it, and then removes it. Reads and writes carry on throughout.
What it locks
| Statement | Lock on the table | Lock on the index | Reads and writes while it waits |
|---|---|---|---|
DROP INDEX | ACCESS EXCLUSIVE | ACCESS EXCLUSIVE | Wait (queue behind it) |
DROP INDEX CONCURRENTLY | SHARE UPDATE EXCLUSIVE | SHARE UPDATE EXCLUSIVE | Run |
We tested the queue directly: with a transaction that had read the table still open, a plain
DROP INDEX waited 2.5 s for it, and a SELECT that arrived meanwhile waited behind the
DROP INDEX. With DROP INDEX CONCURRENTLY in the same situation, SELECT count(*), a lookup by
primary key and an INSERT all ran while it waited. While it waits, the index is already marked
invalid (indisvalid = false), so new queries stop using it.
Does it rewrite the table?
No. Neither form touches the table’s data; the index’s file is deleted when the transaction commits.
On our 3-million-row table a plain DROP INDEX took 1 ms once it had the lock.
Run it safely
Prefer DROP INDEX CONCURRENTLY:
DROP INDEX CONCURRENTLY orders_status_idx;
Its limits:
-
It can’t run in a transaction block:
ERROR: DROP INDEX CONCURRENTLY cannot run inside a transaction block. Turn off your migration tool’s transaction for this step. -
One index per statement, and no
CASCADE. -
It can’t drop an index that backs a primary key or unique constraint:
ERROR: cannot drop index seo_mig.t_pkey because constraint t_pkey on table seo_mig.t requires it HINT: You can drop constraint t_pkey on table seo_mig.t instead.ALTER TABLE … DROP CONSTRAINTtakes ACCESS EXCLUSIVE, so give it alock_timeouttoo. -
It can’t be used on indexes of partitioned tables (according to the PostgreSQL documentation).
It waits for every transaction using the table to finish, so a long one delays it; it doesn’t hold anyone else up meanwhile.
If you use a plain DROP INDEX (for example inside a transaction with other changes), set
lock_timeout so it gives up instead of building a queue:
SET lock_timeout = '2s';
DROP INDEX orders_status_idx;
On ERROR: canceling statement due to lock timeout, nothing changed: retry after a pause. See
lock timeout.
Before dropping, check the index isn’t needed. Indexes that are never scanned show
idx_scan = 0 (counts since statistics were last reset, on this server only, so check replicas
too):
SELECT indexrelid::regclass AS index, idx_scan
FROM pg_stat_user_indexes
WHERE relid = 'orders'::regclass
ORDER BY idx_scan;
How we checked
On PostgreSQL 14.24, 15.19, 16.14, 17.11 and 18.6 (Docker on a Mac). Lock and timing on the 3-million-row table, in a rolled-back transaction:
CREATE INDEX big_n_idx ON seo_mig.big (n);
BEGIN;
DROP INDEX seo_mig.big_n_idx;
SELECT relation::regclass, mode FROM pg_locks
WHERE pid = pg_backend_pid()
AND relation IN (SELECT oid FROM pg_class WHERE relnamespace = 'seo_mig'::regnamespace);
ROLLBACK;
(The dropped index no longer appears in pg_class inside the transaction, so only the table’s lock
shows in this query.)
For the concurrent drop, session A ran BEGIN; SELECT a FROM seo_mig.t WHERE a = 5; SELECT pg_sleep(4); COMMIT;,
session B ran DROP INDEX CONCURRENTLY seo_mig.t_a_idx half a second later, and session C read B’s
locks and ran statements with lock_timeout = '1s':
| Version | DROP INDEX lock on table (median time) | DROP INDEX CONCURRENTLY locks while waiting | Index while waiting | SELECT / INSERT meanwhile | Plain DROP INDEX behind the same reader |
|---|---|---|---|---|---|
| 14.24 | AccessExclusiveLock (1 ms) | ShareUpdateExclusiveLock on table and index | indisvalid = false | Runs / runs | Waited 2.5 s; a later SELECT waited |
| 15.19 | AccessExclusiveLock (1 ms) | ShareUpdateExclusiveLock on table and index | indisvalid = false | Runs / runs | Waited 2.5 s; a later SELECT waited |
| 16.14 | AccessExclusiveLock (1 ms) | ShareUpdateExclusiveLock on table and index | indisvalid = false | Runs / runs | Waited 2.5 s; a later SELECT waited |
| 17.11 | AccessExclusiveLock (1 ms) | ShareUpdateExclusiveLock on table and index | indisvalid = false | Runs / runs | Waited 2.5 s; a later SELECT waited |
| 18.6 | AccessExclusiveLock (1 ms) | ShareUpdateExclusiveLock on table and index | indisvalid = false | Runs / runs | Waited 2.5 s; a later SELECT waited |
DROP INDEX CONCURRENTLY returned after 3.5 s on every version, as soon as session A committed. The
transaction-block and constraint errors above were the same on all five.
In Inlet
The structure editor shows the DROP INDEX statement before it runs. If a drop is stuck waiting, the
Activity monitor shows the session it’s waiting for and the queries queued behind it, and lets you
cancel or terminate them.
Related
- Does CREATE INDEX CONCURRENTLY lock the table in PostgreSQL?
- Does CREATE INDEX lock the table in PostgreSQL?
- Does REINDEX lock the table in PostgreSQL?
- Does ALTER TABLE DROP COLUMN lock the table in PostgreSQL?
- ACCESS EXCLUSIVE lock in PostgreSQL
- SHARE UPDATE EXCLUSIVE lock in PostgreSQL
- canceling statement due to lock timeout