A migration that feels slow is usually one of three things: waiting for a lock, doing I/O, or burning CPU. Those have different fixes, so the first step is to tell them apart instead of guessing from the migration log. Postgres exposes that distinction in pg_stat_activity through the state, wait_event_type and wait_event columns.
A useful first query, adapted from Crunchy Data's lock investigation guide, lists every non-idle backend with how long its transaction and current query have been running the Crunchy Data walkthrough:
SELECT
pid,
usename,
application_name,
state,
wait_event_type,
wait_event,
now() - xact_start AS xact_age,
now() - query_start AS query_age,
left(query, 60) AS query
FROM pg_stat_activity
WHERE state <> 'idle'
AND pid <> pg_backend_pid()
ORDER BY query_age DESC;
Read the output as a decision table:
| state | wait_event_type | wait_event | What it means for the migration |
|---|---|---|---|
| active | Lock | relation | Waiting on a table lock held by another session |
| active | Lock | transactionid | Waiting on a row lock or a transaction that touched rows it needs |
| active | Lock | extension | Waiting on a lock that a background process or extension holds |
| active | IO | DataFileRead, WALWrite | Working, but the storage is the bottleneck |
| active | CPU | (usually NULL or LWLock) | Working, or contending on an internal lightweight lock |
| idle in transaction | Client | ClientRead | Holding locks while doing nothing; the classic blocker |
| active | Client | ClientWrite | Sending a large result set to the client |
The row that matters most during a migration is idle in transaction. A connection pool or an ORM that opened a transaction and paused holds whatever locks it already took, and an ALTER TABLE waiting for ACCESS EXCLUSIVE will sit behind it indefinitely. Bytebase's guide to reading pg_stat_activity for lock problems makes the same recommendation: filter on active sessions with a non-null wait event, and if the same pid keeps showing the same wait across two runs a few seconds apart, treat it as a real wait rather than noise the Bytebase lock guide.



