대용량 데이터를 다루는 서비스에서 SQL 쿼리 성능은 곧 서비스 속도와 직결됩니다. 같은 데이터를 가져오는 쿼리라도 몇 초 걸리느냐, 몇 밀리초 걸리느냐의 차이는 사용자 경험을 크게 바꿉니다. 이번 글에서는 쿼리 최적화의 기초 중 기초인 인덱스와 실행 계획(EXPLAIN)을 중심으로, 실무에서 꼭 알아야 할 최적화 원리를 정리하겠습니다.
인덱스(Index)란?
인덱스는 데이터베이스의 책갈피 역할을 합니다. 전체 데이터를 처음부터 끝까지 읽는 Full Table Scan 대신, 인덱스를 통해 원하는 데이터가 있는 위치를 빠르게 찾아갈 수 있습니다.
📌 인덱스 기본 원리
* DB는 보통 B-Tree 구조로 인덱스를 저장
* 루트 노드 → 브랜치 노드 → 리프 노드로 내려가며 값을 탐색
* 리프 노드에 해당 레코드의 실제 위치(ROWID 등) 저장
📌 인덱스 장점
* 조회 속도 향상 (특히 WHERE, JOIN, ORDER BY, GROUP BY에 사용 시)
* 대량 데이터에서도 일정한 검색 속도 유지
📌 인덱스 단점
* INSERT, UPDATE, DELETE 시 인덱스 갱신 오버헤드 발생
* 디스크 공간 사용량 증가
* 너무 많은 인덱스 → 쿼리 성능 저하
인덱스 종류
1) 기본 인덱스(Primary Key)
* 고유한 값에 자동 생성
* 각 테이블당 하나만 존재
2) 고유 인덱스(Unique Index)
* 중복 허용 안 함
* 이메일, 주민번호 같은 컬럼에 주로 사용
3) 비고유 인덱스(Non-Unique Index)
* 중복 가능
* 조회 조건에 자주 쓰이는 컬럼에 사용
4) 복합 인덱스(Composite Index)
* 여러 컬럼을 묶어서 생성
* 인덱스 순서가 중요 (선행 컬럼 기준 검색 시만 효율적)
5) 커버링 인덱스(Covering Index)
* 쿼리에 필요한 모든 컬럼이 인덱스에 포함되어 데이터 페이지 접근 없이 인덱스만으로 처리 가능
인덱스 생성 예제
-- 단일 컬럼 인덱스
CREATE INDEX idx_users_email ON users(email);
-- 복합 인덱스 (name, age 순서)
CREATE INDEX idx_users_name_age ON users(name, age);
💡 복합 인덱스 주의점
* ( name, age) 인덱스 → name으로 검색 O, age 단독 검색 X
* 선행 컬럼을 WHERE 조건에 포함시키는 것이 중요
인덱스 사용 여부 확인하기
아무리 인덱스를 걸어도 쿼리가 그 인덱스를 사용하지 않을 수 있습니다. 이럴 때 쓰는 도구가 바로 EXPLAIN입니다.
실행 계획(EXPLAIN) 분석
MySQL에서 EXPLAIN은 쿼리 실행 계획을 보여주는 명령어입니다.
EXPLAIN
SELECT name, age
FROM users
WHERE email = 'test@example.com';
📌 주요 컬럼 설명
[컬럼 설명]
id | 쿼리 실행 순서 |
select_type | SELECT 유형 (SIMPLE, SUBQUERY, DERIVED 등) |
table | 참조하는 테이블명 |
type | 접근 방식 (ALL, index, range, ref, eq_ref, const 등) |
possible_keys | 사용할 수 있는 인덱스 후보 |
key | 실제 사용된 인덱스 |
rows | 예측 읽기 건수 |
Extra | 추가 정보 (Using index, Using filesort 등) |
📌 type(접근 방식) 성능 순서
const > eq_ref > ref > range > index > ALL
* ALL: 전체 테이블 스캔 (가장 느림)
* range: 범위 검색 (BETWEEN, >, < 등)
* ref: 동등 비교로 인덱스 참조
* const: PK나 Unique Key로 단일 행 조회
📌 Extra 컬럼 해석
* Using index: 커버링 인덱스 사용 (성능 좋음)
* Using where: WHERE 필터링 수행
* Using filesort: 별도 정렬 발생 (가능하면 피해야 함)
* Using temporary: 임시 테이블 생성 (GROUP BY, DISTINCT에서 자주 발생)
인덱스 + EXPLAIN 최적화 예시
📌 최적화 전
EXPLAIN
SELECT name, age
FROM users
WHERE YEAR(created_at) = 2024;
* 문제점: YEAR(created_at) 함수 사용 → 인덱스 적용 불가
* type: ALL (풀스캔)
* rows: 1,000,000
📌 최적화 후
EXPLAIN
SELECT name, age
FROM users
WHERE created_at >= '2024-01-01'
AND created_at < '2025-01-01';
* 결과: range 접근 방식
* 인덱스 사용 (idx_created_at)
* rows: 10,000 (훨씬 적음)
💡 함수나 연산이 컬럼에 직접 적용되면 인덱스 무력화
→ 조건식을 변형해서 인덱스를 활용해야 함
실무 최적화 팁
1. WHERE 절 컬럼 순서와 인덱스 순서 맞추기
2. SELECT * 지양 → 필요한 컬럼만 조회 (커버링 인덱스 유도)
3. 함수/연산 피하기 → 인덱스 활용 가능 형태로 변경
4. LIMIT 최적화 → 정렬 + 인덱스 활용
5. 통계 갱신 (ANALYZE TABLE) → 옵티마이저가 최신 데이터 분포를 알아야 최적화 가능
마무리
인덱스와 실행 계획(EXPLAIN)은 SQL 최적화의 출발점이자 필수 도구입니다.
* 인덱스는 검색 속도를 빠르게
* EXPLAIN은 쿼리의 병목 구간을 시각화
* 인덱스 설계 → 실행 계획 확인 → 조건식 최적화 → 재검증
이 사이클을 반복하면 대부분의 쿼리 성능 문제를 해결할 수 있습니다.