TL;DR
- 확장이 PostgreSQL의 진정한 힘: pg_stat_statements(쿼리 분석), pgvector(AI), PostGIS(지리), TimescaleDB(시계열), pg_partman(파티셔닝)
- 3대 튜닝 파라미터:
shared_buffers(RAM 25%),work_mem(정렬/해시 메모리),effective_cache_size(RAM 50-75%) - Autovacuum이 핵심: 잘못 설정하면 테이블 bloat → 디스크 폭증 → 쿼리 느려짐.
autovacuum_vacuum_scale_factor튜닝 필수 - Patroni: PostgreSQL HA의 사실상 표준. 자동 failover 30초 이내 가능
- 백업: pgBackRest가 pg_dump보다 10x+ 빠름. WAL 아카이빙 + PITR 필수
1. PostgreSQL이 2025년에도 사랑받는 이유
1.1 핵심 이유 5가지
- 확장성(Extensibility) — 200+ 공식 확장. 벡터, 시계열, 지리정보까지 모두
- JSONB 성능 — MongoDB와 비슷하거나 더 빠른 JSON 처리
- MVCC — 읽기-쓰기 충돌 없음 (동시성 우수)
- 표준 준수 — ANSI SQL 가장 충실하게 구현
- 오픈소스 — Oracle/SQL Server 대비 라이선스 비용 0
1.2 NewSQL 시대에도 PostgreSQL이 표준인 이유
CockroachDB, YugabyteDB, Spanner 등 분산 SQL DB가 등장했지만, 대부분 PostgreSQL wire protocol 호환입니다. 이는 PostgreSQL 자체의 위치가 안정적임을 의미합니다.
2. 필수 확장 (Extensions) Top 10
2.1 운영 필수: pg_stat_statements
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = 'all'
pg_stat_statements.max = 10000
활용:
-- TOP 10 가장 느린 쿼리
SELECT
substring(query, 1, 80) as query,
calls,
total_exec_time::int as total_ms,
mean_exec_time::int as avg_ms,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
이 쿼리 하나로 80%의 성능 문제가 해결됩니다. 어떤 쿼리가 가장 많은 시간을 잡아먹는지 즉시 보입니다.
2.2 AI 시대의 필수: pgvector
CREATE EXTENSION IF NOT EXISTS vector;
-- 768차원 임베딩 저장
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(768)
);
-- HNSW 인덱스 (PostgreSQL 16+)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
-- 의미 기반 검색
SELECT id, content, 1 - (embedding <=> '[0.1, 0.2, ...]'::vector) AS similarity
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
왜 별도 Vector DB 대신 pgvector?
- 기존 PostgreSQL 운영 노하우 활용
- ACID 트랜잭션 (벡터 + 메타데이터 일관성)
- 비용 절감 (별도 인프라 불필요)
2.3 시계열: TimescaleDB
CREATE EXTENSION IF NOT EXISTS timescaledb;
CREATE TABLE metrics (
time TIMESTAMPTZ NOT NULL,
device_id TEXT,
temperature FLOAT,
humidity FLOAT
);
-- 하이퍼테이블로 변환 (자동 파티셔닝)
SELECT create_hypertable('metrics', 'time', chunk_time_interval => INTERVAL '1 day');
-- 자동 데이터 보존 (30일 후 삭제)
SELECT add_retention_policy('metrics', INTERVAL '30 days');
-- 연속 집계 (Continuous Aggregates)
CREATE MATERIALIZED VIEW metrics_hourly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS bucket,
device_id,
AVG(temperature) AS avg_temp
FROM metrics
GROUP BY bucket, device_id;
2.4 기타 핵심 확장
| 확장 | 용도 |
|---|---|
| PostGIS | 지리정보 (지도, 위치 검색) |
| pg_partman | 자동 파티션 관리 |
| pg_cron | DB 내 스케줄러 (cron job) |
| pgaudit | 감사 로깅 (규제 준수) |
| pglogical | 논리 복제 (다른 PG 버전 간) |
| citus | 수평 샤딩 (분산 PG) |
| pg_repack | 락 없이 테이블 재구성 |
3. 핵심 성능 튜닝
3.1 메모리 파라미터 (가장 중요)
| 파라미터 | 권장값 (16GB RAM 서버) | 설명 |
|---|---|---|
shared_buffers | 4GB (RAM 25%) | PostgreSQL 자체 캐시 |
effective_cache_size | 12GB (RAM 75%) | OS 캐시 추정 (옵티마이저 힌트) |
work_mem | 64MB | 정렬/해시당 메모리 |
maintenance_work_mem | 1GB | VACUUM/CREATE INDEX용 |
wal_buffers | 16MB | WAL 버퍼 |
계산 공식: shared_buffers + (work_mem × max_connections) ≤ RAM × 0.5
예: 16GB RAM, max_connections=100, work_mem=64MB → 4GB + (64MB × 100) = 4GB + 6.4GB = 10.4GB ✅ (16GB의 65%로 안전)
3.2 체크포인트 튜닝
# 너무 자주 체크포인트 → I/O 폭증
# 너무 드물게 체크포인트 → 복구 시간 증가
checkpoint_timeout = 15min # 기본 5분 → 15분
checkpoint_completion_target = 0.9 # 9분에 걸쳐 완료
max_wal_size = 4GB # WAL 크기 한계
min_wal_size = 1GB
3.3 쿼리 플래너
random_page_cost = 1.1 # SSD인 경우 (HDD는 4.0)
effective_io_concurrency = 200 # SSD/NVMe (HDD는 1)
default_statistics_target = 100 # 통계 정확도 (높으면 느린 ANALYZE)
3.4 연결 관리
max_connections = 100 # PgBouncer가 있다면 충분
중요: PostgreSQL의 max_connections를 무작정 늘리면 안 됩니다. 각 연결이 메모리를 잡아먹습니다. PgBouncer 같은 connection pooler를 사용하세요.
4. VACUUM과 Autovacuum의 모든 것
4.1 왜 VACUUM이 필요한가?
PostgreSQL의 MVCC는 UPDATE를 "INSERT 새 행 + 옛 행 표시"로 처리합니다. 옛 행은 즉시 삭제되지 않고 "dead tuple"로 남습니다. VACUUM이 이를 청소합니다.
VACUUM 없이 일어나는 일:
- 테이블 크기가 계속 증가 (디스크 폭증)
- 인덱스가 dead tuple을 가리켜 쿼리 느려짐
- 트랜잭션 ID wraparound — 데이터 손상 위험!
4.2 Autovacuum 모니터링
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
ROUND(n_dead_tup::numeric / NULLIF(n_live_tup, 0), 2) AS dead_ratio,
last_autovacuum,
autovacuum_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
dead_ratio가 0.2 이상이면 autovacuum이 따라가지 못하는 것입니다.
4.3 Autovacuum 튜닝
autovacuum = on
autovacuum_max_workers = 5 # 기본 3 → 5
autovacuum_naptime = 30s # 기본 1분 → 30초
autovacuum_vacuum_scale_factor = 0.05 # 기본 0.2 → 0.05 (5% dead tuple에서 트리거)
autovacuum_analyze_scale_factor = 0.02
autovacuum_vacuum_cost_limit = 2000 # 더 빨리 작업
테이블별 설정 (큰 테이블일수록 더 자주):
ALTER TABLE big_table SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_analyze_scale_factor = 0.005
);
4.4 수동 VACUUM이 필요한 경우
- 장애 복구 후:
VACUUM ANALYZE로 통계 재구성 - 대량 DELETE 후: dead tuple이 폭증
- 인덱스 bloat:
REINDEX CONCURRENTLY
-- 락 없이 인덱스 재구성 (PG 12+)
REINDEX INDEX CONCURRENTLY idx_users_email;
-- 락 없이 테이블 재구성 (pg_repack 확장 필요)
SELECT pg_repack.repack_table('public.users');
5. 모니터링과 디버깅
5.1 실시간 활동 확인
-- 현재 실행 중인 쿼리
SELECT pid, usename, state, query_start, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;
-- 락 대기
SELECT
blocked.pid AS blocked_pid,
blocking.pid AS blocking_pid,
blocked.query AS blocked_query,
blocking.query AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_locks AS blocked_lock ON blocked.pid = blocked_lock.pid
JOIN pg_locks AS blocking_lock
ON blocked_lock.locktype = blocking_lock.locktype
AND blocked_lock.relation = blocking_lock.relation
JOIN pg_stat_activity AS blocking ON blocking.pid = blocking_lock.pid
WHERE NOT blocked_lock.granted AND blocking_lock.granted;
5.2 EXPLAIN ANALYZE 읽기
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE customer_id = 12345;
주요 지표:
Seq ScanvsIndex Scan— 인덱스 사용 여부actual rowsvsestimated rows— 통계 정확도Buffers: shared hit/read— 캐시 히트율Execution Time— 실제 시간
5.3 pgBadger — 로그 분석 도구
# 슬로우 쿼리 로깅 활성화
log_min_duration_statement = 1000 # 1초 이상 쿼리 로깅
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
# pgBadger로 분석
pgbadger -j 4 /var/log/postgresql/postgresql-*.log -o report.html
pgBadger는 PostgreSQL 로그를 파싱하여 HTML 보고서를 생성합니다 — 슬로우 쿼리 TOP, 시간대별 활동, 에러 분석 등.
6. 고가용성 (HA) — Patroni
6.1 Patroni 아키텍처
┌──────────┐ ┌──────────┐ ┌──────────┐
│ etcd-1 │ │ etcd-2 │ │ etcd-3 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
└──────────────────┴──────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Patroni │ │ Patroni │ │ Patroni │
│ + PG │ │ + PG │ │ + PG │
│ Primary │ │ Replica │ │ Replica │
└─────────┘ └─────────┘ └─────────┘
작동 원리:
- 각 PostgreSQL 노드에 Patroni 데몬 실행
- Patroni가 etcd에 leader lock 갱신
- Primary가 다운되면 lock 만료 → Patroni가 새 Primary 선출
- HAProxy/PgBouncer가 새 Primary로 자동 라우팅
6.2 Patroni 설정 예시
# patroni.yml
scope: postgres-cluster
namespace: /service/
name: postgres-1
restapi:
listen: 0.0.0.0:8008
connect_address: 192.168.1.10:8008
etcd3:
hosts: 192.168.1.20:2379,192.168.1.21:2379,192.168.1.22:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
parameters:
max_connections: 100
shared_buffers: 4GB
wal_level: replica
hot_standby: 'on'
max_wal_senders: 10
postgresql:
listen: 0.0.0.0:5432
connect_address: 192.168.1.10:5432
data_dir: /var/lib/postgresql/16/main
authentication:
replication:
username: replicator
password: secret
6.3 Patroni 대안
| 도구 | 특징 |
|---|---|
| Patroni | 가장 인기, etcd 필요 |
| repmgr | EnterpriseDB, 단순함 |
| CloudNativePG | K8s 네이티브 (CNCF) |
| Stolon | 컨테이너 친화적 |
| pg_auto_failover | Microsoft 개발, 단순 |
7. 백업과 복구
7.1 pg_dump vs pgBackRest
| pg_dump | pgBackRest | |
|---|---|---|
| 속도 | 느림 (단일 스레드) | 빠름 (병렬) |
| 증분 | ❌ | ✅ |
| 압축 | gzip | lz4/zstd |
| PITR | ❌ | ✅ |
| S3 통합 | ❌ | ✅ |
| 권장 용도 | 작은 DB | 프로덕션 |
7.2 pgBackRest 설정
# /etc/pgbackrest.conf
[global]
repo1-path=/var/lib/pgbackrest
repo1-retention-full=2
repo1-retention-diff=4
repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=YOUR_PASSWORD
repo1-s3-bucket=my-pg-backups
repo1-s3-region=us-east-1
process-max=4
log-level-console=info
start-fast=y
[main]
pg1-path=/var/lib/postgresql/16/main
pg1-port=5432
# 전체 백업
pgbackrest --stanza=main backup --type=full
# 증분 백업
pgbackrest --stanza=main backup --type=incr
# PITR 복구 (특정 시점으로)
pgbackrest --stanza=main \
--type=time \
--target="2025-04-15 14:30:00" \
restore
7.3 WAL 아카이빙
# postgresql.conf
archive_mode = on
archive_command = 'pgbackrest --stanza=main archive-push %p'
wal_level = replica
max_wal_senders = 10
WAL 아카이빙이 활성화되면, **임의 시점 복구(PITR)**가 가능합니다. 이는 "어제 오후 3시 25분 30초로 되돌려줘" 같은 요구에 응답할 수 있게 합니다.
8. 흔한 함정과 해결책
8.1 인덱스 bloat
-- 인덱스 크기 확인
SELECT
schemaname,
relname,
indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 10;
-- 사용되지 않는 인덱스
SELECT
schemaname || '.' || relname AS table,
indexrelname AS unused_index,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
8.2 N+1 쿼리
ORM이 가장 잘 일으키는 문제. pg_stat_statements로 동일한 쿼리가 수천 번 호출되는 것을 발견하면 N+1입니다. 해결:
- Prisma:
include로 eager loading - TypeORM:
relations옵션 - Django:
select_related/prefetch_related - SQLAlchemy:
joinedload
8.3 트랜잭션 ID wraparound
-- 위험도 확인
SELECT datname, age(datfrozenxid)
FROM pg_database
ORDER BY age(datfrozenxid) DESC;
age가 2억(200,000,000) 가까워지면 위험. autovacuum이 잘 동작하면 자동 처리되지만, autovacuum이 막혀있으면 수동 VACUUM FREEZE 필요.
8.4 connection 폭증
PostgreSQL은 각 연결이 별도 프로세스입니다. 1,000 연결 = 1,000 프로세스 = 메모리 폭증. 해결:
- PgBouncer: connection pooler (transaction mode 권장)
- Pgpool-II: 더 무거운 풀러 + 로드 밸런싱
- app side: connection pool (HikariCP, sequelize pool 등)
9. PostgreSQL 17 (2024-09) 새 기능
- 점진적 정렬 (
incremental sort) 개선 - Logical replication: failover 슬롯 동기화
- pg_basebackup: 점진적 백업 지원
- VACUUM: 메모리 기반 dead tuple 추적 (속도 향상)
- MERGE 명령: RETURNING 절 추가
퀴즈
1. shared_buffers를 RAM의 50%로 설정하면 안 되는 이유는?
답: PostgreSQL은 자체 shared_buffers 외에도 OS 파일 시스템 캐시를 활용합니다. effective_cache_size로 OS 캐시를 옵티마이저에 알려줘야 합니다. shared_buffers를 너무 크게 잡으면 (1) OS 캐시 공간이 줄어 이중 캐싱 비효율, (2) 페이지 교체 알고리즘이 비효율적, (3) 백업/체크포인트가 느려집니다. RAM의 25%가 일반적으로 최적입니다.
2. autovacuum이 따라가지 못하는 징후는?
답: pg_stat_user_tables에서 n_dead_tup이 n_live_tup의 20% 이상이면 따라가지 못하는 것입니다. 해결: (1) autovacuum_max_workers 증가, (2) autovacuum_vacuum_scale_factor 감소 (0.2 → 0.05), (3) autovacuum_naptime 감소, (4) 큰 테이블은 ALTER TABLE로 개별 설정.
3. PgBouncer가 필요한 이유는?
답: PostgreSQL의 각 연결은 별도 프로세스입니다 (~10MB). 1,000 연결 = 10GB+ 메모리. 또한 프로세스 생성/소멸 비용이 큽니다. PgBouncer는 적은 수의 실제 연결을 풀에 유지하고 클라이언트 연결을 멀티플렉싱합니다. transaction pooling 모드가 가장 효율적이지만 prepared statements와 호환 문제가 있습니다.
4. WAL 아카이빙이 PITR에 어떻게 사용되나요?
답: WAL(Write-Ahead Log)은 모든 데이터 변경을 순차적으로 기록합니다. WAL 아카이빙은 이 로그를 외부 저장소(S3 등)에 복사합니다. 복구 시: (1) 가장 가까운 base backup 복원, (2) 원하는 시점까지 WAL을 재생(replay). 결과: 임의 시점으로 복구 가능. pgBackRest의 --type=time이 이를 자동화합니다.
5. pg_repack과 VACUUM FULL의 차이는?
답: VACUUM FULL은 테이블 락이 걸려 운영 중인 서비스에서는 사용 불가. pg_repack은 락 없이 테이블을 재구성합니다 — 새 테이블에 데이터를 복사하고, 이름만 swap합니다. 대용량 테이블의 bloat 제거에 필수. 단, 트리거와 같은 동시 변경은 신중히 처리해야 합니다.
참고 자료
- PostgreSQL Documentation — 공식 문서
- Use The Index, Luke! — 인덱스 가이드
- Postgres Weekly — 주간 뉴스레터
- Patroni — HA 솔루션
- pgBackRest — 엔터프라이즈 백업
- pgBadger — 로그 분석
- pgvector — 벡터 검색
- TimescaleDB — 시계열
- PostGIS — 지리정보
- pgtune — 자동 튜닝 도구
- PoWA — 성능 분석 워크로드
현재 단락 (1/312)
- **확장이 PostgreSQL의 진정한 힘**: pg_stat_statements(쿼리 분석), pgvector(AI), PostGIS(지리), TimescaleDB(시계열),...