여러 칼럼을 하나로 묶은 인덱스. 칼럼 순서가 전부다.
왜 순서가 중요한가 — 전화번호부와 같다
INDEX (a, b, c)는 a로 먼저 정렬하고, a가 같은 것끼리 b로, b도 같으면 c로 정렬한다. 전화번호부가 성 → 이름 순인 것과 정확히 같다.
INDEX (a, b, c) 가 도움 되는 조회
WHERE a = ? ✅
WHERE a = ? AND b = ? ✅
WHERE a = ? AND b = ? AND c = ? ✅
WHERE b = ? ❌ a를 건너뛰면 정렬이 무의미
WHERE a = ? AND c = ? △ a까지만 인덱스, c는 읽어서 걸러냄
성을 모르면 전화번호부에서 이름만으로 못 찾는 것과 같다. 이 성질을 왼쪽 접두사(leftmost prefix) 규칙이라 한다.
칼럼 순서를 정하는 규칙
- 등치(=) 조건 칼럼을 앞에, — 범위(> · BETWEEN) 칼럼을 뒤에
범위 조건이 걸린 칼럼 뒤부터는 인덱스 정렬을 쓸 수 없기 때문이다.
-- ❌ 나쁜 순서: created_at 이 범위라 status 정렬이 죽는다
CREATE INDEX idx_bad ON orders (created_at, status);
-- ✅ 좋은 순서: 등치 먼저, 범위 나중
CREATE INDEX idx_good ON orders (status, created_at);
SELECT * FROM orders
WHERE status = 'PAID' AND created_at >= '2026-01-01';
ORDER BY까지 덤으로 해결된다
인덱스 순서가 정렬 순서와 맞으면 별도 정렬 연산이 사라진다.
-- idx_good 이 (status, created_at) 이므로 정렬이 이미 돼 있다
SELECT * FROM orders WHERE status = 'PAID' ORDER BY created_at LIMIT 20;
면접 함정
- ❌ "칼럼마다 인덱스를 하나씩 만들면 복합 인덱스와 같다" → 다르다. 단일 인덱스 여러 개는 대개 하나만 선택되거나 비싼 병합을 거친다. 함께 쓰이는 조건은 하나의 복합 인덱스가 낫다.
- ❌ "(a,b,c)가 있으면 (a,b)도 따로 필요하다" → 필요 없다. 왼쪽 접두사라 이미 커버된다.
정말 잘 쓰이는지 확인하기
-- PostgreSQL: Index Cond 와 Filter 를 구분해서 본다
EXPLAIN ANALYZE
SELECT * FROM orders WHERE status = 'PAID' AND created_at >= '2026-01-01';
-- Index Cond: ((status = 'PAID') AND (created_at >= '2026-01-01')) ← 둘 다 인덱스로 좁힘 (좋음)
-- Filter: (created_at >= '2026-01-01') ← 읽어서 걸러냄 (인덱스 미활용)
Index Cond에 들어가면 인덱스로 범위를 좁힌 것이고, Filter로 빠지면 읽은 뒤 버린 것이다. 칼럼 순서가 잘못됐는지 판단하는 가장 정확한 신호다.
-- MySQL: key_len 으로 인덱스를 몇 바이트까지 썼는지 본다
EXPLAIN SELECT * FROM orders WHERE status = 'PAID' AND created_at >= '2026-01-01';
-- key_len 이 status 칼럼 길이만큼이면 created_at 은 못 쓴 것이다
안 쓰는 인덱스 찾아내기
-- PostgreSQL: idx_scan 이 0이면 한 번도 안 쓰인 인덱스다
SELECT relname, indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes WHERE idx_scan = 0 ORDER BY pg_relation_size(indexrelid) DESC;
안 쓰는 인덱스는 순수 부채다 — 쓰기마다 갱신되고 디스크를 먹는다. 지우기 전 영향을 보고 싶으면 MySQL 8.0+의 ALTER TABLE ... ALTER INDEX ... INVISIBLE로 먼저 숨겨 본다.