--- id: db-lock-analysis title: Postgres Lock 분석 — Deadlock / Wait / 진단 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [database, postgres, lock, vibe-coding] tech_stack: { language: "SQL / Postgres", applicable_to: ["Backend"] } applied_in: [] aliases: [pg lock, deadlock, lock_timeout, statement_timeout, blocked queries, FOR UPDATE] --- # Postgres Lock 분석 > "느린 query" 가 사실 lock wait. **`pg_stat_activity` + `pg_locks`** 로 누가 누구를 차단. **lock_timeout / statement_timeout** 으로 무한 hang 방지. ## 📖 핵심 개념 - Row-level lock: SELECT FOR UPDATE / DELETE / UPDATE. - Table-level lock: ALTER / DROP / VACUUM FULL. - Advisory lock: 직접 잡음. - Deadlock: 양방 lock — Postgres 가 1개 abort. ## 💻 코드 패턴 ### Locking SELECT ```sql -- Row lock (다른 transaction 의 같은 row UPDATE 차단) SELECT * FROM accounts WHERE id = $1 FOR UPDATE; -- Skip locked (큐 패턴) SELECT * FROM jobs WHERE status = 'pending' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED; -- No wait (즉시 fail) SELECT * FROM accounts WHERE id = $1 FOR UPDATE NOWAIT; -- Share (read 차단 X but write 차단) SELECT * FROM accounts WHERE id = $1 FOR SHARE; ``` ### Lock conflicts ``` SELECT : ACCESS SHARE SELECT FOR UPDATE / SHARE : ROW SHARE → ROW EXCLUSIVE INSERT/UPDATE/DELETE : ROW EXCLUSIVE CREATE INDEX : SHARE CREATE INDEX CONCURRENTLY : SHARE UPDATE EXCLUSIVE (덜 차단) ALTER TABLE : ACCESS EXCLUSIVE (모두 차단) ``` → ALTER TABLE = 모든 query 차단. Concurrent 가능한 변경 사용. ### Blocked queries 진단 ```sql SELECT blocked.pid AS blocked_pid, blocked.usename AS blocked_user, blocked.query AS blocked_query, blocking.pid AS blocking_pid, blocking.usename AS blocking_user, blocking.query AS blocking_query, age(NOW(), blocked.query_start) AS blocked_for FROM pg_stat_activity blocked JOIN pg_stat_activity blocking ON blocking.pid = ANY(pg_blocking_pids(blocked.pid)) WHERE NOT blocked.pid = blocking.pid; ``` → "이 query 가 저 query 를 대기 중". ### Lock timeout ```sql SET lock_timeout = '5s'; -- 5초 안 못 잡으면 ERROR -- Statement timeout SET statement_timeout = '30s'; -- 30초 안 안 끝나면 cancel ``` ```ts // App 에서 — 매 connection await pool.query("SET statement_timeout = '30s'"); await pool.query("SET lock_timeout = '5s'"); ``` 또는 connection string: ``` postgresql://...?options=-c%20statement_timeout=30s ``` ### Deadlock ``` Tx A: UPDATE a → wait for b (held by Tx B) Tx B: UPDATE b → wait for a (held by Tx A) → deadlock — Postgres 가 1개 abort ``` ```sql -- log log_lock_waits = on deadlock_timeout = 1s -- 1초 후 deadlock 검사 ``` → Deadlock 자주 = 코드에서 lock 순서 통일. ```ts // ❌ 다른 순서 async function transferA(from, to) { await update(from, ...); // lock from await update(to, ...); // lock to } async function transferB(from, to) { await update(to, ...); // lock to await update(from, ...); // lock from — deadlock 가능 } // ✅ 항상 ID 순 async function transfer(from, to) { const [first, second] = [from, to].sort(); await update(first, ...); await update(second, ...); } ``` ### Long-running transaction ```sql -- 30분+ transaction 검출 SELECT pid, usename, state, query, age(NOW(), xact_start) AS tx_age FROM pg_stat_activity WHERE state != 'idle' AND age(NOW(), xact_start) > INTERVAL '30 minutes'; ``` ```ts // 강제 cancel await db.query('SELECT pg_cancel_backend($1)', [pid]); // 또는 더 강력 await db.query('SELECT pg_terminate_backend($1)', [pid]); ``` ### Idle in transaction (큰 문제) ``` App 이 BEGIN → 외부 API call → 1분 hang → 다른 트랜잭션 차단 ``` ```sql -- 검출 SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction'; -- 한도 ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s'; SELECT pg_reload_conf(); ``` → 60초 후 자동 cancel. ### Migration safety ```sql -- ❌ Big table 에 column 추가 (default = NULL OK, default value 면 lock) ALTER TABLE orders ADD COLUMN x INT DEFAULT 0; -- PG 11+ = fast (metadata only). 옛 PG = full rewrite. -- ❌ NOT NULL 추가 ALTER TABLE orders ADD COLUMN x INT NOT NULL DEFAULT 0; -- 점진적 migration -- ✅ Safer pattern (아래 DB_Migration_Safety 참조) ``` ### Index — concurrently ```sql -- ❌ Lock 모든 write CREATE INDEX orders_user ON orders (user_id); -- ✅ Lock 없이 (느리지만 안전) CREATE INDEX CONCURRENTLY orders_user ON orders (user_id); ``` ### Advisory lock (app 레벨) ```sql -- 단일 instance 가 작업 SELECT pg_advisory_lock(42); -- bigint key -- 작업 SELECT pg_advisory_unlock(42); -- Transaction-scoped (자동 release) SELECT pg_advisory_xact_lock(hashtext('process-orders')); COMMIT; ``` → Cron / single-leader 패턴. ### Lock 통계 ```sql SELECT mode, count(*) FROM pg_locks GROUP BY mode; -- 어떤 종류 lock 가 많은지 -- Wait 시간 SELECT query, wait_event_type, wait_event, count(*) FROM pg_stat_activity WHERE wait_event IS NOT NULL GROUP BY 1, 2, 3 ORDER BY count DESC; ``` ### App 측 transaction 짧게 ```ts // ❌ 트랜잭션 안 외부 호출 await db.transaction(async (tx) => { const order = await tx.orders.find(id); const result = await fetch(externalApi); // 1초+ await tx.orders.update(...); }); // 1초 lock // ✅ const order = await db.orders.find(id); const result = await fetch(externalApi); await db.transaction(async (tx) => { await tx.orders.update(...); // ms }); ``` ### Optimistic vs Pessimistic ```sql -- Pessimistic — lock SELECT * FROM orders WHERE id = $1 FOR UPDATE; -- 작업 -- Optimistic — version check UPDATE orders SET ..., version = version + 1 WHERE id = $1 AND version = $expected_version; -- 0 row affected = 다른 process 가 변경 — retry ``` → Lock contention 적음 = optimistic. ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | 큰 query lock 차단 | lock_timeout | | 일반 prod | statement_timeout 30s | | Idle in transaction | idle_in_transaction_session_timeout | | Cron leader | Advisory lock | | 큰 table ALTER | concurrent migration patterns | | Update conflict | Optimistic version | ## ❌ 안티패턴 - **Lock timeout 없음**: hang. 5-30s 항상. - **Long transaction (30분+)**: autovacuum / 다른 query 차단. - **Idle in transaction 무한**: timeout 설정. - **Lock 순서 다름**: deadlock. - **ALTER TABLE prod + 큰 table**: 모두 차단. - **CREATE INDEX (non-concurrent) prod**: write 차단. - **App 이 외부 API 후 commit**: tx 길어짐. ## 🤖 LLM 활용 힌트 - statement_timeout + lock_timeout 항상. - pg_blocking_pids 로 진단. - 트랜잭션 안 외부 호출 금지. ## 🔗 관련 문서 - [[DB_Vacuum_Autovacuum]] - [[DB_Distributed_Locks]] - [[DB_Migration_Safety]]