이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › DB › 인덱스와 실행 계획
복합·커버링 인덱스
복합 인덱스는 컬럼 순서대로만 쓸 수 있다
여러 컬럼을 묶은 인덱스는 첫 컬럼부터 차례로 정렬돼 있습니다. 그래서 WHERE 조건이 앞 컬럼부터 채워져야 탐색 범위를 좁힐 수 있고, 범위 조건을 만나면 그 뒤 컬럼은 범위를 좁히는 데 쓰이지 못합니다. 조회하는 컬럼이 모두 인덱스 안에 있어서 테이블을 찾아가지 않아도 되는 경우, 그 인덱스를 커버링 인덱스라고 합니다.
문서·표준 대조 · MySQL 공식 문서
이 실습에 나오는 개념
- 복합 인덱스와 Leftmost Prefix
- 복합 인덱스는 두 개 이상의 컬럼을 묶어 만든 인덱스다. 첫 번째 컬럼으로 먼저 정렬하고 그 안에서 두 번째 컬럼으로 정렬하는 식으로 구성되므로, 인덱스의 왼쪽 컬럼부터 차례로 조건에 사용될 때만 탐색에 쓸 수 있다. (a, b, c) 인덱스는 a 조건이나 a와 b 조건에는 쓰이지만 b만 있는 조건에는 쓰이지 않는다(예외적으로 MySQL 8.0.13 이상은 선두 컬럼의 값 종류가 적을 때 옵티마이저가 Skip Scan으로 이 인덱스를 쓸 수 있다).
- 커버링 인덱스
- 쿼리가 필요로 하는 모든 컬럼이 인덱스에 포함되어 있어, 테이블의 행을 읽지 않고 인덱스만으로 결과를 만들 수 있는 경우를 말한다. 테이블 접근이 생략되어 조회가 빨라진다. MySQL의 실행 계획에서는 Extra 항목에 Using index로 표시된다.
- 클러스터드 인덱스와 세컨더리 인덱스
- 클러스터드 인덱스는 인덱스의 끝(리프)에 행 데이터 자체가 저장된 인덱스로, 테이블의 데이터가 그 키 순서대로 저장된다. MySQL InnoDB에서는 기본 키가 클러스터드 인덱스다. InnoDB의 세컨더리(보조) 인덱스는 리프에 기본 키 값을 가지고 있어, 세컨더리 인덱스로 찾은 뒤 그 기본 키로 클러스터드 인덱스를 한 번 더 찾아 행을 읽는다.
- 카디널리티와 선택도
- 카디널리티는 컬럼에 들어 있는 서로 다른 값의 개수다. 서로 다른 값이 많을수록(예: 주민번호, 이메일) 조건 하나로 걸러지는 행이 적어 인덱스의 효과가 크고, 값의 종류가 적을수록(예: 성별) 조건에 해당하는 행이 많아 인덱스의 효과가 작다. 선택도는 카디널리티를 테이블의 행 수로 나눈 값으로, 1에 가까울수록 조건 하나로 행이 잘 걸러진다.
- 실행 계획(EXPLAIN)
- 데이터베이스가 쿼리를 어떤 방법으로 실행할지 정한 계획이다. 어떤 인덱스를 쓰는지, 테이블을 어떤 순서로 조인하는지, 몇 행을 읽을 것으로 예상하는지를 보여 준다. 쿼리 앞에 EXPLAIN을 붙여 확인하며, 쿼리가 느릴 때 원인을 찾는 출발점이다.
이렇게 바꿔 보세요
- 처음 설정으로 — 세 조건이 인덱스 순서와 맞아 세 컬럼 모두 범위를 좁힙니다.
- 첫 컬럼 조건을 빼면 — member_id 조건이 없으면 이 인덱스로는 범위를 좁힐 수 없습니다.
- 가운데 컬럼 조건을 빼면 — 첫 컬럼까지만 쓰이고, 날짜 조건은 인덱스를 읽으면서 걸러냅니다.
- 범위 조건 컬럼이 가운데 있으면 — (member_id, created_at, status). 범위 뒤의 status는 범위를 좁히지 못합니다.
- 인덱스에 있는 컬럼만 조회 — 테이블에 가지 않고 인덱스만 읽고 끝납니다(커버링 인덱스).
- 첫 컬럼 조건 없이 인덱스 컬럼만 조회 — 범위는 못 좁히지만 테이블 대신 인덱스 전체를 훑습니다.
처음 설정으로 실행하면
- orders 테이블에는 100만 행이 있습니다. 인덱스는 member_id → status → created_at 순서로 정렬돼 있습니다. 첫 컬럼이 같은 것끼리 모여 있고, 그 안에서 둘째 컬럼 순, 다시 그 안에서 셋째 컬럼 순입니다.
- 1번째 컬럼 member_id: 조건 member_id = 7이 있고, 인덱스의 첫 컬럼이므로 이 조건으로 읽을 범위를 좁힙니다.
- 2번째 컬럼 status: 조건 status = 'PAID'이 있고, 앞 컬럼들이 모두 정해져 있으므로 이 조건으로 읽을 범위를 좁힙니다.
- 3번째 컬럼 created_at: 조건 created_at >= '2026-09-01'이 있고, 앞 컬럼들이 모두 정해져 있으므로 이 조건으로 읽을 범위를 좁힙니다.
- 인덱스에서 좁혀진 범위의 항목 50개를 읽습니다.
- SELECT *는 인덱스에 없는 컬럼도 필요합니다. 남은 50건마다 테이블(클러스터드 인덱스)로 가서 행 전체를 읽어 옵니다.
실행 결과
- 조건이 인덱스 순서대로 빠짐없이 이어져 세 컬럼 모두 범위를 좁혔습니다. WHERE에 적는 순서는 상관없고, 인덱스의 컬럼 순서가 기준입니다.
코드
CREATE INDEX idx_orders ON orders (member_id, status, created_at);
SELECT *
FROM orders
WHERE member_id = 7
AND status = 'PAID'
AND created_at >= '2026-09-01';
이 모형의 전제
100만 행 테이블에서 member_id = 7은 전체의 0.1%, status = 'PAID'는 50%, 날짜 조건은 10%가 해당한다고 가정한 계산입니다. 인덱스 컬럼을 왼쪽부터 써야 하고 범위 조건 뒤의 컬럼은 탐색 범위를 좁히지 못한다는 규칙은 MySQL 공식 문서의 복합 인덱스 설명을 따랐습니다. 순서가 끊긴 뒤의 조건을 인덱스 단계에서 걸러내는 것(Using index condition)은 MySQL의 인덱스 컨디션 푸시다운 동작입니다. 실제 옵티마이저는 통계에 따라 다른 계획을 고를 수 있으며, 여기서는 이 인덱스를 쓴다고 전제했습니다. 실제 DB의 EXPLAIN 결과와 대조한 것은 아닙니다.
앞 실습: 인덱스와 풀 스캔 · 다음 실습: 실행 계획 읽기