단일 · 복합 인덱스 —
컬럼 순서가 중요한 이유
사진관 직원은 사진을 먼저 고객별로 나누고, 같은 고객의 사진은 촬영 날짜순으로 다시 정리합니다. 복합 인덱스가 정확히 이 모양이에요 — 여러 기준을 순서대로 적용한 하나의 구조입니다. 오늘은 그 순서가 왜 그렇게 중요한지를 봅니다.
이번 섹션의 여정
완료 ✓
완료 ✓
지금 여기
EXPLAIN · 비용과 함정
사진관의 보관함 — 두 번 정리한 하나의 구조
사진관에 고객 사진이 수천 장 쌓여 있다고 해볼게요.
직원은 먼저 고객별로 보관함을 나눕니다. 그리고 같은 고객의 사진은 촬영 날짜순으로 정리해 둡니다. 그러면 철수의 최근 사진을 찾을 때 철수 보관함으로 간 뒤 날짜만 따라가면 되죠.
여기서 중요한 건 보관함이 두 개의 서로 다른 정리 체계가 아니라는 점입니다. 고객으로 나눈 뒤 그 안을 날짜로 정리한 하나의 체계예요. 복합 인덱스도 정확히 이렇게 동작합니다.
단일 인덱스와 복합 인덱스
기준이 컬럼 하나뿐이면 단일 인덱스입니다.
INDEX (member_id)“회원 번호 하나를 기준으로 정리한 인덱스” 라고 읽습니다.
기준이 두 개 이상이고 그 사이에 순서가 있으면 복합 인덱스입니다.
INDEX (member_id, ordered_at)“회원 번호를 먼저 정리하고, 같은 회원 안에서 주문일순으로 다시 정리한 인덱스” 라고 읽어요. 괄호와 쉼표를 그대로 읽지 말고 — 먼저 / 그 안에서 라는 말로 바꿔 읽는 습관을 들이면 헷갈릴 일이 거의 없습니다.
| 적는 방법 | 뜻 | 인덱스 개수 |
|---|---|---|
| INDEX (member_id) | 회원 번호 하나로 정리 | 1개 |
| INDEX (ordered_at) | 주문일 하나로 정리 | 1개 |
| INDEX (member_id, ordered_at) | 회원 번호로 먼저, 그 안에서 주문일로 | 1개 |
복합 인덱스는 “인덱스 두 개”가 아닙니다
이 강의에서 가장 자주 나오는 오해를 먼저 지우고 갈게요.
INDEX (member_id, ordered_at) 은 회원 번호 인덱스와 주문일 인덱스를 따로 만든 것이 아닙니다. 두 컬럼을 정해진 순서로 묶은 하나의 인덱스예요.
기본키는 하나이고 그것을 구성하는 컬럼이 N개일 수 있다 — 3-4 에서 복합키를 이렇게 정리했죠. 복합 인덱스도 똑같습니다. 인덱스는 하나, 그 하나를 구성하는 컬럼이 N개입니다.
숫자를 세는 위치가 다를 뿐입니다. 구성 컬럼을 세면 2개, 인덱스를 세면 1개예요. 이걸 헷갈리면 “복합 인덱스를 만들었으니 두 번째 컬럼만으로도 잘 찾겠지” 라는 잘못된 기대가 바로 따라옵니다. 뒤에서 그 이유를 보겠습니다.
회원 한 명의 최근 주문 찾기
고객센터에서 이런 요청이 들어왔다고 해볼게요 — “회원 1번의 2025년 10월 이후 주문을 최신순으로 보여주세요.”
SELECT order_id, member_id, ordered_at
FROM orders
WHERE member_id = 1
AND ordered_at >= '2025-10-01'
ORDER BY ordered_at DESC;| order_id | member_id | ordered_at |
|---|---|---|
| 34 | 1 | 2026-02-15 |
| 27 | 1 | 2025-12-10 |
| 20 | 1 | 2025-10-12 |
6-2 에서 인덱스 후보를 찾는 자리로 WHERE · JOIN · ORDER BY 세 곳을 봤죠. 이 조회에는 그중 WHERE 와 ORDER BY 가 들어 있는데 — WHERE 가 두 갈래입니다.
| 자리 | 이 조회에서 | 역할 |
|---|---|---|
| WHERE 등호 | member_id = 1 | 값이 하나로 딱 정해진다 |
| WHERE 범위 | ordered_at >= '2025-10-01' | 한 구간을 훑는다 |
| ORDER BY | ordered_at DESC | 주문일순으로 줄을 세운다 |
두 컬럼이 늘 함께 쓰이는데 정리 기준을 하나만 두는 건 아깝죠. 그래서 이 조회에 맞춘 구조를 생각합니다 — 회원 번호를 먼저, 주문일을 두 번째로.
회원 1번 구간으로 한 번에 가고, 그 안은 이미 주문일순이니 10월 1일 지점부터 아래쪽이 우리가 찾는 주문 전부입니다. 최신순으로 달라고 했으니 그 구간을 아래에서 위로 거슬러 읽으면 되고요 — 이미 날짜순으로 정리돼 있어서 줄을 다시 세울 일이 없습니다. 찾기와 정렬이 한 구조 안에서 같이 해결되는 것, 이게 복합 인덱스를 쓰는 이유예요.
왼쪽 컬럼부터 — 첫 기준이 없으면 흩어집니다
INDEX (member_id, ordered_at) 에서 첫 번째 기준은 회원 번호입니다. 그래서 —
| 조회 조건 | 첫 기준이 있는가 | 결과 |
|---|---|---|
| 회원 번호만 | 있다 | ✓ 첫 기준부터 따라갈 수 있다 |
| 회원 번호 + 주문일 | 있다 | ✓ 첫 기준 → 두 번째 기준 순서로 |
| 주문일만 | 없다 | △ 한 구간으로 바로 가기 어렵다 |
주문일만으로 찾는 경우가 왜 어려운지, 실제 shop 데이터로 보겠습니다. 2025년 10월에 들어온 주문은 세 건인데 — 회원이 전부 다릅니다.
| ordered_at | order_id | member_id | 위 인덱스에서의 위치 |
|---|---|---|---|
| 2025-10-12 | 20 | 1 | 회원 1번 묶음 안 |
| 2025-10-20 | 21 | 12 | 회원 12번 묶음 안 |
| 2025-10-30 | 22 | 13 | 회원 13번 묶음 안 |
철수 보관함에도 10월 사진이 있고, 영희 보관함에도 10월 사진이 있는 것과 같아요. 날짜는 고객별 묶음 안에 흩어져 있어서, 고객을 모르면 한 곳으로 바로 갈 수가 없습니다.
주문일만 있는 조건에서 이 인덱스를 절대 사용할 수 없다는 뜻은 아니에요. 데이터베이스가 인덱스 전체를 훑는 방식을 고를 때도 있습니다. 다만 주문일만 빠르게 찾으라고 만든 구조는 아니라는 뜻입니다. 실제로 어떤 길을 고르는지는 6-4 의 실행 계획에서 직접 확인합니다.
여기서 한 가지 더 못 박고 가겠습니다.
왼쪽 컬럼부터 중요하다는 말의 “왼쪽”은 WHERE 절에 조건을 적은 순서가 아닙니다. 인덱스를 정의할 때 먼저 적은 컬럼을 뜻합니다.
WHERE ordered_at >= '2025-10-01' AND member_id = 1 처럼 조건 순서를 바꿔 적어도 결과도 같고, 인덱스를 쓰는 방식도 달라지지 않습니다. 순서를 정하는 건 SQL 을 적은 순서가 아니라 인덱스를 만든 순서예요.
단일 인덱스 두 개 vs 복합 인덱스 하나
“그러면 member_id 인덱스, ordered_at 인덱스를 각각 만들면 되지 않나요?” — 자연스러운 질문입니다. 두 구조를 나란히 놓고 보겠습니다.
회원 1번의 10월 이후 주문을 최신순으로 찾을 때
정리 기준하나는 회원 번호로만, 하나는 주문일로만 줄을 세운다
없는 정보”회원 안에서 날짜순” 이라는 순서는 어디에도 없다
정렬회원으로 좁힌 뒤 날짜 정렬을 따로 해야 할 수 있다
비용인덱스가 두 개 — 저장 공간과 쓰기 부담도 두 몫
정리 기준회원으로 묶고, 그 묶음 안이 이미 주문일순이다
찾기회원 1번 구간으로 간 뒤 10월 지점부터 아래로 읽으면 끝
정렬이미 날짜순이라 별도 정렬 작업을 줄일 수 있다
덤회원 번호만으로 찾을 때도 첫 기준이라 그대로 쓸 수 있다
두 컬럼이 함께 쓰이는 조회가 잦다면 — 인덱스를 두 개 만드는 것보다 순서를 가진 하나가 그 조회에 더 곧바른 길입니다.
MySQL 이 여러 인덱스를 함께 활용하는 방식(인덱스 병합)이 존재하긴 합니다. 다만 회원으로 좁히고 그 안에서 날짜순으로 읽는 이 작업에는 복합 인덱스 하나가 더 단순하고 곧은 길이에요.
순서는 SQL 이 정합니다
그렇다고 member_id 를 항상 앞에 둬야 하는 건 아닙니다. 정답 공식이 있는 게 아니라, 우리 서비스가 무엇을 먼저 찾는지가 순서를 정해요.
| 자주 실행하는 조회 | 알맞은 컬럼 순서 | 이유 |
|---|---|---|
| 회원 한 명의 주문을 최신순으로 | (member_id, ordered_at) | 회원으로 크게 묶고 그 안에서 날짜순 |
| 전체 주문을 기간으로 훑을 때 | (ordered_at, member_id) | 날짜가 한 구간에 모여 있어야 한다 |
순서를 정할 때는 이 세 가지만 물어보면 됩니다.
앞선 예에서 member_id = 1 은 값이 하나로 정해지는 등호 조건이고, ordered_at 은 범위이자 정렬 기준이었죠. 그래서 (member_id, ordered_at) 이 나온 겁니다. 컬럼의 중요도 순서가 아니라 — 조회가 좁혀 들어가는 순서입니다.
shop 에는 이미 복합 인덱스가 하나 있습니다
새로 만들기 전에, 우리 shop 에 이미 있는 복합 인덱스를 하나 보겠습니다. 3-4 에서 본 order_items 의 기본키죠.
| Key_name | Seq_in_index | Column_name |
|---|---|---|
| PRIMARY | 1 | order_id |
| PRIMARY | 2 | product_id |
| fk_oi_product | 1 | product_id |
Key_name 이 이름, Seq_in_index 가 그 인덱스 안에서 몇 번째 기준인지를 알려줍니다. PRIMARY 라는 이름이 두 번 나오지만 인덱스는 하나예요 — 1번 기준이 order_id, 2번 기준이 product_id 입니다. 앞에서 말한 “인덱스는 하나, 구성 컬럼은 N개” 가 화면에 그대로 찍혀 있는 셈이죠.
세 번째 줄을 보세요. product_id 에는 별도 인덱스가 있는데 order_id 에는
없습니다. order_id 는 이미 PK 인덱스의 첫 번째 기준이라 그대로 쓸 수
있고, product_id 는 두 번째 기준이라 혼자서는 곧바른 길이 아니어서 —
외래키가 자기 인덱스를 따로 만든 겁니다.
이번엔 orders 를 보겠습니다.
| Key_name | Seq_in_index | Column_name |
|---|---|---|
| PRIMARY | 1 | order_id |
| fk_orders_member | 1 | member_id |
init.sql 어디에도 CREATE INDEX 는 없습니다. 하지만 InnoDB 는 외래키를
만들 때 쓸 인덱스가 없으면 자식 컬럼에 인덱스를 자동으로 만들어요. 그래서
orders.member_id, products.category_id, order_items.product_id 에는 이미
인덱스가 있습니다. “내가 안 만들었으니 인덱스가 없다” 고 단정하면 안 되는
이유예요.
그러니 우리에게 없는 건 회원 번호로 묶고 그 안에서 주문일순으로 정리한 구조입니다. 만들어 볼게요.
CREATE INDEX idx_orders_member_ordered
ON orders (member_id, ordered_at);| Key_name | Seq_in_index | Column_name |
|---|---|---|
| PRIMARY | 1 | order_id |
| fk_orders_member | 1 | member_id |
| idx_orders_member_ordered | 1 | member_id |
| idx_orders_member_ordered | 2 | ordered_at |
여기서 눈치채셨을지 모르겠어요. 새로 만든 복합 인덱스는 첫 기준이 member_id 라서 — 회원 번호만으로 찾을 때도 쓸 수 있습니다. 그러면 기존 fk_orders_member 단일 인덱스와 역할이 상당 부분 겹치죠.
인덱스는 만들어 두기만 하면 되는 게 아니라 데이터를 넣고 고칠 때마다 함께 갱신되는 구조물입니다. 역할이 겹치는 인덱스를 그대로 두면 그만큼 비용만 늘어요. 무엇을 남기고 무엇을 정리할지는 6-5 에서 본격적으로 다룹니다.
한눈에 정리
| 개념 | 뜻 | shop 에서는 |
|---|---|---|
| 단일 인덱스 | 컬럼 하나를 기준으로 정리한 구조 | orders.member_id (FK 가 자동 생성) |
| 복합 인덱스 | 컬럼 여러 개를 정해진 순서로 묶은 하나의 인덱스 | order_items 의 PK (order_id, product_id) |
| 왼쪽 컬럼 우선 | 인덱스에서 먼저 적은 컬럼부터 따라간다 | order_id 만으로도 PK 인덱스를 쓸 수 있다 |
| 첫 기준이 없으면 | 값이 여러 묶음에 흩어져 한 구간으로 바로 가기 어렵다 | product_id 만으로는 PK 인덱스가 곧바른 길이 아님 |
| 순서를 정하는 법 | 자주 실행하는 조회가 결정 — 등호는 앞, 범위·정렬은 뒤 | 회원별 주문 조회 → (member_id, ordered_at) |
오늘의 핵심은 한 문장입니다 — 복합 인덱스는 첫 번째 컬럼으로 크게 묶고, 같은 묶음 안에서 두 번째 컬럼으로 다시 정리한 하나의 인덱스입니다. 인덱스가 두 개가 아니고, 컬럼 순서는 중요도가 아니라 우리가 자주 실행하는 조회가 정합니다.
여기까지는 “이런 구조를 만들면 좋겠다” 는 우리의 계획이었어요. 그런데 데이터베이스가 실제로 그 인덱스를 골랐는지는 어떻게 확인할까요? 다음 6-4 에서 실행 계획으로 직접 들여다봅니다.