기술 · 실용 가이드
PostgreSQL에서 ‘database is being accessed by other users’가 뜬다면? 강제 삭제 전에 세션 원장을 만드세요
SQLSTATE 55006을 없애기 전에 누가 그 상태를 소유하는지 확인하세요. 세션, 준비된 트랜잭션, 논리 복제 객체와 가시성 한계를 읽기 전용 원장으로 남긴 뒤, 새 스냅샷을 한 번 더 수집해 재연결 경합을 찾습니다.

‘database is being accessed by other users’ 오류는 원인 진단이 아닙니다. 배타적 접근이 필요한 작업을 수행하려는 순간 대상 데이터베이스에 연결이나 클러스터 객체가 남아 있어 PostgreSQL이 거부한 상태입니다. 첫 번째 목표는 숫자를 0으로 만드는 것이 아니라, 보이는 모든 소유자와 보이지 않는 모든 경계를 설명할 증거를 보존하는 것입니다.
애플리케이션 객체가 해제된 뒤에도 연결 풀이 새 세션을 열 수 있습니다. 관리 도구 자체가 실수로 종료하려는 연결일 수도 있습니다. PostgreSQL 공식 문서는 강제 데이터베이스 삭제로도 정리되지 않는 세 가지 유형을 명시합니다. 준비된 트랜잭션, 활성 논리 복제 슬롯, 구독입니다. 관리형 서비스에서는 남은 백엔드가 고객에게 종료 권한이 없는 공급자 역할에 속할 수도 있습니다.
이 가이드는 어떤 변경도 하기 전에 멈춥니다. 하나의 읽기 전용 트랜잭션으로 서버와 데이터베이스 식별 정보, 세션 소유 단서, 준비된 트랜잭션, 논리 슬롯, 구독, 역할 가시성을 수집합니다. 그런 다음 완전히 새로운 두 번째 트랜잭션으로 재연결 경합을 찾습니다. 결과물은 데이터베이스 소유자나 공급자의 변경 절차로 넘길 증거 묶음이며, 사용자를 취소하거나 권한을 바꾸고 PostgreSQL을 재시작하거나 데이터베이스 이름을 바꾸거나 삭제하는 스크립트가 아닙니다.
1. 문제를 지우기 전에 거부된 현장을 보존합니다
정확한 SQLSTATE, 대상 데이터베이스, 시도한 작업, UTC 시각, 관련 배포 또는 복원 단계, PostgreSQL이 보고한 연결 수를 기록합니다. 같은 사고 기록에 즉시 종료 쿼리를 붙이고 증거가 완성됐다고 판단하지 마세요. 풀이 재연결하거나 백엔드가 사라지면 소유 관계의 흔적이 달라집니다.
이 가이드에서 검토한 Stack Overflow 사례는 revoke와 terminate 시도 후에도 24개 세션을 보고했습니다. 별도의 DBA.StackExchange 사례에서는 유지 관리 데이터베이스에 연결된 스크립트가 자기 연결을 종료했습니다. 서로 다른 문제 유형이지만, 둘 다 ‘전부 죽이고 다시 시도’하는 방식이 차단 원인을 설명할 맥락을 잃게 함을 보여 줍니다.
복원이나 환경 교체 과정이라면 백업 증거와 변경 요청서를 별도로 보존하세요. 이 원장은 백업이 복원 가능하다는 사실, 데이터베이스를 버려도 된다는 사실, 애플리케이션 소유자가 중단을 승인했다는 사실을 증명하지 않습니다.
2. 수집기가 어느 서버와 데이터베이스를 사용하는지 증명합니다
postgres 또는 대상이 아닌 다른 데이터베이스로 새 유지 관리 세션을 여세요. PostgreSQL의 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은 현재 쿼리 정보가 트랜잭션에서 처음 요청될 때 스냅샷으로 잡히며 같은 트랜잭션 동안 일관되게 유지된다고 설명합니다. 여러 뷰를 연결하기에는 유용하지만, 같은 트랜잭션 안에서 기다리는 것으로는 나중에 필요한 새 비교 자료가 만들어지지 않습니다. 첫 번째 읽기 전용 패스를 커밋한 뒤 두 번째 스냅샷을 위해 새 트랜잭션을 시작합니다.
일반 역할도 다른 세션의 존재와 일반 속성은 볼 수 있지만, 다른 역할이 소유한 세션의 많은 필드는 null일 수 있습니다. pg_read_all_stats가 없다면 원장에 ‘가시성 불완전’을 표시해야 합니다. null은 증거가 없다는 뜻이 아니며 상태 변경 명령을 정당화하지도 않습니다.
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가 있는지도 원장에 기록합니다. 수집기가 올바른 유지 관리 데이터베이스에 연결됐다면 대상 행에 그 PID가 없어야 합니다.
application_name과 client_addr는 단서이지 최종 신원 정보가 아닙니다. application_name이 비어 있어도 실제 클라이언트일 수 있습니다. client_addr가 null이면 Unix 소켓이나 내부 프로세스일 수 있습니다. 역할, 엔드포인트, 배포 맥락과 시간이 함께 맞을 때만 특정 애플리케이션 소유자에게 귀속합니다.
5. 세션 목록이 놓치는 차단 유형을 확인합니다
대상 데이터베이스의 pg_prepared_xacts를 조회하고 트랜잭션 ID, GID, 준비 시각, 소유자, 데이터베이스를 기록합니다. 준비된 트랜잭션은 지속되는 2단계 상태이며 유휴 클라이언트 세션이 아닙니다. 이 가이드에서는 커밋하거나 롤백하지 않습니다. GID를 해당 애플리케이션 또는 트랜잭션 코디네이터에게 전달하세요.
대상 데이터베이스에 연결된 논리 pg_replication_slots를 조회하고 슬롯 이름, 플러그인, 유형, temporary, active, active_pid, 보존 중인 트랜잭션 식별자와 LSN 위치를 기록합니다. active가 false여도 슬롯은 소유자가 있는 복제 객체일 수 있습니다. 비활성이라는 이유만으로 삭제하지 마세요.
대상 데이터베이스 OID로 pg_subscription을 조회하고 구독 이름, 소유자, 활성 상태, 슬롯 이름, publication 이름만 기록합니다. subconninfo는 선택하지 마세요. PostgreSQL은 이 필드에 평문 비밀번호가 들어갈 수 있다고 경고하고 일반 사용자의 접근을 제한합니다. 구독은 복제 설계의 일부이지 일반 정리 목록이 아닙니다.
이 확인들은 장식이 아닙니다. PostgreSQL은 FORCE가 대상 데이터베이스의 준비된 트랜잭션, 활성 논리 복제 슬롯, 구독을 지우지 않는다고 명시합니다. 어느 유형이든 행이 나타나면 중지하고 소유 시스템의 처리 계획을 받습니다.
6. 완전히 새로운 두 번째 스냅샷으로 재연결 경합을 찾습니다
첫 번째 읽기 전용 트랜잭션을 커밋하고 UTC 시각과 함께 출력을 저장합니다. 애플리케이션 연결 풀 주기에 맞는 짧은 간격 뒤에 새 읽기 전용 트랜잭션을 열고 같은 쿼리를 다시 실행합니다. 첫 트랜잭션에 캐시된 결과를 두 번째 스냅샷으로 사용하지 마세요.
행 순서가 아니라 안정적인 키를 비교합니다. 세션은 PID와 backend_start, 준비된 트랜잭션은 GID, 복제 슬롯은 슬롯 이름, 구독은 OID 또는 이름을 사용합니다. 같은 역할, 애플리케이션, 클라이언트 계열에서 새 PID가 나타나면 풀이나 워커가 재연결 중일 수 있습니다. 공급자 소유 역할이 간헐적으로 나타난다면 잠깐의 빈틈을 노려 삭제하지 말고 관리형 서비스로 에스컬레이션합니다.
두 스냅샷이 다르면 올바른 결과는 ‘재연결 경합’입니다. 둘 다 보존하세요. 소유자가 계속 세션을 다시 만들고 있을 때 한순간 연결 수가 0인 것은 정지 상태를 증명하지 않습니다.
7. 소유자 분기를 분류하고 경계에서 멈춥니다
애플리케이션 풀: 여러 클라이언트 백엔드가 역할, 애플리케이션, 엔드포인트를 공유하거나 사라졌다가 새 PID로 돌아옵니다. 원장을 서비스 소유자에게 전달합니다. 관리 도구: 클라이언트 이름이나 로컬 소켓이 pgAdmin, psql, IDE, 마이그레이션 도구를 가리키면 해당 소유자의 정상 절차로 닫습니다. 백그라운드 워커: backend_type이나 애플리케이션 맥락으로 식별해 익명 사용자처럼 다루지 말고 구성 요소 소유자에게 에스컬레이션합니다.
준비된 트랜잭션: pg_prepared_xacts에 대상 행이 있으며 결정권은 트랜잭션 코디네이터에게 있습니다. 복제 객체: 논리 슬롯이나 구독이 대상을 가리키며 수명 주기는 복제 소유자가 결정합니다. 관리형 서비스: 공급자 또는 슈퍼유저 역할은 보이지만 고객 역할로 조치할 수 없다면 두 스냅샷을 첨부해 공급자 지원 건을 엽니다.
가시성 불완전: 수집기에 pg_read_all_stats가 없거나 관련 필드가 가려졌거나 서버 엔드포인트가 모호하거나 필요한 카탈로그 쿼리가 거부된 상태입니다. 이 분기에서는 항상 중지합니다. 권한 있는 데이터베이스 관리자나 공급자에게 같은 원장을 다시 실행하도록 요청하고, 권한 오류를 객체 부재로 해석하지 마세요.
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 실제 환경 통합 테스트가 아니다.
운영에 쓰기 전 권한 있는 관리자가 같은 주 버전과 관리형 서비스 제한을 가진 비프로덕션 클러스터에서 실행하고 모든 결과 집합을 확인한 뒤 스크립트 해시를 첫 번째 스냅샷과 보관해야 한다. 커밋하고 연결 풀 한 주기를 기다린 뒤 정확히 같은 바이트를 두 번째 스냅샷으로 다시 실행한다. 카탈로그 거부, null 필드 또는 버전 차이는 미해결 경계이며 원장을 완전해 보이게 하려고 실패한 검사를 삭제하면 안 된다.
