PostgreSQL 안쪽
PostgreSQL 안쪽
PostgreSQL 을 오래 쓸수록 한 번쯤 마주치는 개념들이 있습니다. MVCC · 인덱스 · EXPLAIN · 격리 수준 · VACUUM · 파티셔닝.
1. MVCC
Multi-Version Concurrency Control 은 같은 행의 여러 버전을 동시에 보관해 읽기와 쓰기가 서로를 차단하지 않게 하는 동시성 제어 기법입니다. PostgreSQL 은 이 모델 위에 서 있습니다.
- 트랜잭션은 시작 시점의 스냅샷에서 데이터를 봅니다.
- 갱신은 새 행 버전을 만들고 옛 버전에 트랜잭션 ID 로 표시를 남깁니다.
- 어떤 트랜잭션도 더 이상 보지 않는 옛 버전은 VACUUM 의 정리 대상이 됩니다.
장점은 읽기 잠금 없이 일관된 스냅샷을 제공한다는 점입니다. 한계는 죽은 튜플(dead tuple) 이 쌓이며 발생하는 bloat 입니다.
2. 인덱스 종류
PostgreSQL 은 여러 인덱스 액세스 메서드를 기본 제공합니다.
| 종류 | 어울리는 자리 |
|---|---|
| B-tree | 동등·범위 비교 (기본값). 거의 모든 등호·부등호 검색. |
| Hash | 동등 비교 한정. WAL 기록은 9.6+ 에서 안정화. |
| GIN | 다중 값 컬럼 (tsvector · jsonb · 배열). 풀텍스트·키 존재 검사. |
| GiST | 범위·공간 (PostGIS), 유사도 검색의 일부. |
| SP-GiST | 공간 분할 (Quad-tree · KD-tree 류). |
| BRIN | 매우 큰 테이블의 자연 정렬된 컬럼 (타임스탬프 등). 작은 인덱스 크기. |
| BLOOM | 다중 컬럼 조합 동등 검사 (extension). |
색인 선택은 데이터 분포·쿼리 모양에 좌우됩니다. 잘못된 인덱스는 성능보다 디스크·쓰기 비용을 먼저 늘립니다.
3. EXPLAIN 과 EXPLAIN ANALYZE
EXPLAIN 은 플래너가 만든 실행 계획을 보여주고 EXPLAIN ANALYZE 는 실제 실행 시간·행 수까지 측정합니다.
EXPLAIN ANALYZE
SELECT id, email FROM users WHERE email = 'a@b.com';
읽을 때 자주 보는 노드들.
- Seq Scan: 테이블 전체 스캔. 작은 테이블·낮은 선택도에서 정상.
- Index Scan / Index Only Scan: 인덱스를 통한 접근. Index Only 는 visibility map 이 깨끗할 때만 의미가 있습니다.
- Bitmap Heap Scan: 여러 인덱스 결과를 비트맵으로 합쳐 한 번에 힙을 봅니다.
- Nested Loop / Hash Join / Merge Join: 조인 알고리즘. 데이터 크기·분포에 따라 플래너가 선택.
actual timevscost: cost 는 추정, actual time 은 실측. 차이가 크면 통계가 낡았을 가능성.
BUFFERS 옵션을 켜면 캐시 적중·디스크 읽기 양이 보입니다.
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
4. 트랜잭션 격리 수준
SQL 표준은 네 단계를 정의합니다. PostgreSQL 의 동작은 이렇습니다.
| 수준 | PostgreSQL 의 실제 동작 |
|---|---|
| Read Uncommitted | Read Committed 와 동일하게 처리 (더티 리드 발생 안 함) |
| Read Committed | 기본값. 각 문장마다 새 스냅샷. 비반복 읽기·팬텀 가능. |
| Repeatable Read | 트랜잭션 시작 시점 스냅샷 고정. SSI 보강으로 일부 팬텀 차단. |
| Serializable | SSI (Serializable Snapshot Isolation, 9.1 도입). 직렬화 가능한 결과 보장. 충돌 시 한쪽 트랜잭션 실패. |
수준을 높이면 안전성이 오르지만 충돌·재시도 비용이 늘어납니다. 대부분의 OLTP 는 Read Committed 로 충분합니다. 결제·인벤토리 같은 자리에서는 Serializable 또는 명시적 잠금을 검토합니다.
5. VACUUM 과 autovacuum
VACUUM 은 죽은 튜플을 회수하고 가시성 맵·통계를 갱신합니다. autovacuum 데몬이 임계치에 따라 자동으로 실행합니다.
VACUUM(기본): 공간을 free space map 에 돌려줍니다. 디스크는 줄지 않습니다.VACUUM FULL: 테이블을 다시 쓰며 디스크까지 줄입니다. 그동안 ACCESS EXCLUSIVE 락이 걸려 운영 중에는 위험합니다.ANALYZE: 통계 수집. 플래너 정확도에 영향.
bloat 는 죽은 튜플이 회수보다 빨리 쌓일 때 발생합니다. 결과는 디스크 사용량 증가, 인덱스 비대, 캐시 적중률 저하입니다. 대량 갱신·삭제 패턴이 있는 테이블에서 흔합니다. 점검 도구로 pgstattuple extension 이 자주 거론됩니다.
6. 파티셔닝
PostgreSQL 10 부터 선언적 파티셔닝이 들어왔습니다. 한 테이블을 키 기준으로 자식 테이블로 나눠 저장합니다.
CREATE TABLE events (
id BIGSERIAL,
occurred_at TIMESTAMPTZ NOT NULL,
payload JSONB
) PARTITION BY RANGE (occurred_at);
CREATE TABLE events_2026_04 PARTITION OF events
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
장점은 큰 테이블의 인덱스·VACUUM 비용을 파티션 단위로 분산할 수 있다는 점입니다. 한계는 각 파티션이 별도 통계를 가지고, 글로벌 인덱스가 (현재까지) 없어 PK · 유니크는 파티션 키를 포함해야 한다는 점입니다.
7. 분산 SQL · 시계열 전용
샤딩 자동화·분산 강한 일관성을 우선하면 Citus (PostgreSQL extension) · CockroachDB · YugabyteDB 같은 후보가 거론됩니다. 단일 노드 PostgreSQL 의 단순함을 잃는 대신 글로벌 규모를 얻습니다.
timescaledb 는 PostgreSQL 위에 시계열 하이퍼테이블·압축·연속 집계 뷰를 더합니다. InfluxDB 같은 자체 엔진이 별도 자리를 갖지만 "기존 SQL 도구를 그대로 쓰고 싶다" 는 요구에서는 timescaledb 가 자주 선택됩니다.
8. 운영 도구
CREATE INDEX CONCURRENTLY— 운영 트래픽이 있는 테이블의 인덱스를 만들 때 락을 줄입니다 (다만 테이블당 하나만 동시에 가능).pg_stat_statements— 쿼리별 누적 실행 시간·횟수·plan 을 봅니다. 튜닝의 입구입니다.pg_stat_activity·pg_locks— 현재 세션·잠금 점검. 데드락 추적.- 장기 실행 트랜잭션 — 긴 트랜잭션은 vacuum horizon 을 잡아 두어 bloat 를 가속합니다. 주기적인 점검이 필요합니다.
9. 자주 걸리는 자리
JSONB 인덱스 선택 — 쿼리 모양에 맞춰 GIN(jsonb_path_ops) · 식 인덱스를 골라야 합니다. 무조건 GIN 이 정답은 아닙니다.
SELECT * + 큰 행 — TOAST 컬럼이 끌려 나오며 의외의 비용을 만듭니다.
잘못된 통계 — ANALYZE 가 오래 안 됐거나 샘플이 작으면 플래너가 잘못된 plan 을 고릅니다. default_statistics_target 을 컬럼별로 조정할 수도 있습니다.
SERIALIZABLE 의 재시도 의무 — 충돌이 나면 클라이언트가 트랜잭션을 다시 실행해야 합니다. 라이브러리·코드가 이를 가정해야 합니다.
인덱스 비대 — 같은 행이 자주 갱신되면 HOT 업데이트가 깨져 인덱스 항목이 늘어납니다. 주기적인 REINDEX CONCURRENTLY 를 검토합니다.
하고픈 말
PostgreSQL 안쪽은 한 번에 다 알 필요가 없습니다. EXPLAIN 을 읽을 줄 알면 시작이고, 격리 수준·VACUUM 은 사고가 나야 비로소 의미가 보입니다. 그 사고가 운영에 닿기 전에 먼저 만나는 글이 이 자리입니다.
Next
- three-layer-cache
- redis-roles
Postgres MVCC 문서 · Postgres 인덱스 종류 · EXPLAIN 사용법 · 트랜잭션 격리 · Routine Vacuuming 을 참고합니다.