개발 트러블슈팅 완전 정복 | 웹개발·백엔드·데브옵스 실무 에러 해결법. Docker, GitHub Actions, TypeScript, MySQL, Redis 등 실전 경험 기반의 개발 블로그
목요일
MySQL slow query 완벽 진단 및 최적화 가이드
🔍 검색 키워드: PostgreSQL VACUUM, 데드 튜플 정리, PostgreSQL 성능 저하, autovacuum, PostgreSQL 디스크 사용량, VACUUM ANALYZE
PostgreSQL은 MVCC(Multi-Version Concurrency Control) 방식으로 동작하기 때문에, 데이터를 삭제해도 물리적으로 즉시 제거되지 않습니다. 시간이 지나면서 불필요한 "죽은" 데이터(데드 튜플)가 누적되어 디스크 용량을 낭비하고 쿼리 성능을 저하시킵니다. 이 글에서는 VACUUM의 동작 원리와 최적화 방법을 설명합니다.
증상: PostgreSQL의 숨은 성능 저하
PostgreSQL 서버를 운영하다 보면 나타나는 문제들:
# 문제 1: 디스크 사용량은 계속 증가하지만 DELETE했는데도?
SELECT pg_size_pretty(pg_total_relation_size('users'));
-- 5GB
# 문제 2: 같은 쿼리인데 시간이 점점 느려짐
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'completed';
-- Seq Scan on orders (cost=0.00..50000.00 rows=100000)
# 문제 3: 특정 테이블에서만 SELECT 느림
SELECT n_live_tup, n_dead_tup FROM pg_stat_user_tables
WHERE relname = 'orders';
-- 결과: n_live_tup=100000, n_dead_tup=500000 (데드 튜플이 많음!)
원인 분석: MVCC와 데드 튜플
PostgreSQL의 MVCC 동작 방식:
1. 트랜잭션 A가 행을 삭제하면, 실제로는 그 행을 "삭제됨" 표시
2. 트랜잭션 B가 여전히 이전 버전을 참조 중이면, 그 행은 메모리에 남아야 함
3. 모든 트랜잭션이 새 버전만 참조하게 되면, 그때 "데드 튜플" 처리 가능
데드 튜플이 문제인 이유:
- 디스크 용량 낭비: 삭제한 데이터도 차지
- 인덱스 비대화: 데드 튜플도 인덱스 스캔에 포함
- 쿼리 성능 저하: Seq Scan 비용 증가
원인 추적: 데드 튜플 상태 확인
데드 튜플 누적 상태를 확인합니다:
SELECT schemaname, relname, n_live_tup, n_dead_tup,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;
결과 해석:
- n_live_tup: 활성 행 수
- n_dead_tup: 데드 튜플 수
- dead_ratio: 데드 튜플 비율. 10% 이상이면 VACUUM 필요
해결방법 1: 자동 VACUUM (autovacuum) 설정 최적화
PostgreSQL은 기본적으로 autovacuum 데몬이 자동으로 VACUUM을 실행합니다. 하지만 기본 설정은 보수적이므로, 대용량 데이터베이스에서는 수동 튜닝이 필요합니다.
postgresql.conf 설정 확인:
SHOW autovacuum;
SHOW autovacuum_naptime;
SHOW autovacuum_vacuum_threshold;
권장 설정 (운영 환경):
postgresql.conf에 추가:
autovacuum = on
autovacuum_naptime = '10s' # 10초마다 체크 (기본 1분)
autovacuum_vacuum_threshold = 50 # 50개 행 변경 시 VACUUM (기본 50)
autovacuum_analyze_threshold = 50 # 50개 행 변경 시 ANALYZE (기본 50)
# 테이블별 추가 튜닝
autovacuum_vacuum_scale_factor = 0.05 # 테이블의 5% 변경 시
autovacuum_vacuum_cost_limit = 1000 # I/O 부하 제한 (기본 -1)
재시작:
sudo systemctl restart postgresql
해결방법 2: 수동 VACUUM 실행
긴급 상황에서는 수동으로 VACUUM을 실행합니다:
# 기본 VACUUM (먹다 남은 공간 재사용)
VACUUM;
# ANALYZE 포함 (통계 업데이트)
VACUUM ANALYZE;
# 특정 테이블만 처리
VACUUM ANALYZE orders;
# FULL (여유 공간을 OS로 반환, 시간 오래 걸림)
VACUUM FULL;
주의: VACUUM FULL은 테이블을 잠금(LOCK)으로 처리하므로, 운영 중에는 피해야 합니다.
해결방법 3: 추가 최적화
1. REINDEX로 인덱스 재구성
데드 튜플로 비대화된 인덱스를 재구성:
REINDEX TABLE orders;
2. pg_repack 확장 모듈 (다운타임 없음)
CREATE EXTENSION pg_repack;
SELECT pg_repack.repack_table('orders');
pg_repack은 FULL VACUUM과 달리 테이블 잠금 없이 재구성합니다. 운영 중 사용 가능.
3. 정기적인 VACUUM ANALYZE 스케줄
크론 작업으로 자동 실행:
# 매일 새벽 2시에 VACUUM ANALYZE 실행
0 2 * * * psql -d mydb -c "VACUUM ANALYZE;"
성능 회복 비교
경미한 데드 튜플: VACUUM ANALYZE → 통계 업데이트, 쿼리 최적화 → ~초 단위
중간 정도 비대화: REINDEX TABLE → 인덱스 재구성 → ~분 단위
심각한 비대화: pg_repack (비운영) 또는 VACUUM FULL → 공간 회복 → ~분~시간 단위
모니터링 체크리스트
데드 튜플 비율: pg_stat_user_tables에서 dead_ratio → 10% 이상이면 VACUUM
자동 VACUUM 실행 여부: pg_stat_user_tables의 last_vacuum → 안 실행되면 autovacuum_naptime 단축
인덱스 비대화: pg_indexes_size(table) → 인덱스가 테이블보다 크면 REINDEX
디스크 사용량: pg_size_pretty(pg_database_size(...)) → 지속 증가면 VACUUM FULL 또는 pg_repack
PostgreSQL의 성능 저하는 대부분 데드 튜플 누적으로 인한 것입니다. 정기적인 VACUUM과 ANALYZE로 데이터베이스를 "청소"하면, 쿼리 성능과 디스크 효율을 동시에 개선할 수 있습니다.
피드 구독하기:
글 (Atom)