PostgreSQL ALTER TABLE 一直等锁,别只按 query_start 找阻塞事务
结合事务开始时间与实际阻塞PID查锁等待,分别处理取锁超时和空闲事务终止,避免误杀会话。
给一张不大的表加一个可空字段,ALTER TABLE 长时间停在锁等待。查看 pg_stat_activity 时,某个 idle in transaction 会话的 query_start 比迁移开始还晚,于是把它排除掉。这个时间比较不够:事务可能早已开始,后面的一次查询只是更新了 query_start。
以下按 PostgreSQL 18 的活动统计、表级锁和超时规则说明。现场没有采到阻塞关系时,不能补推某个开发者或某个 ORM 一定持有了锁;应在等待仍存在时读取事务状态与实际阻塞 PID。
空闲会话,也可能还在一个持锁事务里
普通 SELECT 会在涉及的表上取得 AccessShareLock。需要 AccessExclusiveLock 的结构变更与它冲突,而普通事务获得的锁通常一直保留到事务结束。查询已经返回、客户端暂时没有再发送 SQL,并不等于事务已经提交或回滚。
idle in transaction 表示会话处于事务中,正在等待客户端下一条命令。它与没有打开事务的 idle 不同。小表、可空列或预计执行很快,都不能免除结构变更需要取得的锁;具体 ALTER TABLE 形式应按对应版本核对。
query_start 是当前查询开始的时间;会话不在 active 状态时,它表示最后一条查询的开始时间。xact_start 才是当前事务开始的时间。不能因最后一条查询较新,就认为这个事务没有在迁移之前访问过目标表。
在等待发生时保存一次阻塞关系
使用已经授权、能查看必要活动统计的数据库账号。下面是当前数据库的一次只读查询,不会取消或终止会话;结果可能包含用户名和应用信息,应保存到受控记录中:
SELECT pid, usename, application_name,
state, xact_start, query_start,
wait_event_type, wait_event,
pg_blocking_pids(pid) AS blocking_pids
FROM pg_stat_activity
WHERE datname = current_database()
AND pid <> pg_backend_pid()
AND xact_start IS NOT NULL
ORDER BY xact_start;
先找到正在等待的迁移 PID,再沿 blocking_pids 查看阻塞会话及其事务开始时间。必要时由数据库负责人结合 pg_locks 和应用日志确认目标对象与事务用途。统计可见性受权限限制,缺字段或空结果不能自动证明没有阻塞。
pg_blocking_pids 也会列出排在前面的冲突锁请求,不能把每一个返回 PID 都解释成已经拿着目标锁。并行执行可能产生重复 PID;预备事务可能用 PID 0 表示,不能把它当成普通进程来终止。频繁调用还会增加锁管理开销,避免用高频循环刷新整个实例。
迁移结束后再查询,原阻塞关系可能已经消失。没有现场结果时,只能说明可能的持锁路径,不能把后来一次成功重试当成先前根因的证明。
先结束已确认的业务事务,再安排迁移
由事务的拥有者核对业务状态,正常提交或回滚确实不应继续打开的事务。直接取消一条查询不一定结束仍打开的事务,尤其不能通过批量终止所有 idle 会话处理锁等待。
确需终止会话时,应先确认 PID、当前连接用途和未提交写入,评估应用重连与回滚影响,并按变更授权处理。不能因会话暂时空闲就认为终止没有数据或可用性影响。
迁移自身可以采用经过评估的 lock_timeout,让取锁失败及时返回,而不是无限等待。它限制的是每次取锁等待,会中止语句;事务随后仍须按失败流程处理。优先限定在这次迁移的连接或事务范围,并在结束后恢复原设置,不给整个实例套一个随意选定的秒数。
idle_in_transaction_session_timeout 则会终止长时间在打开事务中等待客户端的会话。它不是正在执行 SQL 的运行超时,也不等同于 idle_session_timeout。设置角色或连接策略前,先评估合法的业务等待、连接池处理和重连行为。
修改前保存迁移脚本、超时设置及数据库恢复资料。在隔离环境确认超时与失败处理,再通过获准迁移入口执行,验收目标结构、迁移版本及业务读写。若等待消失但迁移已经执行,恢复超时或应用代码不会撤销字段变更;结构回退仍按迁移方案处理。


