“느리면 인덱스 걸면 되지 않나요?” 절반만 맞는 말입니다. 인덱스를 걸어도 여전히 느린 경우가 많고, 특히 복합 인덱스는 컬럼 순서를 잘못 잡으면 아예 타지 않습니다. 세 컬럼짜리 인덱스를 만들어놓고도 쿼리가 풀스캔을 하고 있다면, 원인은 인덱스가 없어서가 아니라 순서가 쿼리와 맞지 않아서입니다.
복합 인덱스는 여러 컬럼을 묶은 하나의 정렬된 구조입니다. 이 “정렬 순서”가 곧 인덱스의 성능을 결정합니다. 같은 컬럼 조합이라도 (a, b, c)로 만드느냐 (c, b, a)로 만드느냐에 따라, 어떤 쿼리는 즉시 빨라지고 어떤 쿼리는 인덱스를 전혀 못 씁니다.
이 글에서는 복합 인덱스 설계의 핵심인 최좌측 접두사 규칙, 등치 조건과 범위 조건의 배치, ORDER BY까지 인덱스로 처리하는 법, 그리고 인덱스를 무력화하는 흔한 실수까지 원리 중심으로 정리합니다.
오해: 컬럼마다 인덱스를 걸면 될까
가장 흔한 오해는 “조건에 쓰는 컬럼마다 인덱스를 하나씩 만들면 된다”는 생각입니다. status에 하나, user_id에 하나, created_at에 하나. 하지만 WHERE status = ? AND user_id = ? AND created_at > ?처럼 여러 조건이 함께 오는 쿼리에서는, 이렇게 흩어진 단일 인덱스들이 기대만큼 효과를 내지 못합니다.
데이터베이스는 보통 하나의 테이블 접근에 인덱스 하나를 주로 사용합니다. 조건이 세 컬럼에 걸쳐 있는데 인덱스가 따로따로면, 그중 하나로 후보를 좁힌 뒤 나머지는 걸러내는 식이 됩니다. 이 조합 조건을 한 번에 처리하려면 여러 컬럼을 묶은 복합 인덱스가 필요합니다.
CREATE INDEX idx_posts_status_user_created
ON posts (status, user_id, created_at);
문제는 이 순서를 어떻게 정하느냐입니다. 인덱스 성능을 다룬 서버 교체 후 1시간마다 죽는 서비스, 원인은 DB 락에서도 인덱스를 잘못 태우는 문제를 다뤘는데, 복합 인덱스에서는 순서가 그 출발점입니다.
최좌측 접두사 규칙: 인덱스는 왼쪽부터만 쓸 수 있다
복합 인덱스의 가장 중요한 원리는 최좌측 접두사(leftmost prefix) 규칙입니다. (status, user_id, created_at) 인덱스는 왼쪽부터 이어지는 조합에만 사용할 수 있습니다. 즉 (status), (status, user_id), (status, user_id, created_at) 조건에는 쓰이지만, 왼쪽이 빠지면 인덱스를 타지 못합니다.
이유는 인덱스가 왼쪽 컬럼을 기준으로 먼저 정렬돼 있기 때문입니다. 전화번호부가 성(姓)으로 먼저 정렬돼 있으면 이름만으로는 찾기 어려운 것과 같습니다. WHERE user_id = ?처럼 첫 컬럼 status 없이 두 번째 컬럼만 조건으로 주면, 이 인덱스로는 원하는 위치를 특정할 수 없어 풀스캔으로 빠집니다.
그래서 복합 인덱스 설계의 첫 질문은 이것입니다. “어떤 컬럼이 거의 항상 조건으로 들어오는가.” 대부분의 쿼리에서 등치 조건으로 함께 들어오는 컬럼을 맨 앞에 두어야, 그 인덱스가 여러 쿼리에서 재사용됩니다.
등치 조건은 앞, 범위 조건은 뒤에
두 번째 원리는 등치(=) 조건을 앞에, 범위(>, <, BETWEEN) 조건을 뒤에 두는 것입니다. 이건 성능에 직접적인 영향을 줍니다.
인덱스는 범위 조건을 만나는 순간, 그 뒤 컬럼의 정렬 순서를 더 이상 활용하지 못합니다. 예를 들어 WHERE status = ? AND user_id > ? AND created_at = ?라는 쿼리에 (status, user_id, created_at) 인덱스를 쓰면, status(등치)와 user_id(범위)까지는 인덱스로 좁히지만 created_at은 인덱스로 걸러내지 못합니다. user_id가 범위라 그 뒤부터는 정렬이 흐트러지기 때문입니다.
그래서 조건에 범위가 섞여 있다면, 등치로 들어오는 컬럼을 모두 앞에 배치하고 범위 컬럼을 그 뒤에 두는 것이 유리합니다. 이 배치 하나로 인덱스가 걸러낼 수 있는 범위가 크게 달라집니다. 쿼리 지연의 실제 원인을 추적한 MySQL 쿼리 지연 해결: 3개월 클라우드 DB 분투기 사례에서도, 결국 조건과 인덱스 순서의 정합이 핵심이었습니다.
ORDER BY까지 인덱스로 해결하기
복합 인덱스는 조건 필터링뿐 아니라 정렬 비용도 없애줄 수 있습니다. 인덱스는 이미 정렬된 구조이므로, ORDER BY가 인덱스 순서와 일치하면 데이터베이스는 별도의 정렬(filesort) 없이 인덱스를 읽는 순서 그대로 결과를 돌려줍니다.
-- (status, created_at) 인덱스가 있으면 정렬까지 인덱스로 해결
SELECT id, title
FROM posts
WHERE status = 'PUBLISHED'
ORDER BY created_at DESC
LIMIT 20;
이 쿼리는 status로 좁힌 뒤 created_at 순서로 정렬해야 하는데, (status, created_at) 인덱스가 있으면 그 정렬이 공짜가 됩니다. 대용량에서 정렬은 상당한 비용이라, ORDER BY와 LIMIT이 함께 오는 목록 쿼리에서 이 효과는 특히 큽니다. 그래서 복합 인덱스를 설계할 때는 WHERE 조건뿐 아니라 ORDER BY 컬럼까지 순서에 포함하는 것을 함께 고려해야 합니다.
조건별 (status, user_id, created_at) 인덱스 활용 여부

같은 인덱스라도 쿼리 조건에 따라 활용도가 크게 달라집니다. (status, user_id, created_at) 인덱스를 기준으로 정리하면 다음과 같습니다.
| WHERE / ORDER BY 조건 | 인덱스 활용 |
|---|---|
status = ? | ✅ 완전 활용 (최좌측) |
status = ? AND user_id = ? | ✅ 완전 활용 |
status = ? AND user_id = ? AND created_at > ? | ✅ 완전 활용 |
user_id = ? (status 없음) | ❌ 못 탐 (좌측 컬럼 빠짐) |
status = ? AND created_at > ? | ⚠️ status만 활용, created_at은 건너뜀 |
status = ? AND user_id > ? AND created_at = ? | ⚠️ user_id 범위 이후 created_at은 정렬 못 씀 |
status = ? ORDER BY user_id | ✅ 정렬까지 인덱스로 처리 |
핵심은 왼쪽부터 등치로 이어지다가, 범위를 만나면 거기서 인덱스 활용이 끊긴다는 패턴입니다. 이 표 한 장이 복합 인덱스 순서 설계의 대부분을 설명합니다.
흔한 실수: 컬럼을 가공하면 인덱스를 못 탄다
마지막으로, 순서를 완벽히 잡아도 인덱스를 무력화하는 대표적 실수가 있습니다. WHERE 절에서 인덱스 컬럼을 함수나 연산으로 가공하는 것입니다.
-- created_at에 함수를 씌우면 인덱스를 못 탄다
WHERE DATE(created_at) = '2026-06-28'
-- 컬럼을 가공하지 말고 범위로 바꾼다
WHERE created_at >= '2026-06-28 00:00:00'
AND created_at < '2026-06-29 00:00:00'
인덱스는 컬럼의 원래 값을 기준으로 정렬돼 있습니다. DATE(created_at)처럼 함수를 씌우면 데이터베이스는 모든 행의 값을 일일이 계산해봐야 하므로 인덱스를 쓸 수 없습니다. 같은 이유로 컬럼에 산술 연산을 걸거나, 타입이 안 맞아 암묵적 형변환이 일어나는 경우에도 인덱스가 무력화됩니다. 조건은 항상 컬럼을 가공하지 않은 형태로 작성해야 합니다. 인덱스와 쿼리 작성의 관계를 더 넓게 보고 싶다면 JPA N+1 해결, Fetch Join·EntityGraph·QueryDSL 비교도 함께 참고할 만합니다.
자주 묻는 질문 (FAQ)
Q1. 컬럼마다 단일 인덱스를 여러 개 만드는 것과 복합 인덱스 하나는 뭐가 다른가요?
여러 조건이 함께 오는 쿼리라면 복합 인덱스가 유리합니다. 단일 인덱스들은 보통 하나만 주로 사용돼 나머지 조건은 필터링에 그치지만, 복합 인덱스는 조합 조건을 한 번에 좁혀줍니다. 다만 단일 조건 쿼리가 다양하다면 단일 인덱스가 나을 수도 있어, 실제 쿼리 패턴을 기준으로 정합니다.
Q2. 복합 인덱스 컬럼 순서는 선택도가 높은 것부터 두면 되나요?
선택도(카디널리티)도 참고 요소지만, 더 중요한 건 실제 쿼리에서 어떤 컬럼이 항상 등치 조건으로 들어오느냐입니다. 늘 =로 함께 조회되는 컬럼을 앞에 두고, 범위 조건 컬럼을 뒤에 두는 배치가 대체로 더 효과적입니다.
Q3. 인덱스를 걸었는데도 안 타는지 어떻게 확인하나요?
EXPLAIN으로 실행 계획을 확인하면 됩니다. type이 ALL(풀스캔)인지, key에 의도한 인덱스가 잡혔는지, rows가 얼마나 줄었는지를 봅니다. 인덱스가 있는데도 안 탄다면 컬럼 가공, 순서 불일치, 형변환 등을 의심해야 합니다.
핵심은 결국 순서다
복합 인덱스 설계는 컬럼을 많이 묶는 일이 아니라, 쿼리가 데이터를 읽는 순서대로 컬럼을 배열하는 일입니다. 최좌측 접두사 규칙에 따라 자주 등치로 들어오는 컬럼을 앞에 두고, 범위 조건은 뒤로 미루며, ORDER BY까지 그 순서에 얹으면 필터링과 정렬을 한 번에 인덱스로 해결할 수 있습니다. 반대로 순서를 쿼리와 어긋나게 잡거나 조건에서 컬럼을 가공하면, 아무리 인덱스를 많이 만들어도 풀스캔으로 되돌아갑니다. 인덱스를 새로 설계할 때는 컬럼 목록을 나열하기 전에, 이 인덱스를 쓸 쿼리들이 어떤 순서로 조건을 거는지부터 적어보는 것이 가장 확실한 출발점입니다.