> ## Content Index
> Fetch the complete content index at: https://mlog.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# PostgreSQL idle in transaction 락 대기: 쿼리 취소로 해결되지 않는 이유
- URL: https://mlog.me/postgresql-idle-in-transaction-lock-cancel-terminate/
- Published: 2026-10-23T05:38:24.000Z
- Updated: 2026-10-23T05:38:24.000Z
- Description: PostgreSQL 쿼리가 멈췄을 때 pg_blocking_pids로 차단 세션을 찾고, idle in transaction에서 쿼리 취소·트랜잭션 종료·연결 종료를 구분합니다.
- Author: mLog
- Tags: 데이터베이스, PostgreSQL, 트랜잭션

UPDATE가 끝나지 않을 때 오래 걸린 쿼리부터 취소하면 문제가 남을 수 있다. 먼저 **기다리는 세션과 잠금을 유지하는 세션을 구분**해야 한다. 차단 세션이 `idle in transaction`이라면 현재 실행 중인 SQL이 없어도 트랜잭션은 열려 있을 수 있다.

이 글은 2026-10-23 PostgreSQL 18 공식 문서를 기준으로 작성한 진단 예제다. 아래 재현은 별도 시험 DB용이며 이번 작성 환경에서 실행한 결과가 아니다. 운영 장애를 해결했다는 경험이나 성능 개선 수치는 제시하지 않는다.

## 1\. active와 Lock을 함께 읽는다

우선 접속한 DB와 버전을 확인한다.

```sql
SELECT current_database(), version();
SELECT pid, application_name, state,
       wait_event_type, wait_event,
       xact_start, query_start, state_change,
       left(query, 300) AS query_sample
FROM pg_stat_activity
WHERE datname = current_database()
  AND pid <> pg_backend_pid()
  AND (wait_event_type = 'Lock'
       OR state = 'idle in transaction')
ORDER BY xact_start NULLS LAST;

```

`active`는 SQL을 처리 중이라는 뜻이며 CPU를 계속 쓴다는 뜻은 아니다. `Lock` 대기는 잠금 계층을 조사할 신호다. 반대로 대기 이벤트가 비었다고 CPU 병목이 확정되는 것도 아니다. 순간 관측만으로 원인을 단정하지 않는다.

`query_start`는 잠금 대기 시작 시각이 아니다. 비활성 세션의 `query`는 마지막 SQL일 수 있으므로 이를 현재 실행 중인 쿼리라고 표시하면 오진한다. 다른 계정의 세부 정보가 가려지는 경우도 있다. 필요한 모니터링 권한을 DBA와 확인하고 앱 계정에 광범위한 권한을 추가하지 않는다. 쿼리 문자열에는 개인정보나 값이 포함될 수 있어 외부 공유 전에 가린다. [활동 통계 공식 문서](https://www.postgresql.org/docs/18/monitoring-stats.html?ref=mlog.me)

전체 모니터링 흐름은 [PostgreSQL 모니터링 쿼리 모음](https://mlog.me/postgresql-monitoring-queries/)에서 확인하고, 여기서는 차단 관계만 좁혀 본다.

## 2\. 누구를 취소할지 정하기 전에 차단 관계를 확인한다

```sql
SELECT w.pid AS waiting_pid,
       w.application_name AS waiting_app,
       p.blocking_pid,
       b.backend_start AS blocking_backend_start,
       b.application_name AS blocking_app,
       b.state AS blocking_state,
       b.xact_start AS blocking_xact_start,
       left(b.query, 300) AS blocking_last_query
FROM pg_stat_activity AS w
CROSS JOIN LATERAL
  unnest(pg_blocking_pids(w.pid)) AS p(blocking_pid)
LEFT JOIN pg_stat_activity AS b ON b.pid = p.blocking_pid
WHERE w.datname = current_database()
  AND w.wait_event_type = 'Lock';

```

이 SQL은 현재 직접 차단 관계를 보여주는 예다. 모든 대기 사슬의 최상위 원인을 자동으로 판정하지 않는다. 차단 세션도 다른 세션을 기다릴 수 있어 그 PID의 관계를 추가로 확인한다.

함수 결과는 이미 잠금을 가진 세션뿐 아니라 대기열 앞에서 충돌을 만드는 세션도 포함할 수 있다. 병렬 처리 때문에 PID가 중복될 수 있고 준비된 트랜잭션은 PID 0으로 나타날 수 있다. 그래서 왼쪽 조인으로 연결 정보를 보존했다. PID 0은 종료할 일반 세션으로 취급하지 않는다. 빠른 반복 호출도 피한다. [pg\_blocking\_pids 공식 문서](https://www.postgresql.org/docs/18/functions-info.html?ref=mlog.me)

## 3\. 세 세션으로 차이를 재현한다

다음은 업무 데이터가 없는 별도 시험 DB에서만 수행한다. 같은 DB에 A·B·관측용 C 세 연결을 연다. A와 B는 명시적으로 연 트랜잭션이 유지되게 하고, 관측용 C는 자동 커밋으로 매 조회를 별도 트랜잭션에서 실행한다. C에서 트랜잭션을 계속 열어 두면 활동 정보가 같은 관측값으로 보일 수 있다. GUI 도구의 자동 커밋 설정을 각각 확인한다. 공유 테이블이 필요하므로 TEMP TABLE은 사용하지 않는다.

초기 설정 연결에서 다음을 실행한다. 이름이 이미 존재하면 다른 고유 이름을 사용하고 기존 객체를 삭제하지 않는다.

```sql
CREATE TABLE public.mlog_lock_demo_20261023 (
  id integer PRIMARY KEY,
  value integer NOT NULL
);
INSERT INTO public.mlog_lock_demo_20261023 VALUES (1, 0);

```

세션 A:

```sql
SET application_name = 'mlog_lock_demo_A';
BEGIN;
UPDATE public.mlog_lock_demo_20261023
SET value = value + 1 WHERE id = 1;
-- 여기서 COMMIT/ROLLBACK을 하지 않고 대기한다.

```

세션 B:

```sql
SET application_name = 'mlog_lock_demo_B';
BEGIN;
SET LOCAL lock_timeout = '30s';
UPDATE public.mlog_lock_demo_20261023
SET value = value + 1 WHERE id = 1;

```

30초는 관측 시간을 제한하기 위한 예시이며 운영 권장값이 아니다. B가 대기하는 동안 C에서 앞의 진단 SQL을 실행한다. 예상 관찰은 A의 열린 트랜잭션과 B의 잠금 대기다. 실제 PID·대기 이벤트·시각은 결과에서 확인한다.

관측 후 A에서 `ROLLBACK;`을 실행한다. B의 UPDATE가 완료되면 B에서도 `ROLLBACK;`을 실행해 시험 변경을 남기지 않는다. B가 먼저 타임아웃됐어도 B에서 `ROLLBACK;`을 실행해 오류 상태의 트랜잭션을 정리한다. 두 연결이 종료된 것을 확인한 뒤에만 시험 테이블을 정리한다.

```sql
DROP TABLE public.mlog_lock_demo_20261023;

```

이는 자신이 방금 만든 시험 객체에 한정한다. 실제 서비스 테이블에 적용하면 안 된다. 트랜잭션 종료와 잠금 관계는 [잠금 공식 문서](https://www.postgresql.org/docs/18/explicit-locking.html?ref=mlog.me)를 기준으로 판단한다.

## 4\. 쿼리 취소와 연결 종료는 효과가 다르다

`pg_cancel_backend`는 대상의 현재 쿼리를 취소하는 기능이다. 유휴 상태에서 열린 트랜잭션을 닫는 명령으로 사용하지 않는다. 차단 중인 트랜잭션의 소유자가 정상적인 업무 판단에 따라 COMMIT 또는 ROLLBACK하는 경로를 먼저 확인한다. 다른 연결에서 실행한 `ROLLBACK`은 그 연결 자신의 트랜잭션에만 작용한다.

`pg_terminate_backend`는 연결을 종료한다. 실행 중 요청 실패와 미커밋 작업 손실, 앱의 재접속·재시도까지 영향을 고려해야 한다. 반환값만 보고 복구 완료로 판단하지 말고 대상 연결 종료와 대기 해소를 다시 조회한다. 실행 전 PID뿐 아니라 `backend_start`, 사용자, 애플리케이션, 트랜잭션 내용을 재확인한다. 오래된 화면의 PID를 그대로 재사용하지 않는다.

이 글은 일괄 종료 SQL을 제공하지 않는다. 어떤 트랜잭션을 포기해도 되는지 판단하는 것은 조회 조건만으로 자동화할 수 없기 때문이다. [서버 신호 함수 공식 문서](https://www.postgresql.org/docs/18/functions-admin.html?ref=mlog.me)

## 5\. 타임아웃은 트랜잭션 설계를 대신하지 않는다

`lock_timeout`은 잠금 획득 대기에 적용하며 잠금 시도마다 따로 계산된다. `statement_timeout`은 문장 전체 실행 시간을 제한한다. 두 값을 같은 기준으로 읽지 않는다. 전역 설정부터 바꾸기보다 작업별 허용 시간과 앱 오류 처리를 확인한다.

`idle_in_transaction_session_timeout`은 열린 트랜잭션에서 클라이언트 요청을 기다리는 연결에 대한 정책이다. 연결 풀이 끊긴 연결을 어떻게 처리하는지 시험한 뒤 도입한다. 시간 초과 후 무제한 즉시 재시도하면 부하를 더할 수 있다. [연결 기본 설정 공식 문서](https://www.postgresql.org/docs/18/runtime-config-client.html?ref=mlog.me)

트랜잭션 안에서 외부 API나 사용자 입력을 기다리는 구간이 있는지도 점검한다. 대기 중인 연결 때문에 앱 풀이 고갈됐다면 [HikariCP와 max\_connections 구분](https://mlog.me/postgresql-hikaricp-pool-size-max-connections-budget/)을 함께 확인한다. 풀 크기를 늘리는 것만으로 차단 원인이 사라지지는 않는다.

잠금 대기가 해소된 뒤 반복적으로 부담을 만드는 SQL을 찾을 때는 [pg\_stat\_statements 구간 비교](https://mlog.me/postgresql-pg-stat-statements-snapshot-delta-slow-queries/)로 이어간다. 현재 대기 관계와 완료된 실행의 누적 통계는 구분해서 읽는다.