일요일

PostgreSQL VACUUM: 자동 실행과 수동 튜닝으로 성능 회복

🔍 검색 키워드: 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_ratio10% 이상이면 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로 데이터베이스를 "청소"하면, 쿼리 성능과 디스크 효율을 동시에 개선할 수 있습니다.

목요일

MySQL slow query 완벽 진단 및 최적화 가이드

🔍 검색 키워드: MySQL slow query, 느린 쿼리 로그, MySQL 성능 진단, 쿼리 최적화, EXPLAIN, slow_query_log, MySQL 병목

MySQL 서버를 운영하다 보면 "어떤 쿼리가 느린지 알 수 없다"는 답답한 상황을 마주치게 됩니다. 특히 야간에 갑자기 데이터베이스 응답이 느려지면, 어디부터 손을 대야 할지 막막하죠. 이 글에서는 MySQL slow query log를 활용해 느린 쿼리를 찾아내고 체계적으로 최적화하는 방법을 알려드립니다.

증상: 느린 쿼리는 어떻게 나타나나?

MySQL이 느려질 때 보이는 전형적인 증상들:

- 애플리케이션이 데이터베이스 요청에서 멈춰 있음
- 커넥션 풀이 고갈되었다는 에러 메시지
- DB CPU 사용률이 100%에 가까움
- 특정 시간대(주로 야간 배치 시간)에 연쇄적으로 느려짐

원인 분석: slow query log 활성화 및 확인

1단계: slow query log 활성화

MySQL 설정 파일(my.cnf 또는 my.ini)에 다음을 추가합니다:

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-query.log
long_query_time = 2
log_queries_not_using_indexes = 1

MySQL 재시작:

sudo systemctl restart mysql

2단계: slow query log 확인

cat /var/log/mysql/slow-query.log

또는 MySQL 클라이언트에서:

SET SESSION long_query_time = 0;
SELECT sleep(3);
SELECT * FROM large_table WHERE id > 1000000;

로그 파일에 다음과 같이 기록됩니다:

# Time: 2026-09-09T08:15:23.456789Z
# User@Host: user@[localhost]
# Query_time: 3.234567  Lock_time: 0.000123  Rows_sent: 5000  Rows_examined: 1000000
SELECT * FROM large_table WHERE id > 1000000;

중요한 메트릭 해석:

- Query_time: 쿼리 실행에 걸린 전체 시간
- Rows_examined: 서버가 검사한 행의 수 (클수록 비효율적)
- Rows_sent: 클라이언트로 반환된 행의 수
- 비율이 중요: Rows_examined / Rows_sent 비율이 크면 불필요한 행을 많이 검사 중

해결방법: EXPLAIN으로 원인 찾기 및 최적화

1단계: EXPLAIN으로 쿼리 실행 계획 분석

느린 쿼리 앞에 EXPLAIN을 붙여 실행 계획을 확인합니다:

EXPLAIN SELECT * FROM users WHERE created_at > '2026-01-01' AND status = 'active';

결과에서 확인할 항목:

- type: 조인 타입. ALL은 풀 테이블 스캔(매우 느림), ref나 eq_ref는 양호
- possible_keys: 사용할 수 있는 인덱스 목록
- key: 실제로 사용한 인덱스. NULL이면 인덱스를 사용하지 않음
- rows: 서버가 검사할 것으로 예상하는 행 수. 작을수록 좋음
- Extra: 추가 정보. Using filesort, Using temporary는 성능 악화 신호

2단계: 인덱스 추가 또는 최적화

느린 쿼리에서 자주 필터링되는 컬럼에 인덱스를 추가:

ALTER TABLE users ADD INDEX idx_status_created (status, created_at);

복합 인덱스(composite index) 작성 시 주의:

- WHERE 절에서 사용되는 순서대로 컬럼 배열
- 카디널리티가 높은 컬럼(고유값이 많은)을 먼저 배치

3단계: 쿼리 리팩토링

- 불필요한 컬럼 제거: SELECT * 대신 필요한 컬럼만 명시
- 조인 최적화: 큰 테이블 조인 전에 필터링
- 서브쿼리 제거: 불가피한 경우 JOIN으로 변경

예시:

# 느린 쿼리 (서브쿼리 + 풀 조인)
SELECT * FROM orders 
WHERE user_id IN (SELECT id FROM users WHERE status = 'active');

# 최적화된 쿼리 (JOIN + 인덱스)
SELECT o.* FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';

실무 체크리스트

단계확인 항목개선 방법
진단slow query log 활성화?MySQL 설정 파일에 slow_query_log = 1 추가
분석Rows_examined / Rows_sent 비율?비율이 100 이상이면 인덱스 확인 필요
분석EXPLAIN type = ALL?풀 테이블 스캔 중. 인덱스 추가 필수
최적화WHERE 절 컬럼에 인덱스?복합 인덱스로 효율성 증대
최적화불필요한 컬럼 선택?SELECT *를 구체적인 컬럼으로 변경
모니터링주기적으로 느린 쿼리 검토?주 1회 slow query log 분석 스케줄 추천

느린 쿼리는 서비스 전체의 병목입니다. 정기적으로 slow query log를 점검하고 체계적으로 최적화하면, 사용자 경험은 물론 서버 리소스 효율도 크게 개선됩니다.

일요일

Next.js hydration 불일치 에러 완벽 해결 가이드

Next.js SSR 사용 시 hydration 불일치 에러가 자주 발생합니다.

증상: Hydration failed because the initial UI does not match what was rendered on the server.

원인:
1. 서버와 클라이언트 렌더링 로직 차이
2. Date.now()나 Math.random() 같은 비결정적 값 사용
3. window, localStorage 등 브라우저 API를 SSR 단계에서 접근

해결방법:
1. useEffect 내에서만 클라이언트 전용 콘텐츠 렌더링 - isClient 상태로 분기 처리
2. suppressHydrationWarning 속성 사용 - 작은 불일치는 무시 (남용 금지)
3. force-dynamic으로 동적 렌더링 - 정적 생성 대신 매번 렌더링
4. localStorage 접근은 반드시 useEffect 안에서 처리

정리: 원인을 파악하면 대부분 useEffect 패턴으로 해결 가능합니다.