지난 글에서는 단일 인덱스를 정리했다.
단일 인덱스는 하나의 컬럼을 기준으로 만든 인덱스다.
CREATE INDEX idx_users_email
ON users(email);
하지만 실무 쿼리는 보통 이렇게 단순하지 않다.
SELECT *
FROM orders
WHERE user_id = 10
AND status = 'PAID'
ORDER BY created_at DESC;
조건이 여러 개 있고,
정렬도 함께 들어간다.
이럴 때 등장하는 것이 복합 인덱스다.
1. 복합 인덱스란?
복합 인덱스는 여러 컬럼을 묶어서 만든 인덱스다.
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at);
이 인덱스는 다음 세 컬럼을 함께 사용한다.
user_id
status
created_at
하지만 중요한 점이 있다.
복합 인덱스는
단일 인덱스 3개를 만든 것과 다르다.
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
이것과
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at);
이것은 완전히 다르다.
2. 복합 인덱스는 어떻게 정렬될까?
복합 인덱스는 여러 컬럼을 하나의 묶음처럼 정렬한다.
(user_id, status, created_at)
정렬 순서는 다음과 같다.
1. user_id로 먼저 정렬
2. user_id가 같으면 status로 정렬
3. status도 같으면 created_at으로 정렬
예를 들면 이런 식이다.
(user_id, status, created_at)
--------------------------------
(1, CANCEL, 2026-04-01)
(1, PAID, 2026-04-01)
(1, PAID, 2026-04-02)
(2, PAID, 2026-04-01)
(2, READY, 2026-04-03)
(3, PAID, 2026-04-01)
코드로 보면 튜플 비교와 비슷하다.
def compare(a, b):
if a.user_id != b.user_id:
return a.user_id - b.user_id
if a.status != b.status:
return compare_string(a.status, b.status)
return compare_date(a.created_at, b.created_at)
즉, 복합 인덱스는
여러 컬럼을 따로따로 보는 게 아니라
하나의 정렬 기준으로 본다.
3. Leftmost Prefix Rule
복합 인덱스에서 가장 중요한 개념은
Leftmost Prefix Rule이다.
말은 어렵지만 핵심은 단순하다.
복합 인덱스는 왼쪽 컬럼부터 순서대로 사용할 때 가장 잘 동작한다.
예를 들어 이런 인덱스가 있다.
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at);
이 인덱스는 다음 조건에 잘 맞는다.
WHERE user_id = 10;
WHERE user_id = 10
AND status = 'PAID';
WHERE user_id = 10
AND status = 'PAID'
AND created_at >= '2026-04-01';
왜냐하면 왼쪽부터 사용하고 있기 때문이다.
user_id -> status -> created_at
반면 이런 쿼리는 잘 맞지 않는다.
WHERE status = 'PAID';
왜 그럴까?
인덱스가 status 기준으로 먼저 정렬된 게 아니기 때문이다.
실제 정렬은 이렇다.
user_id = 1 안에서 status 정렬
user_id = 2 안에서 status 정렬
user_id = 3 안에서 status 정렬
...
전체적으로 보면 status만 기준으로 정렬되어 있지 않다.
그래서 status = 'PAID'만으로는
원하는 위치로 바로 이동하기 어렵다.
4. 컬럼 순서가 중요한 이유
다음 두 인덱스는 다르다.
CREATE INDEX idx_1
ON orders(user_id, status, created_at);
CREATE INDEX idx_2
ON orders(created_at, user_id, status);
둘 다 같은 컬럼을 가지고 있다.
하지만 정렬 순서가 다르다.
자주 실행되는 쿼리가 이것이라면:
SELECT *
FROM orders
WHERE user_id = 10
AND status = 'PAID'
ORDER BY created_at DESC;
보통은 이런 인덱스가 더 자연스럽다.
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at);
이유는 다음과 같다.
1. user_id = 10으로 먼저 범위를 좁힌다.
2. 그 안에서 status = PAID로 다시 좁힌다.
3. 그 안에서 created_at 순서를 활용한다.
즉, WHERE 조건과 ORDER BY 흐름이
인덱스 정렬 순서와 잘 맞는다.
5. 범위 조건이 들어오면?
복합 인덱스에서 범위 조건은 조심해야 한다.
예를 들어 이런 인덱스가 있다.
CREATE INDEX idx_orders_user_created_status
ON orders(user_id, created_at, status);
그리고 쿼리는 다음과 같다.
SELECT *
FROM orders
WHERE user_id = 10
AND created_at >= '2026-04-01'
AND status = 'PAID';
여기서 created_at은 범위 조건이다.
created_at >= '2026-04-01'
일반적으로 복합 인덱스에서는
범위 조건 이후 컬럼은 정밀하게 활용하기 어려워질 수 있다.
즉,
user_id는 잘 사용
created_at 범위도 사용
status는 인덱스 탐색 조건으로는 약해질 수 있음
그래서 컬럼 순서를 잡을 때는
자주 사용하는 쿼리 패턴을 봐야 한다.
보통은 이런 흐름을 많이 본다.
등호 조건
-> 범위 조건
-> 정렬 조건
예를 들어:
WHERE user_id = ?
AND status = ?
AND created_at BETWEEN ? AND ?
ORDER BY created_at DESC
이런 쿼리가 많다면 후보는 다음과 같다.
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at);
6. 단일 인덱스 여러 개와 복합 인덱스 하나
이 부분도 많이 헷갈린다.
CREATE INDEX idx_orders_user_id
ON orders(user_id);
CREATE INDEX idx_orders_status
ON orders(status);
이렇게 만든 것과
CREATE INDEX idx_orders_user_status
ON orders(user_id, status);
이렇게 만든 것은 다르다.
쿼리가 다음과 같다고 하자.
SELECT *
FROM orders
WHERE user_id = 10
AND status = 'PAID';
복합 인덱스는 바로 이런 구조로 찾을 수 있다.
(user_id = 10, status = PAID)
반면 단일 인덱스 두 개는
DB가 하나만 사용하거나, 경우에 따라 둘을 조합해야 한다.
user_id 인덱스로 후보 찾기
status 인덱스로 후보 찾기
교집합 계산
DB마다 최적화 방식은 다르지만
자주 같이 쓰이는 조건이라면
복합 인덱스가 더 직접적인 해결책인 경우가 많다.
7. Covering Index
복합 인덱스를 이해하면
Covering Index도 자연스럽게 이해할 수 있다.
예를 들어 이런 인덱스가 있다.
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at);
그리고 쿼리는 다음과 같다.
SELECT user_id, status, created_at
FROM orders
WHERE user_id = 10
AND status = 'PAID';
이 쿼리에 필요한 컬럼은 모두 인덱스 안에 있다.
user_id
status
created_at
그러면 DB는 실제 테이블 row를 읽지 않고
인덱스만 보고 결과를 반환할 수 있다.
이것을 Covering Index라고 한다.
Covering Index는 쿼리에 필요한 컬럼을 인덱스가 모두 포함하고 있는 경우다.
장점은 명확하다.
테이블 접근을 줄일 수 있다.
하지만 단점도 있다.
인덱스 크기가 커진다.
쓰기 비용이 증가한다.
캐시 효율이 떨어질 수 있다.
그래서 무조건 컬럼을 많이 넣는 것은 좋지 않다.
8. EXPLAIN으로 확인하기
인덱스를 만들었다고 끝이 아니다.
정말 사용되는지 확인해야 한다.
MySQL에서는 다음처럼 볼 수 있다.
EXPLAIN
SELECT *
FROM orders
WHERE user_id = 10
AND status = 'PAID'
ORDER BY created_at DESC;
PostgreSQL에서는 보통 이렇게 본다.
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 10
AND status = 'PAID'
ORDER BY created_at DESC;
확인할 것은 다음이다.
Full Table Scan인지
Index Scan인지
Index Only Scan인지
어떤 인덱스를 사용했는지
예상 row 수와 실제 row 수가 비슷한지
별도 Sort가 발생했는지
인덱스 설계는 감으로 끝내면 안 된다.
최종 판단은 실행 계획으로 확인해야 한다.
9. 실무 인덱스 설계 기준
인덱스를 설계할 때는
테이블부터 보는 것보다 쿼리부터 보는 것이 좋다.
질문은 다음과 같다.
이 쿼리는 자주 실행되는가?
조건 컬럼은 무엇인가?
등호 조건인가, 범위 조건인가?
정렬이 필요한가?
조회 결과는 전체 중 몇 %인가?
기존 인덱스와 겹치지 않는가?
쓰기 비용을 감당할 수 있는가?
특히 복합 인덱스는 컬럼 순서가 중요하다.
일반적인 기준은 다음과 같다.
1. 자주 같이 쓰이는 조건을 묶는다.
2. 등호 조건 컬럼을 앞쪽에 둔다.
3. 범위 조건 컬럼은 그 다음에 둔다.
4. ORDER BY에 쓰이는 컬럼까지 고려한다.
5. 너무 많은 인덱스를 만들지 않는다.
하지만 이건 절대 규칙은 아니다.
데이터 분포와 쿼리 패턴에 따라 달라진다.
그래서 항상 EXPLAIN으로 확인해야 한다.
10. 핵심 요약
복합 인덱스는 실무에서 매우 중요하다.
정리하면 다음과 같다.
1. 복합 인덱스는 여러 컬럼을 묶은 인덱스다.
2. 여러 단일 인덱스와 같은 의미가 아니다.
3. 복합 인덱스는 왼쪽 컬럼부터 정렬된다.
4. Leftmost Prefix Rule이 중요하다.
5. 컬럼 순서가 성능을 결정한다.
6. 자주 같이 쓰는 WHERE 조건을 기준으로 설계한다.
7. ORDER BY와 Covering Index까지 고려할 수 있다.
8. 최종 판단은 EXPLAIN으로 확인한다.
한 줄로 정리하면 이렇다.
복합 인덱스는 쿼리 패턴에 맞춰 만든 정렬 구조다.
인덱스는 많이 만든다고 좋은 것이 아니다.
정확한 위치에, 정확한 순서로 만들어야 한다.
결국 인덱스 설계의 핵심은 이것이다.
DB가 덜 읽게 만드는 것.
그게 인덱스의 본질이다.
'프로그래밍공부(Programming Study) > CS-데이터베이스(Database)' 카테고리의 다른 글
| 단일 인덱스는 어떻게 동작할까? (0) | 2026.04.30 |
|---|---|
| DB는 데이터를 어떻게 찾을까? - Full Scan과 B+Tree (0) | 2026.04.29 |
| CAP 정리: 일관성, 가용성, 분할내성의 상충관계 완벽 정리 (1) | 2024.12.04 |
| MongoDB 특징 및 설치/환경설정 (0) | 2023.01.03 |
| MySQL 버전별 차이 (0) | 2022.12.03 |
댓글