[MySQL] 12. 성능 튜닝 (Performance Tuning)
MySQL/MariaDB의 성능 분석, 쿼리 최적화, 서버 튜닝 기법을 정리합니다.
성능 튜닝 3대 영역
| 영역 | 설명 | 효과 |
|---|---|---|
| 쿼리 최적화 | SQL 문 자체를 개선 | 가장 큰 효과 (80%) |
| 인덱스 최적화 | 적절한 인덱스 설계 | 높은 효과 |
| 서버 설정 튜닝 | my.cnf 파라미터 조정 | 보조적 효과 |
슬로우 쿼리 로그 (Slow Query Log)
실행 시간이 긴 쿼리를 자동으로 기록합니다.
설정
# my.cnf (my.ini)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-query.log
long_query_time = 1 # 1초 이상 걸리는 쿼리 기록
log_queries_not_using_indexes = 1 # 인덱스 미사용 쿼리도 기록
-- 동적 설정 (재시작 없이)
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 1;
-- 현재 설정 확인
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
슬로우 쿼리 분석 도구
# mysqldumpslow: 슬로우 쿼리 요약
mysqldumpslow -s t -t 10 /var/log/mysql/slow-query.log
# -s t: 실행 시간 순 정렬
# -t 10: 상위 10개
# pt-query-digest (Percona Toolkit): 더 상세한 분석
pt-query-digest /var/log/mysql/slow-query.log
EXPLAIN 심화 분석
EXPLAIN ANALYZE (MySQL 8.0.18+)
실제 실행 시간과 행 수를 포함한 상세 분석을 제공합니다.
EXPLAIN ANALYZE
SELECT e.name, d.name AS dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.id
WHERE e.salary > 5000000;
실행 계획 개선 패턴
패턴 1: Full Table Scan → Index Scan
-- ❌ type: ALL (Full Table Scan)
EXPLAIN SELECT * FROM orders WHERE customer_name = '홍길동';
-- ✅ 인덱스 추가 후 type: ref
CREATE INDEX idx_customer_name ON orders (customer_name);
EXPLAIN SELECT * FROM orders WHERE customer_name = '홍길동';
패턴 2: filesort 제거
-- ❌ Extra: Using filesort
EXPLAIN SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at;
-- ✅ 복합 인덱스로 filesort 제거
CREATE INDEX idx_status_created ON orders (status, created_at);
패턴 3: Using temporary 제거
-- ❌ Extra: Using temporary
EXPLAIN SELECT dept_id, COUNT(*) FROM employees GROUP BY dept_id ORDER BY COUNT(*) DESC;
-- ✅ 인덱스 활용
CREATE INDEX idx_dept ON employees (dept_id);
쿼리 최적화 기법
1. SELECT 최적화
-- ❌ 불필요한 전체 컬럼 조회
SELECT * FROM employees WHERE dept_id = 1;
-- ✅ 필요한 컬럼만 조회
SELECT id, name, salary FROM employees WHERE dept_id = 1;
-- ❌ 서브쿼리 (비효율적인 경우)
SELECT * FROM employees
WHERE dept_id IN (SELECT id FROM departments WHERE name = '개발팀');
-- ✅ JOIN으로 변환
SELECT e.* FROM employees e
INNER JOIN departments d ON e.dept_id = d.id
WHERE d.name = '개발팀';
2. WHERE 절 최적화
-- ❌ 컬럼에 함수 적용 (인덱스 무효화)
SELECT * FROM orders WHERE DATE(created_at) = '2026-05-04';
SELECT * FROM employees WHERE LOWER(name) = '홍길동';
SELECT * FROM orders WHERE amount + 100 > 5000;
-- ✅ 값 쪽에 연산 적용
SELECT * FROM orders WHERE created_at >= '2026-05-04' AND created_at < '2026-05-05';
SELECT * FROM employees WHERE name = '홍길동';
SELECT * FROM orders WHERE amount > 4900;
3. JOIN 최적화
-- 작은 테이블을 드라이빙 테이블로 (옵티마이저가 자동 판단하지만 힌트 가능)
SELECT /*+ JOIN_ORDER(d, e) */ e.name, d.name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.id;
-- JOIN 컬럼에 인덱스 필수
-- employees.dept_id에 인덱스가 없으면 Nested Loop 시 매번 Full Scan
CREATE INDEX idx_dept_id ON employees (dept_id);
4. LIMIT 활용
-- ❌ 전체 조회 후 애플리케이션에서 제한
SELECT * FROM logs ORDER BY created_at DESC;
-- ✅ DB 레벨에서 제한
SELECT * FROM logs ORDER BY created_at DESC LIMIT 100;
-- EXISTS로 존재 여부만 확인
-- ❌
SELECT COUNT(*) FROM orders WHERE user_id = 1; -- 전체 카운트
-- ✅
SELECT EXISTS(SELECT 1 FROM orders WHERE user_id = 1); -- 1건만 확인
5. 대량 데이터 처리
-- ❌ 한 번에 대량 UPDATE
UPDATE orders SET status = 'archived' WHERE created_at < '2025-01-01';
-- ✅ 배치 처리
-- 1000건씩 나누어 처리 (락 경합 감소)
UPDATE orders SET status = 'archived'
WHERE created_at < '2025-01-01' AND status != 'archived'
LIMIT 1000;
-- 영향받은 행이 0이 될 때까지 반복
-- ❌ 한 번에 대량 INSERT
INSERT INTO target SELECT * FROM source; -- 수백만 건
-- ✅ 배치 INSERT
INSERT INTO target SELECT * FROM source WHERE id BETWEEN 1 AND 10000;
INSERT INTO target SELECT * FROM source WHERE id BETWEEN 10001 AND 20000;
서버 설정 튜닝 (my.cnf)
InnoDB 핵심 파라미터
[mysqld]
# === 메모리 설정 ===
# InnoDB 버퍼 풀: 전체 메모리의 60~80% (가장 중요한 설정)
innodb_buffer_pool_size = 4G
# 버퍼 풀 인스턴스 (버퍼 풀 1GB당 1개 권장)
innodb_buffer_pool_instances = 4
# === 로그 설정 ===
# Redo 로그 크기 (쓰기 성능에 영향)
innodb_log_file_size = 1G
# 로그 버퍼 크기
innodb_log_buffer_size = 64M
# === I/O 설정 ===
# I/O 스레드 수
innodb_read_io_threads = 8
innodb_write_io_threads = 8
# I/O 용량 (SSD: 2000~5000, HDD: 200~400)
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
# === 동시성 설정 ===
# 동시 스레드 수 (0 = 무제한)
innodb_thread_concurrency = 0
# === 플러시 설정 ===
# 트랜잭션 커밋 시 로그 플러시 (1: 안전, 2: 성능)
innodb_flush_log_at_trx_commit = 1
# 플러시 방식 (Linux에서 O_DIRECT 권장)
innodb_flush_method = O_DIRECT
연결 및 쿼리 설정
[mysqld]
# 최대 동시 접속 수
max_connections = 500
# 쿼리 타임아웃 (초)
wait_timeout = 600
interactive_timeout = 600
# 임시 테이블 크기
tmp_table_size = 256M
max_heap_table_size = 256M
# 정렬 버퍼
sort_buffer_size = 4M
join_buffer_size = 4M
# 테이블 캐시
table_open_cache = 4000
현재 설정 확인 및 동적 변경
-- 현재 값 확인
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW VARIABLES LIKE 'max_connections';
-- 동적 변경 (재시작 없이)
SET GLOBAL max_connections = 500;
SET GLOBAL innodb_buffer_pool_size = 4294967296; -- 4GB
-- 상태 모니터링
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';
모니터링 쿼리
현재 프로세스 확인
-- 현재 실행 중인 쿼리
SHOW PROCESSLIST;
SHOW FULL PROCESSLIST;
-- 특정 쿼리 강제 종료
KILL 프로세스ID;
KILL QUERY 프로세스ID; -- 쿼리만 종료 (연결 유지)
버퍼 풀 히트율
SELECT
(1 - (
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') /
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests')
)) * 100 AS buffer_pool_hit_rate;
-- 99% 이상이 정상, 95% 미만이면 버퍼 풀 증가 필요
테이블/인덱스 크기 확인
-- 테이블별 크기
SELECT
table_name,
ROUND(data_length / 1024 / 1024, 2) AS data_mb,
ROUND(index_length / 1024 / 1024, 2) AS index_mb,
ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_mb,
table_rows
FROM information_schema.tables
WHERE table_schema = 'mydb'
ORDER BY total_mb DESC;
사용되지 않는 인덱스 확인 (MySQL 8.0+)
SELECT
object_schema, object_name, index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND count_star = 0
AND object_schema = 'mydb'
ORDER BY object_name;
튜닝 체크리스트
| 단계 | 점검 항목 | 확인 방법 |
|---|---|---|
| 1 | 슬로우 쿼리 로그 활성화 | SHOW VARIABLES LIKE 'slow_query%' |
| 2 | 상위 슬로우 쿼리 분석 | mysqldumpslow 또는 pt-query-digest |
| 3 | EXPLAIN으로 실행 계획 확인 | EXPLAIN SELECT ... |
| 4 | 인덱스 누락 확인 | type이 ALL인 쿼리 |
| 5 | 불필요한 인덱스 제거 | performance_schema 조회 |
| 6 | 버퍼 풀 히트율 확인 | 99% 이상 유지 |
| 7 | 커넥션 수 확인 | max_connections 대비 사용량 |
| 8 | 디스크 I/O 확인 | innodb_io_capacity 조정 |
| 9 | 테이블 크기 확인 | 대용량 테이블 파티셔닝 검토 |
| 10 | 정기적인 ANALYZE TABLE | 통계 정보 갱신 |
-- 테이블 통계 갱신
ANALYZE TABLE employees;
-- 테이블 최적화 (단편화 제거)
OPTIMIZE TABLE employees;
- [MySQL] 13. 자주 발생하는 Troubleshooting
- [MySQL] 12. 성능 튜닝 (Performance Tuning)
- [MySQL] 11. 백업과 복구 (Backup & Restore)
- [MySQL] 10. 사용자 관리와 권한 (User & Privilege)
- [MySQL] 09. 뷰, 스토어드 프로시저, 함수, 트리거
- [MySQL] 08. 트랜잭션과 락 (Transaction & Lock)
- [MySQL] 07. 인덱스 (Index)
- [MySQL] 06. 내장 함수 정리
- [MySQL] 05. 서브쿼리와 고급 SELECT
- [MySQL] 04. JOIN (테이블 결합)
- [MySQL] 03. CRUD 기본 (INSERT, SELECT, UPDATE, DELETE)
- [MySQL] 02. 데이터베이스와 테이블 관리
- [MySQL] 01. MySQL/MariaDB 소개 및 설치