Tecnología · GUÍA PRÁCTICA
¿PostgreSQL dice «database is being accessed by other users»? Cree un registro de sesiones antes de forzar el borrado
No haga desaparecer SQLSTATE 55006 antes de saber quién lo provoca. Capture un registro de solo lectura, ligado al clúster, con sesiones, transacciones preparadas, objetos de replicación lógica y límites de visibilidad; después repita la captura para detectar reconexiones antes de cualquier acción destructiva.

El error «database is being accessed by other users» no es un diagnóstico. PostgreSQL está rechazando una operación que necesita acceso exclusivo porque todavía hay una conexión u objeto del clúster asociado a la base de datos objetivo. La primera tarea no es reducir el contador a cero, sino conservar pruebas suficientes para explicar cada propietario visible y cada límite de visibilidad.
Un pool puede abrir otra sesión después de que la aplicación haya liberado su objeto. La herramienta de administración también puede ser la conexión que usted está a punto de cerrar por error. PostgreSQL documenta además tres clases de bloqueo que ni siquiera un borrado forzado elimina: transacciones preparadas, slots activos de replicación lógica y suscripciones. En un servicio administrado, el backend restante puede pertenecer a un rol del proveedor que el cliente no puede terminar.
Esta guía se detiene antes de modificar nada. Una transacción de solo lectura captura la identidad del servidor y de la base de datos, indicios de propiedad de las sesiones, transacciones preparadas, slots lógicos, suscripciones y límites de visibilidad del rol. Una segunda transacción nueva revela carreras de reconexión. El resultado es un paquete de pruebas para el propietario de la base de datos o el proceso de cambios del proveedor, no un script para cancelar usuarios, cambiar privilegios, reiniciar PostgreSQL, renombrar o borrar una base de datos.
1. Conserve el rechazo antes de intentar eliminarlo
Registre el SQLSTATE exacto, la base objetivo, la operación intentada, la hora UTC, el paso de despliegue o restauración relacionado y el número de conexiones informado por PostgreSQL. No añada inmediatamente una consulta de terminación al mismo incidente y dé por completa la prueba. Cuando un pool se reconecta o un backend desaparece, cambia el rastro de propiedad.
El caso de Stack Overflow revisado para esta guía seguía mostrando 24 sesiones tras intentos de revoke y terminate. Otro caso en DBA.StackExchange muestra un script que termina su propia conexión a la base de mantenimiento. Son familias de fallo distintas, pero ambas demuestran que «matar todo y volver a intentar» destruye el contexto necesario para explicar el bloqueo.
Si la solicitud forma parte de una restauración o sustitución de entorno, conserve por separado la copia de seguridad y el ticket de cambio. Este registro no demuestra que una copia sea restaurable, que la base pueda desecharse ni que su propietario haya aprobado una interrupción.
2. Demuestre qué servidor y qué base usa el recolector
Abra una sesión de mantenimiento nueva contra postgres u otra base que no sea la objetivo. La documentación de DROP DATABASE exige esa separación, y la misma práctica evita que el recolector de pruebas se convierta en una de las sesiones que intenta explicar.
Capture current_database(), current_user, session_user, pg_backend_pid(), inet_server_addr(), inet_server_port(), server_version, server_version_num y pg_is_in_recovery(). Compare el endpoint con la configuración de la aplicación y el ticket de cambio. Si un proxy o endpoint administrado impide demostrar el clúster exacto, marque «identidad del clúster sin resolver» en vez de confiar en un nombre de host conocido.
La primera consulta también debe calcular si current_database() coincide con la base objetivo. Si es verdadero, deténgase y vuelva a conectar mediante otra base de mantenimiento. Si el objetivo no aparece en pg_database, deténgase: un nombre mal escrito, otro clúster o un cambio ya finalizado constituyen incidentes diferentes.
3. Abra una captura de solo lectura con límites claros
Use BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY para cada pasada. Registre el OID, propietario, datallowconn y límite de conexiones de la base objetivo, si el recolector dispone de CONNECT, la pertenencia a pg_read_all_stats y el valor de track_activities.
PostgreSQL explica que la información de consultas actuales se captura cuando se solicita por primera vez dentro de una transacción y permanece coherente durante ella. Esto ayuda a relacionar varias vistas, pero esperar dentro de la misma transacción no crea la comparación nueva que hará falta después. Confirme la primera pasada de solo lectura y abra otra transacción para la segunda captura.
Un usuario ordinario puede ver que existen sesiones ajenas y algunas propiedades generales, pero muchos campos de sesiones de otros roles pueden ser nulos. Sin pg_read_all_stats, el registro debe indicar «visibilidad incompleta». Un valor nulo no equivale a ausencia de evidencia y nunca autoriza una orden que modifique el estado.
4. Inventaríe sesiones sin exportar el texto SQL
Para las filas cuyo datname sea la base objetivo, capture pid, leader_pid, usename, application_name, client_addr, client_port, backend_type, state, backend_start, xact_start, query_start, state_change, wait_event_type y wait_event. Ordene por tipo de backend, rol, aplicación, cliente y PID para reconocer miembros repetidos de un pool.
No exporte query de forma predeterminada. El texto SQL puede contener datos de clientes, literales o credenciales y no es necesario para determinar quién posee una conexión. El registro también debe señalar si alguna fila objetivo contiene el PID del recolector; si este se conectó correctamente a la base de mantenimiento, esa marca debe ser falsa.
application_name y client_addr son indicios, no identidades definitivas. Una aplicación sin nombre puede seguir siendo un cliente real. Un client_addr nulo puede indicar un socket Unix o un proceso interno. Atribuya la fila a un propietario solo cuando concuerden el rol, el endpoint, el contexto del despliegue y el momento.
5. Revise los bloqueos que una lista de sesiones no muestra
Consulte pg_prepared_xacts para la base objetivo y registre el identificador de transacción, GID, hora de preparación, propietario y base. Una transacción preparada es estado duradero de dos fases, no una sesión de cliente inactiva. Esta guía no la confirma ni revierte; entregue el GID a la aplicación o coordinador de transacciones que la posee.
Consulte pg_replication_slots para los slots lógicos asociados a la base objetivo. Registre nombre, plugin, tipo, indicador temporary, estado active, active_pid, identificadores de transacción retenidos y posiciones LSN. Un slot inactivo puede seguir siendo un objeto de replicación con propietario. No lo elimine solo porque active sea false.
Consulte pg_subscription mediante el OID de la base objetivo y registre únicamente nombre de la suscripción, propietario, estado habilitado, nombre del slot y publicaciones. No seleccione subconninfo: PostgreSQL advierte que puede contener contraseñas en texto claro y restringe esa columna a usuarios ordinarios. Una suscripción pertenece al diseño de replicación, no a una lista genérica de limpieza.
Estas comprobaciones no son decorativas. PostgreSQL dice expresamente que FORCE no elimina transacciones preparadas, slots activos de replicación lógica ni suscripciones de la base objetivo. Si aparece una fila de cualquiera de estas clases, deténgase y solicite el plan del sistema propietario.
6. Realice una segunda captura nueva para descubrir reconexiones
Confirme la primera transacción de solo lectura y guarde su salida con una hora UTC. Tras un intervalo breve adaptado al ciclo del pool de la aplicación, abra una transacción de solo lectura nueva y repita las mismas consultas. No use como segunda captura los resultados almacenados en la primera transacción.
Compare claves estables, no el orden de las filas: PID más backend_start para sesiones, GID para transacciones preparadas, nombre para slots y OID o nombre para suscripciones. Un PID nuevo con el mismo rol, aplicación y familia de cliente sugiere que un pool o worker se reconecta. Un rol del proveedor que aparece de forma intermitente exige escalar al servicio administrado, no intentar borrar en un breve hueco.
Si las dos capturas no coinciden, el resultado correcto es «carrera de reconexión». Conserve ambas. Un contador cero durante un instante no demuestra inactividad cuando un propietario está creando sesiones de nuevo.
7. Clasifique la rama de propiedad y deténgase en el límite
Pool de aplicación: varios backends comparten rol, aplicación y endpoint, o desaparecen y regresan con PID nuevos; entregue el registro al propietario del servicio. Herramienta administrativa: el nombre del cliente o socket local señala pgAdmin, psql, un IDE o una herramienta de migración; ciérrela mediante el flujo normal de su propietario. Worker: backend_type o el contexto identifica un proceso de fondo; escale al propietario del componente en lugar de tratarlo como usuario anónimo.
Transacción preparada: pg_prepared_xacts contiene una fila objetivo; la decisión corresponde al coordinador. Objeto de replicación: un slot lógico o suscripción señala la base; su propietario decide el ciclo de vida. Servicio administrado: se ve un rol del proveedor o superusuario, pero el cliente no puede actuar; abra un caso con ambas capturas.
Visibilidad incompleta: el recolector carece de pg_read_all_stats, hay campos ocultos, el endpoint es ambiguo o se deniega una consulta de catálogo necesaria. Esta rama siempre se detiene. Pida a un administrador autorizado o al proveedor que repita el mismo registro; no deduzca ausencia a partir de un error de permisos.
8. Guarde un paquete de pruebas redactado, no un script de limpieza
Conserve la hora del incidente, nombre y OID de la base, base de mantenimiento, endpoint y versión, rol y PID del recolector, resultados de pg_read_all_stats y track_activities, las dos capturas, la clasificación, campos sin resolver y el propietario o equipo responsable de decidir. Redacte direcciones de clientes cuando la ruta de soporte no las necesite.
No incluya texto SQL completo, contraseñas, cadenas de conexión ni subconninfo. Calcule un hash si el paquete circulará entre equipos y registre quién lo obtuvo. El artefacto debe permitir distinguir «no hay sesiones visibles», «no hay bloqueos» y «el recolector no puede ver los bloqueos».
La prueba de salida es deliberadamente estricta: cada sesión y objeto visible tiene propietario; ambas capturas son estables; el recolector tiene visibilidad suficiente; se han demostrado objetivo, endpoint y base de mantenimiento; la copia y la interrupción cuentan con su propio cambio aprobado. Hasta entonces, el resultado es «faltan pruebas», no permiso para terminar, revocar, reiniciar, renombrar o borrar.
9. Ejecuta dos veces la hoja integrada de solo lectura
Define target_db con el nombre exacto de la base y ejecuta esta hoja desde postgres u otra base de mantenimiento. El script mide 2,365 bytes UTF-8 y su SHA-256 es e787aced94fd049e0e6335d61a51bca6ef0d659f88264e0e2153276f273fc196. Abre una transacción repeatable-read de solo lectura y consulta identidad, base, sesiones, transacciones preparadas, slots lógicos y suscripciones sin seleccionar el texto SQL original ni cadenas de conexión de suscripciones.
```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; ```
Los mismos bytes pasaron una auditoría estática del límite de solo lectura, el uso coherente del parámetro objetivo, las cinco familias de catálogos, la ausencia del texto query y de subconninfo, y la ausencia de sentencias SQL que cambien estado. La auditoría solo prueba qué contiene el texto; no es una prueba de integración en PostgreSQL real.
Antes de usarlo en operaciones, un administrador autorizado debe ejecutarlo en un clúster no productivo con la misma versión mayor y las mismas restricciones del servicio gestionado, revisar cada resultado y guardar el hash con la primera captura. Después de confirmar la transacción y esperar un ciclo del pool, debe ejecutar exactamente los mismos bytes como segunda captura. Un catálogo denegado, un campo nulo o una diferencia de versión sigue siendo un límite sin resolver; no elimines la comprobación fallida para que el registro parezca completo.
