科技 · 实用指南
PostgreSQL 提示“数据库正被其他用户访问”?强制删除前先建立会话台账
不要急着让 SQLSTATE 55006 消失。先用只读方式记录会话、预备事务、逻辑复制对象和可见性限制,再做第二次新快照捕捉重连竞态,把证据交给真正的所有者。

“数据库正被其他用户访问”不是根因诊断,而是 PostgreSQL 在需要独占访问的操作前发现目标数据库仍有连接或集群对象。首要任务不是把计数变成零,而是保存足够证据,解释每个可见所有者以及每个看不见的边界。
连接池可能在应用对象释放后重新建立会话;管理工具也可能正是你误杀的连接。官方文档还明确列出 FORCE 无法清除的三类阻塞:预备事务、活动的逻辑复制槽和订阅。托管服务里,残留后端还可能属于你无权终止的提供商角色。
本指南在任何变更前停止。它用一次只读事务记录服务器和数据库身份、会话所有权线索、预备事务、逻辑槽、订阅与角色可见性,再用第二个全新事务揭示重连竞态。产物是交给数据库所有者或服务商的证据包,不是取消会话、改权限、重启、改名或删库脚本。
1. 先保存拒绝现场,再考虑清理
记录完整 SQLSTATE、目标数据库、尝试的操作、UTC 时间、相关部署或恢复步骤,以及 PostgreSQL 报告的连接数。不要在同一事故记录里立刻粘贴终止查询并宣称证据完整;连接池重连或后端消失后,所有权轨迹会改变。
本次研究的 Stack Overflow 案例在 revoke 和 terminate 尝试后仍报告 24 个会话;另一个 DBA.StackExchange 案例中,脚本终止了自己连接维护库的会话。两者原因不同,却共同说明“全部杀掉再试”会丢失解释阻塞所需的上下文。
若操作属于恢复或环境替换,备份证明和变更单应单独保存。本台账不能证明备份可恢复、数据库可丢弃,或应用所有者已批准停机。
2. 证明采集器连接的是哪台服务器和哪个数据库
新建维护会话并连接 postgres 或其他非目标数据库。DROP DATABASE 官方文档要求这种分离;同样的做法也能防止证据采集器成为它正在解释的目标会话之一。
记录 current_database()、current_user、session_user、pg_backend_pid()、inet_server_addr()、inet_server_port()、server_version、server_version_num 和 pg_is_in_recovery()。把端点与应用配置和变更单对照;若代理或托管端点使集群身份无法证实,就标记“集群身份未决”,不要只凭熟悉的主机名推断。
第一条查询还应判断 current_database() 是否等于目标库。若为真,停止并改连维护库;若 pg_database 中没有目标库,也要停止,因为拼写错误、连错集群或变更已完成都是不同事件。
3. 建立有边界的只读快照
在一次采集过程中使用 BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY。记录目标数据库 OID、所有者、datallowconn、连接上限、采集角色是否具备 CONNECT,并记录 pg_read_all_stats 成员资格与 track_activities 状态。
PostgreSQL 说明,当前查询信息在事务内首次请求时形成快照,之后保持一致。这有利于关联多个视图,却也意味着在同一事务里等待不会得到真正的新对照。提交第一轮只读事务,再启动新事务收集第二轮。
普通角色能看到其他会话存在及部分通用属性,但其他用户会话的许多字段可能为空。缺少 pg_read_all_stats 时必须标记“可见性不完整”。空值不是“没有证据”,更不能授权任何变更型命令。
4. 清点会话,但不要导出 SQL 正文
对 datname 等于目标库的记录,收集 pid、leader_pid、usename、application_name、client_addr、client_port、backend_type、state、backend_start、xact_start、query_start、state_change、wait_event_type 与 wait_event。按后端类型、角色、应用、客户端和 PID 排序,以便识别成组连接池。
默认不要导出 query 列。查询文本可能包含客户数据、字面量或凭据,而确认连接所有者并不需要它。台账还应标明目标行中是否含采集器 PID;若采集器正确连到维护库,目标行不应包含它。
application_name 和 client_addr 只是线索,不是最终身份。空应用名仍可能是真实客户端;client_addr 为空可能代表 Unix socket 或内部进程。只有角色、端点、部署背景与时间一致时,才把记录归属给应用所有者。
5. 检查会话列表遗漏的阻塞对象
查询目标库的 pg_prepared_xacts,记录事务 ID、GID、准备时间、所有者和数据库。预备事务是持久的两阶段状态,不是空闲客户端会话。本指南不提交也不回滚它;把 GID 交给拥有它的应用或事务协调器。
查询数据库为目标库的逻辑 pg_replication_slots,记录槽名、插件、类型、temporary、active、active_pid、保留事务标识和 LSN。即使 active 为 false,复制槽仍可能是有所有者的对象,不能仅凭“不活动”就删除。
通过目标数据库 OID 查询 pg_subscription,只记录订阅名、所有者、启用状态、槽名与 publication 名称。不要选择 subconninfo;官方文档警告其中可能含明文密码,并限制普通用户访问。订阅属于复制设计,不属于通用清理清单。
这些检查不是装饰。PostgreSQL 明确说明 FORCE 不会清除预备事务、活动逻辑复制槽或目标库订阅。任何一类出现记录,都应停止并取得所属系统的处理方案。
6. 用第二次全新快照暴露重连竞态
提交第一轮只读事务,并将输出连同 UTC 时间保存。等待一个适合该应用连接池周期的短间隔后,开启新的只读事务,重复完全相同的查询;不能把仍在缓存中的第一事务当成第二快照。
比较稳定键而非行顺序:会话用 PID 加 backend_start,预备事务用 GID,复制槽用槽名,订阅用 OID 或名称。相同角色、应用和客户端族出现新 PID,通常意味着连接池或 worker 在重连。间歇出现的提供商角色应进入托管服务升级路径,而不是等待空档抢着删库。
两次快照不一致时,结论应是“重连竞态”,并同时保存两份。某一瞬间计数为零,不能在所有者持续补回会话时证明系统已经静默。
7. 归类所有者分支,并在边界处停止
应用连接池:多个客户端后端共享角色、应用与端点,或消失后以新 PID 返回;把台账交给服务所有者。管理工具:客户端名或本地 socket 指向 pgAdmin、psql、IDE 或迁移工具;通过其正常流程关闭。后台 worker:由 backend_type 或应用背景识别;升级给组件所有者,而非当作匿名用户。
预备事务:pg_prepared_xacts 有目标记录,决定权属于事务协调器。复制对象:逻辑槽或订阅指向目标库,生命周期由复制所有者决定。托管服务:可看到提供商或超级用户角色,但客户角色无权处理;带两份快照向服务商开单。
可见性不完整:采集器没有 pg_read_all_stats、关键字段被遮蔽、服务器端点不明确,或必要目录查询被拒绝。这个分支必须停止,请授权 DBA 或服务商用同一台账重跑;不能把权限错误解释成对象不存在。
8. 保存脱敏证据包,而不是清理脚本
保存事故时间、目标名和 OID、维护库、端点与版本、采集角色和 PID、pg_read_all_stats 与 track_activities 结果、两次快照、分类、未决字段以及下一决策的责任人。支持渠道不需要客户端地址时应脱敏。
不要放入完整 SQL 正文、密码、连接字符串或 subconninfo。证据包跨团队传递时应计算哈希并记录采集人。它必须让另一位授权操作员区分“没有可见会话”“没有阻塞对象”和“采集器看不到阻塞对象”。
退出条件故意严格:所有可见会话和集群对象都有所有者;两次快照稳定;采集器具备足够可见性;目标、端点与维护库已证实;备份与停机决定另有批准。此前结论只能是“仍需证据”,不是终止、改权、重启、改名或删库许可。
9. 将内嵌只读工作表完整运行两次
把 target_db 设为准确数据库名,并从 postgres 或另一维护库运行以下工作表。脚本按 UTF-8 计 2,365 字节,SHA-256 为 e787aced94fd049e0e6335d61a51bca6ef0d659f88264e0e2153276f273fc196。它开启 repeatable-read 只读事务,读取身份、数据库、会话、预备事务、逻辑复制槽和订阅证据,不选择原始查询正文或订阅连接字符串。
```sql \set ON_ERROR_STOP on \pset pager off \if :{?target_db} \else \echo 'Set target_db before running, for example: \\set target_db my_database' \quit 2 \endif BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY; SELECT clock_timestamp() AT TIME ZONE 'UTC' AS captured_at_utc, current_database() AS maintenance_database, :'target_db' AS target_database, current_database() = :'target_db' AS collector_is_on_target, current_user, session_user, pg_backend_pid() AS collector_pid, inet_server_addr()::text AS server_address, inet_server_port() AS server_port, current_setting('server_version') AS server_version, current_setting('server_version_num') AS server_version_num, pg_is_in_recovery() AS server_in_recovery, current_setting('track_activities') AS track_activities, pg_has_role(current_user, 'pg_read_all_stats', 'member') AS has_pg_read_all_stats; SELECT d.oid AS target_oid, d.datname AS target_database, pg_get_userbyid(d.datdba) AS database_owner, d.datallowconn AS connections_allowed, d.datconnlimit AS connection_limit, has_database_privilege(current_user, d.oid, 'CONNECT') AS collector_has_connect FROM pg_database AS d WHERE d.datname = :'target_db'; SELECT a.pid, a.leader_pid, a.usename, a.application_name, a.client_addr::text AS client_address, a.client_port, a.backend_type, a.state, a.backend_start, a.xact_start, a.query_start, a.state_change, a.wait_event_type, a.wait_event FROM pg_stat_activity AS a WHERE a.datname = :'target_db' ORDER BY a.backend_type, a.usename, a.application_name, a.client_addr, a.pid; SELECT x.transaction, x.gid, x.prepared, x.owner, x.database FROM pg_prepared_xacts AS x WHERE x.database = :'target_db' ORDER BY x.prepared, x.gid; SELECT s.slot_name, s.plugin, s.slot_type, s.database, s.temporary, s.active, s.active_pid, s.xmin, s.catalog_xmin, s.restart_lsn, s.confirmed_flush_lsn FROM pg_replication_slots AS s WHERE s.database = :'target_db' ORDER BY s.slot_name; SELECT s.oid AS subscription_oid, s.subname AS subscription_name, pg_get_userbyid(s.subowner) AS subscription_owner, s.subenabled AS enabled, s.subslotname AS slot_name, s.subpublications AS publications FROM pg_subscription AS s JOIN pg_database AS d ON d.oid = s.subdbid WHERE d.datname = :'target_db' ORDER BY s.subname; COMMIT; ```
相同字节已通过静态审计:只读事务边界、统一目标参数、五类目录覆盖、无原始 query 正文与 subconninfo、无状态变更 SQL 语句。静态审计只证明文本包含什么,并不等同于 PostgreSQL 实机集成测试。
投入运维前,应由授权管理员在相同大版本与托管限制的非生产集群运行工作表、检查每个结果集,并把脚本哈希与第一次快照一同保存。提交事务、等待一个连接池周期后,以完全相同字节运行第二次快照。若目录被拒、字段为空或版本不符,仍属于未决边界;不要删掉失败检查来让台账看起来完整。
