PostgreSQL은 기본 설정이 매우 보수적이다. 서버 사양에 맞게 튜닝하지 않으면 하드웨어의 10%도 활용하지 못한다. 실무에서 효과가 큰 순서대로 정리했다.
1. 메모리 설정
가장 효과가 큰 튜닝 영역이다. 대부분의 성능 문제는 여기서 해결된다.
shared_buffers
PostgreSQL이 데이터를 캐시하는 공유 메모리 영역이다. 기본값은 128MB로 너무 작다.
# 권장: 전체 RAM의 25%
# 16GB RAM 서버 기준
shared_buffers = 4GB
effective_cache_size
OS 파일 캐시를 포함하여 PostgreSQL이 사용할 수 있는 총 캐시 크기의 추정치다. 쿼리 플래너가 인덱스 스캔 여부를 결정할 때 참고한다. 실제 메모리를 할당하지는 않는다.
# 권장: 전체 RAM의 50~75%
effective_cache_size = 12GB
work_mem
정렬, 해시 조인 등 쿼리 내부 연산에 사용하는 메모리다. 연결 x 쿼리 x 연산 수만큼 곱해지므로 너무 크게 잡으면 OOM이 발생한다.
# 공식: RAM / max_connections / 4 (대략적 기준)
# 16GB RAM, 200 connections 기준
work_mem = 20MB
maintenance_work_mem
VACUUM, CREATE INDEX, ALTER TABLE 등 유지보수 작업에 사용하는 메모리다.
# 권장: 512MB ~ 2GB
maintenance_work_mem = 1GB
16GB RAM 서버 기준 요약:
| 파라미터 | 기본값 | 권장값 |
|---|---|---|
shared_buffers |
128MB | 4GB |
effective_cache_size |
4GB | 12GB |
work_mem |
4MB | 20MB |
maintenance_work_mem |
64MB | 1GB |
2. WAL (Write-Ahead Log) 설정
쓰기 성능에 직접적인 영향을 주는 설정이다.
wal_buffers
# shared_buffers의 1/32, 최소 64MB
wal_buffers = 128MB
checkpoint 관련
체크포인트가 너무 자주 발생하면 I/O 스파이크가 생긴다.
# 체크포인트 간격 늘리기 (기본 5min)
checkpoint_timeout = 15min
# WAL 최대 크기 (기본 1GB)
max_wal_size = 4GB
min_wal_size = 1GB
# 체크포인트 I/O를 분산 (기본 0.9)
checkpoint_completion_target = 0.9
synchronous_commit
트랜잭션 커밋 시 WAL이 디스크에 기록될 때까지 기다릴지 결정한다.
# 쓰기 성능 향상 (약간의 데이터 손실 가능성 감수)
synchronous_commit = off
주의: off로 설정하면 크래시 시 최근 수백 ms의 트랜잭션이 유실될 수 있다. 로그성 데이터에는 괜찮지만, 결제 등 중요 데이터에는 on을 유지해야 한다.
3. 커넥션 관리
max_connections
PostgreSQL은 커넥션당 프로세스를 fork한다. 커넥션이 많을수록 컨텍스트 스위칭 비용이 증가한다.
# 직접 연결: 실제 필요한 수 + 여유분
max_connections = 200
Connection Pooler 사용 (권장)
애플리케이션이 수백~수천 커넥션을 사용한다면 PostgreSQL에 직접 연결하지 말고 PgBouncer를 사용한다.
# pgbouncer.ini
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp
[pgbouncer]
listen_port = 6432
pool_mode = transaction # 트랜잭션 단위로 커넥션 재사용
max_client_conn = 1000 # 클라이언트 최대 연결
default_pool_size = 50 # DB 실제 연결 수
이렇게 하면 PostgreSQL의 max_connections는 100 이하로 낮추고, 애플리케이션은 PgBouncer에 1000개까지 연결할 수 있다.
4. VACUUM과 Autovacuum
PostgreSQL은 MVCC 구조상 UPDATE/DELETE 후 죽은 튜플(dead tuple)이 쌓인다. VACUUM이 이를 정리한다. Autovacuum이 꺼져 있거나 너무 느리면 테이블 bloat과 성능 저하가 발생한다.
# autovacuum 기본 활성화 (절대 끄지 말 것)
autovacuum = on
# 워커 수 (대규모 DB에서 증가)
autovacuum_max_workers = 5
# 더 자주, 더 빠르게 실행
autovacuum_vacuum_cost_delay = 2ms # 기본 2ms (이전 버전은 20ms)
autovacuum_vacuum_cost_limit = 1000 # 기본 -1 (vacuum_cost_limit 사용)
# 트리거 임계값 조정
autovacuum_vacuum_threshold = 50 # 기본 50
autovacuum_vacuum_scale_factor = 0.05 # 기본 0.2 (5%로 낮추면 더 자주 실행)
큰 테이블에 대한 개별 설정:
-- 1억 행 테이블은 scale_factor 0.01 (1%)로 설정
ALTER TABLE huge_table SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_analyze_scale_factor = 0.005
);
dead tuple 확인:
SELECT relname, n_dead_tup, n_live_tup,
round(n_dead_tup::numeric / NULLIF(n_live_tup, 0) * 100, 2) AS dead_pct,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;
5. 쿼리 분석 (EXPLAIN ANALYZE)
튜닝의 핵심은 느린 쿼리를 찾아서 고치는 것이다.
느린 쿼리 로깅 활성화:
# 200ms 이상 걸리는 쿼리 로깅
log_min_duration_statement = 200ms
EXPLAIN ANALYZE 사용법:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders
WHERE user_id = 12345
AND created_at > '2025-01-01';
실행 계획에서 주의할 키워드:
| 키워드 | 의미 | 대응 |
|---|---|---|
Seq Scan |
전체 테이블 스캔 | 인덱스 추가 검토 |
Sort (disk) |
디스크 정렬 발생 | work_mem 증가 또는 인덱스 |
Hash Join (batches > 1) |
해시가 메모리 초과 | work_mem 증가 |
Nested Loop (rows 큼) |
비효율적 조인 | 조인 조건/인덱스 확인 |
6. 인덱스 전략
기본 B-tree 인덱스:
-- 단일 컬럼
CREATE INDEX idx_orders_user_id ON orders (user_id);
-- 복합 인덱스 (선택도 높은 컬럼을 앞에)
CREATE INDEX idx_orders_user_created ON orders (user_id, created_at DESC);
Partial Index (조건부 인덱스):
특정 조건의 행만 인덱싱하여 크기를 줄이고 성능을 높인다.
-- 활성 주문만 인덱싱
CREATE INDEX idx_orders_active ON orders (created_at)
WHERE status = 'active';
-- NULL이 아닌 행만
CREATE INDEX idx_users_email ON users (email)
WHERE email IS NOT NULL;
Covering Index (INCLUDE):
인덱스만으로 쿼리를 완료하여 테이블 접근을 제거한다 (Index-Only Scan).
-- user_id로 검색하고 name, email을 반환하는 쿼리에 최적
CREATE INDEX idx_users_covering ON users (user_id) INCLUDE (name, email);
사용하지 않는 인덱스 찾기:
SELECT indexrelname, idx_scan, 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;
사용되지 않는 인덱스는 쓰기 성능만 떨어뜨린다. 과감히 삭제한다.
7. 디스크 I/O 설정
# SSD 사용 시 (random I/O가 빠르므로 낮게)
random_page_cost = 1.1 # 기본 4.0 (HDD 기준)
seq_page_cost = 1.0
# 동시 I/O 워커
effective_io_concurrency = 200 # SSD: 200, HDD: 2
random_page_cost를 낮추면 플래너가 인덱스 스캔을 더 적극적으로 선택한다. SSD를 사용하는데 기본값(4.0)을 두면 불필요한 Seq Scan이 발생할 수 있다.
8. pg_stat_statements로 모니터링
실제 운영에서 느린 쿼리를 찾는 가장 효과적인 방법이다.
# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
# DB에서 확장 활성화
CREATE EXTENSION pg_stat_statements;
총 실행 시간 기준 상위 쿼리:
SELECT query,
calls,
round(total_exec_time::numeric, 2) AS total_ms,
round(mean_exec_time::numeric, 2) AS avg_ms,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
I/O가 많은 쿼리:
SELECT query,
shared_blks_read + shared_blks_hit AS total_blks,
round(100.0 * shared_blks_hit / NULLIF(shared_blks_read + shared_blks_hit, 0), 2) AS hit_pct
FROM pg_stat_statements
ORDER BY shared_blks_read DESC
LIMIT 10;
hit_pct가 낮은 쿼리는 캐시 미스가 잦다는 뜻이므로 인덱스나 쿼리 최적화가 필요하다.
9. 전체 설정 요약 (postgresql.conf)
16GB RAM, SSD, 200 커넥션 기준 템플릿이다.
# Memory
shared_buffers = 4GB
effective_cache_size = 12GB
work_mem = 20MB
maintenance_work_mem = 1GB
# WAL
wal_buffers = 128MB
checkpoint_timeout = 15min
max_wal_size = 4GB
min_wal_size = 1GB
checkpoint_completion_target = 0.9
# Disk I/O (SSD)
random_page_cost = 1.1
effective_io_concurrency = 200
# Connections
max_connections = 200
# Autovacuum
autovacuum_max_workers = 5
autovacuum_vacuum_scale_factor = 0.05
# Logging
log_min_duration_statement = 200ms
shared_preload_libraries = 'pg_stat_statements'
정리: 튜닝 우선순위
| 순위 | 영역 | 효과 |
|---|---|---|
| 1 | 쿼리 최적화 + 인덱스 | 10x ~ 1000x |
| 2 | 메모리 설정 | 2x ~ 10x |
| 3 | 커넥션 풀링 | 2x ~ 5x |
| 4 | WAL / Checkpoint | 1.5x ~ 3x (쓰기) |
| 5 | Autovacuum 튜닝 | bloat 방지 (장기적) |
| 6 | 디스크 I/O 설정 | 1.2x ~ 2x |
설정을 변경하기 전에 pg_stat_statements로 현재 병목을 먼저 파악하는 것이 가장 중요하다. 병목을 모르고 설정만 바꾸는 것은 효과가 제한적이다.
'Database > PostgreSQL' 카테고리의 다른 글
| PostgreSQL Replication 서버 세팅 가이드 (0) | 2026.02.24 |
|---|---|
| PostgreSQL 계정 및 권한 관리 가이드 (0) | 2026.02.24 |
| PostgreSQL 데이터 백업 및 복구 (0) | 2024.10.08 |
| PostgreSQL 계정 관리 방법 (0) | 2024.10.06 |