AWS RDS 슬로우 쿼리 튜닝 — Performance Insights·슬로우 쿼리 로그 비교

목차

AWS RDS 슬로우 쿼리 튜닝을 어디서부터 시작할지 정할 때 후보 셋의 비용 차이가 6배 가까이 났다. Performance Insights(PI)는 7일 보존까지 무료지만 장기 보존을 켜면 db.r6g.large 기준 월 약 $7.3가 인스턴스마다 붙는다. 슬로우 쿼리 로그를 CloudWatch Logs로 보내면 수집 $0.50/GB·보관 $0.03/GB·월에 Logs Insights 스캔 $0.005/GB가 더 붙는다. 셋 다 켜놓으면 인스턴스당 월 $20을 넘기는 게 어렵지 않다.

가격표만 보면 PI 무료 옵션으로 끝낼 수 있을 것 같다. 다만 실제로는 PI가 쿼리 원문을 잘라서 보여주는 한계가 있어서, 깊이 들어가려면 슬로우 쿼리 로그가 필요한 순간이 온다. 셋을 다 켜자니 비용이 부담이라 어떤 조합이 우리 환경에 맞는지 비교했다. 결과를 항목별로 적는다. (us-east-1, 2026년 6월 기준 가격. 출처: AWS RDS Performance Insights 요금)

후보 셋을 책상에 올려놓은 배경

운영 중인 RDS는 MySQL 8.0.36 인스턴스 8대, PostgreSQL 15.5 인스턴스 4대로 섞여 있다. 특정 시간대마다 CPU가 80%를 찍는 인스턴스 두 대가 있어서 어떤 쿼리가 시간을 먹는지 정량적으로 잡아야 했다. PI 대시보드를 잠깐 켜봤더니 상위 5개 쿼리가 DB 시간의 약 70%를 점유했다. 거기까지는 PI로 봤는데, 그다음이 문제였다.

그래서, PI의 Top SQL에 보이는 쿼리는 WHERE id IN (?)처럼 IN 절 길이가 잘린다. 실제 IN 절에 ID가 몇 개 들어왔는지, 어떤 파라미터로 호출됐는지는 PI만으로는 안 보였다. 그래서 후보 도구를 셋으로 좁혔다.

  • Performance Insights (AWS 기본 통합)
  • 슬로우 쿼리 로그 파일 + pt-query-digest (Percona Toolkit)
  • CloudWatch Logs Insights

비교 기준 — 가중치를 먼저 정했다

비교 항목을 정하지 않고 도구별 장점만 나열하면 결국 "다 좋다"로 끝난다. 다섯 가지 기준에 가중치를 먼저 매겼다.

기준 가중치 이유
비용 25% 12개 인스턴스에 일괄 적용 시 누적
쿼리 원문 정확도 25% PI는 정규화·잘림 발생
진단 속도 20% 장애 중 5분 안에 답이 필요
운영 부담 20% 로그 회전, 권한, 보관 정책
다중 엔진 지원 10% MySQL·PostgreSQL 표준화

결국, 가중치는 우리 팀 상황에 맞춘 값이다. 인스턴스 한두 대 운영이면 비용 비중을 낮춰도 된다. 반대로 컴플라이언스 때문에 쿼리 원문 보관이 필수면 정확도 비중이 더 커진다.

Performance Insights — 콘솔에서 5분이면 끝

활성화

CLI 한 줄이면 켜진다. 콘솔에서 클릭으로 켜도 동일하다.

# MySQL·PostgreSQL 공통 — PI 활성화
aws rds modify-db-instance \
  --db-instance-identifier prod-mysql-01 \
  --enable-performance-insights \
  --performance-insights-retention-period 7 \
  --apply-immediately

--performance-insights-retention-period는 7 또는 7·31·62·····731 중 하나를 받는다. 7일은 무료, 그 이상은 vCPU 기준으로 과금된다. 보존 기간을 늘려야 할 정도로 장기 트렌드 분석이 필요한지부터 따져봤다(우리는 결국 7일로 충분했다).

무엇을 볼 수 있나

반면, DB Load(평균 활성 세션, AAS) 그래프 위에 Top SQL·Top Waits·Top Hosts가 깔린다. AAS가 vCPU 수를 넘으면 그 시각에 세션이 대기 중이라는 신호다. 우리 인스턴스(2 vCPU)에서 AAS가 5~8을 찍는 구간이 있었고, Top Waits 90%가 io/aurora_redo_log_flush 비슷한 IO 대기였다. PI 화면 보고 1분 안에 IO 병목임을 확신했다.

정확도의 한계

PI는 쿼리 텍스트를 정규화해서 보여준다. WHERE id = 12345WHERE id = ?로 통합되고, IN 절은 길이가 잘린다. RDS for MySQL은 PI에서 SQL 원문을 보려면 performance_schema = 1을 켜야 하고, 이건 인스턴스 재시작이 필요하다. PostgreSQL은 pg_stat_statements가 기본으로 켜져 있어서 좀 더 편하다.

그런데, PI에서 후보 쿼리를 식별한 다음, 실제 파라미터를 봐야 하는 순간이 반드시 온다. 그래서 다음 카드를 봤다.

슬로우 쿼리 로그 — 원문이 필요할 때

MySQL 8.0 활성화

파라미터 그룹에서 세 가지를 켠다.

# 슬로우 쿼리를 파일 모드로 기록
aws rds modify-db-parameter-group \
  --db-parameter-group-name prod-mysql-8-pg \
  --parameters \
    "ParameterName=slow_query_log,ParameterValue=1,ApplyMethod=immediate" \
    "ParameterName=long_query_time,ParameterValue=1,ApplyMethod=immediate" \
    "ParameterName=log_output,ParameterValue=FILE,ApplyMethod=immediate"

long_query_time=1은 1초 이상 걸린 모든 쿼리를 기록한다. 트래픽이 많으면 로그가 빠르게 차므로 0.5~2초 사이에서 조절한다. 우리 환경(피크 1.2k QPS, db.r6g.large)에서 1초 기준으로 시간당 약 80MB가 쌓였다. log_output=FILE은 RDS 내부 파일에 적는 모드로, CloudWatch로 보내려면 별도 export 설정이 필요하다.

PostgreSQL은 파라미터 이름이 다르다

# PG는 log_min_duration_statement (밀리초 단위)
aws rds modify-db-parameter-group \
  --db-parameter-group-name prod-pg-15-pg \
  --parameters \
    "ParameterName=log_min_duration_statement,ParameterValue=1000,ApplyMethod=immediate" \
    "ParameterName=log_statement,ParameterValue=none,ApplyMethod=immediate"

log_statement=all을 켜고 싶은 충동이 들지만, 모든 쿼리가 찍히면 디스크가 순식간에 찬다. none으로 두고 log_min_duration_statement만 쓰는 게 운영에서는 현실적이다. 디버깅용으로 1시간만 all로 켜고 끄는 것도 방법이다.

다운로드와 회전

RDS는 슬로우 로그 파일을 자체적으로 회전시킨다. 기본 보관은 24시간(MySQL)이라 그 안에 받아둬야 한다.

# 슬로우 로그 파일 목록 조회
aws rds describe-db-log-files \
  --db-instance-identifier prod-mysql-01 \
  --filename-contains slowquery

# 파일 다운로드
aws rds download-db-log-file-portion \
  --db-instance-identifier prod-mysql-01 \
  --log-file-name slowquery/mysql-slowquery.log \
  --starting-token 0 \
  --output text > slow.log

여러 시간대를 합치려면 --starting-token을 순회하는 스크립트가 필요하다. 큰 파일은 한 번에 안 받아진다.

pt-query-digest로 로그를 다이제스트

그러나, 슬로우 로그 원본은 사람이 읽기 어렵다. Percona Toolkit의 pt-query-digest로 요약한다.

# 다운받은 로그를 다이제스트
pt-query-digest slow.log > report.txt

# 특정 시간대만 보고 싶다면
pt-query-digest \
  --since '2026-06-05 14:00:00' \
  --until '2026-06-05 15:00:00' \
  slow.log > report-peak.txt

따라서, 리포트 첫 페이지에 Top 쿼리가 누적 시간·평균 시간·호출 횟수 순으로 정렬되어 나온다. 우리 환경에서 1시간치 로그(약 80MB)를 돌리면 분석은 30초 정도였다. (체감상 PI Top SQL을 받아 EXPLAIN 돌리는 시간보다 짧았다.)

물론, 여기서 PI가 잘라서 안 보여주던 IN 절 길이가 그대로 드러난다. ID 5,000개가 IN 절에 들어가는 쿼리를 발견한 적 있었는데, 이건 PI 대시보드만 봐서는 절대 못 잡았다. 자세한 옵션은 Percona pt-query-digest 공식 문서 참고.

CloudWatch Logs Insights — 가끔만 켜는 보조 카드

슬로우 로그를 CloudWatch Logs로 export 하려면 인스턴스 설정에서 한 줄.

aws rds modify-db-instance \
  --db-instance-identifier prod-mysql-01 \
  --cloudwatch-logs-export-configuration \
    '{"EnableLogTypes":["slowquery"]}' \
  --apply-immediately

한편, PostgreSQL은 "postgresql"이 로그 타입이다. Logs Insights에서는 이런 쿼리로 파싱한다.

fields @timestamp, @message
| parse @message /Query_time: (?<qt>\S+).*?# User@Host: (?<usr>\S+).*?\n(?<sql>SELECT.*|UPDATE.*|DELETE.*|INSERT.*)/
| filter qt > 5
| sort qt desc
| limit 50

즉, 쿼리 한 번 돌릴 때 스캔한 GB만큼 과금이 붙는다. 슬로우 로그가 하루 2GB 쌓이는 환경에서 30일치 풀스캔이면 약 60GB × $0.005 = $0.30 한 번에 나간다. 자주 돌리면 PI 보존 비용보다 비싸지는 구간이 생긴다. 그래서 우리는 PI에서 단서 잡고 좁힌 시간대만 Insights로 들어가는 방식으로 쓴다.

항목별 비교표

항목 Performance Insights 슬로우 로그 + pt-query-digest CloudWatch Logs Insights
비용(인스턴스/월) $0(7일) ~ $7.3+ $0(파일 모드) 수집·보관·스캔 누적
쿼리 원문 정규화·잘림 완전 보존 완전 보존
진단 응답 속도 5분 이내 다운로드 + 분석 1~5분 쿼리당 수초~수십초
운영 부담 매우 낮음 다운로드 스크립트 필요 Log Group 보관 정책 필요
MySQL 지원 5.7·8.0 정식 정식
PostgreSQL 지원 10 이상 auto_explain과 함께 정식
장기 트렌드 7~731일 별도 아카이브 보관 정책 자유
학습 곡선 낮음 pt 옵션 학습 필요 Logs Insights 쿼리 학습

그래서, 세 가지를 한 줄로 묶으면 PI는 빠르고 비싸지 않지만 깊이가 부족하고, pt-query-digest는 깊지만 다운로드 절차가 한 단계 들어가고, Logs Insights는 둘의 중간인데 비용 변동성이 가장 크다.

같은 장애 상황에서 셋을 돌려본 결과

테스트 환경: db.r6g.large MySQL 8.0.36, 피크 1.2k QPS, 슬로우 쿼리 시간당 약 400건. 동일한 1시간 구간을 셋 다 분석했을 때 측정값이다.

측정 PI 슬로우 로그 + pt Logs Insights
Top 5 쿼리 식별 약 1분 약 3분(다운+pt 30초) 약 2분
IN 절 실제 길이 확인 불가 가능 가능
30일 트렌드 조회 가능(유료 보존) 별도 아카이브 필요 가능
1회 비용(체감) $0 $0 $0.05 내외

물론, PI는 빠르되 IN 절 길이 확인이 안 됐다. pt-query-digest는 다운로드 단계가 있지만 결과가 가장 자세했다. Logs Insights는 1회 비용이 작아 보여도 분석 패턴(여러 시간대를 반복 조회)에 따라 빠르게 누적된다.

인덱스 최적화로 넘어가는 단계

Top 쿼리 식별이 끝나면 다음은 EXPLAIN이다. MySQL 8.0이면 EXPLAIN ANALYZE가 실제 실행 시간을 측정해준다.

-- 실제 실행 시간까지 측정 (MySQL 8.0+)
EXPLAIN ANALYZE
SELECT u.id, u.email, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at >= '2026-01-01'
GROUP BY u.id;

출력에서 actual time=0.012..1840 같은 단계가 보이면 끝값이 큰 노드가 병목이다. Using filesortUsing temporary가 보이면 인덱스 보강이나 쿼리 재작성을 검토한다.

슬로우 로그에서 자주 만난 패턴 셋

그러나, 1) 복합 인덱스 컬럼 순서 잘못

WHERE status = 'paid' AND created_at >= ? 쿼리에 (created_at, status)로 인덱스가 걸려 있던 케이스. 카디널리티 낮은 컬럼이 앞에 와야 범위 스캔이 효율적이다. (status, created_at)으로 바꾸자 EXPLAIN의 rows가 240만에서 1.2만으로 줄었고, 평균 실행 시간이 1.8초에서 40ms 수준으로 떨어졌다.

2) 컬럼에 함수 적용으로 인덱스 무력화

WHERE DATE(created_at) = '2026-06-01'. 컬럼 쪽에 함수가 걸리면 인덱스를 못 탄다. WHERE created_at >= '2026-06-01' AND created_at < '2026-06-02' 형태로 바꾼다. 같은 결과지만 인덱스 레인지 스캔이 가능해진다.

그러나, 3) LIKE '%xxx%' 와일드카드

따라서, 앞에 와일드카드가 오면 일반 B-Tree 인덱스로 못 잡는다. 데이터가 많지 않으면 LIKE 'xxx%'로 좁히고, 검색 빈도가 높으면 MySQL 전문 인덱스(FULLTEXT)나 PostgreSQL pg_trgm을 검토한다. 트래픽 일정량 이상이면 Elasticsearch·OpenSearch 같은 별도 검색 엔진으로 분리하는 게 정답인 경우도 있다.

PostgreSQL은 auto_explain이 강력하다

슬로우 로그와 EXPLAIN 결과를 한 줄에 같이 박아주는 모듈이다.

# auto_explain 활성화 — shared_preload_libraries는 재시작 필요
aws rds modify-db-parameter-group \
  --db-parameter-group-name prod-pg-15-pg \
  --parameters \
    "ParameterName=shared_preload_libraries,ParameterValue=auto_explain,ApplyMethod=pending-reboot" \
    "ParameterName=auto_explain.log_min_duration,ParameterValue=1000,ApplyMethod=immediate" \
    "ParameterName=auto_explain.log_analyze,ParameterValue=1,ApplyMethod=immediate" \
    "ParameterName=auto_explain.log_buffers,ParameterValue=1,ApplyMethod=immediate"

log_analyze=1은 실제 실행 계획을 측정하는 옵션이라 약간의 오버헤드가 있다. 운영에서 항상 켜기는 부담스럽고, 회귀가 의심될 때만 임시로 켜는 방식이 안전하다. 자세한 옵션은 PostgreSQL auto_explain 공식 문서에 정리되어 있다.

실제 조합과 비용

물론, PI는 12개 인스턴스 모두에 항상 켜둔다(7일 무료 보존). 회귀나 장애가 의심될 때만 슬로우 쿼리 로그를 일시적으로 활성화해 CloudWatch로 보낸다. 1시간이면 충분하고, 끝나면 export를 끈다. Logs Insights는 5분 단위 시간대만 잘라서 돌린다. pt-query-digest는 월 1회 정기 리포트용으로 슬로우 로그를 받아 돌리는데, 이게 PI 대시보드에서 놓친 쿼리를 잡아내는 채널이 됐다.

게다가, 이 조합에서 인스턴스 12대 기준 PI 추가 비용은 $0(보존 7일), CloudWatch는 export·보관 합쳐 월 $3 내외로 유지된다. 풀 옵션($7.3 × 12 + Logs Insights 무제한)으로 켰을 때 추정치 월 $90 대비 30분의 1 수준이다. 비용이 부담되는 환경이면 PI 7일 무료 + 슬로우 로그 파일 모드 + pt-query-digest 조합부터 시작하는 게 무난하다.

EXPLAIN ANALYZE 결과에서 Using filesort가 잡힌 쿼리는 인덱스 추가 전에 ORDER BY 컬럼이 WHERE 조건과 같은 인덱스에 묶일 수 있는지부터 확인한다. 같은 인덱스로 정렬까지 처리되면 Using filesort가 사라지면서 평균 실행 시간이 10배 이상 떨어지는 경우가 흔하다.

관련 글