바이브 코딩 시대의 데이터베이스6-3 단일 · 복합 인덱스
테크타니 LEARN — 섹션 06 · 인덱스

단일 · 복합 인덱스 —
컬럼 순서가 중요한 이유

사진관 직원은 사진을 먼저 고객별로 나누고, 같은 고객의 사진은 촬영 날짜순으로 다시 정리합니다. 복합 인덱스가 정확히 이 모양이에요 — 여러 기준을 순서대로 적용한 하나의 구조입니다. 오늘은 그 순서가 왜 그렇게 중요한지를 봅니다.

난이도보통소요9분준비물6-2 완료오늘왼쪽 컬럼부터

이번 섹션의 여정

6-1 인덱스의 본질

완료 ✓

6-2 언제 만들까

완료 ✓

6-3 단일 · 복합 인덱스

지금 여기

6-4 ~ 6-5

EXPLAIN · 비용과 함정

사진관의 보관함 — 두 번 정리한 하나의 구조

사진관에 고객 사진이 수천 장 쌓여 있다고 해볼게요.

직원은 먼저 고객별로 보관함을 나눕니다. 그리고 같은 고객의 사진은 촬영 날짜순으로 정리해 둡니다. 그러면 철수의 최근 사진을 찾을 때 철수 보관함으로 간 뒤 날짜만 따라가면 되죠.

정리 기준이 두 개 — 순서가 있습니다
사진관 보관함 복합 인덱스
─────────────────────────── ───────────────────────────
철수 보관함 → 촬영 날짜순 회원 1번 묶음 → 주문일순
영희 보관함 → 촬영 날짜순 회원 2번 묶음 → 주문일순
 
고객으로 크게 나누고 — 그 안에서 날짜로 다시 정리한다

여기서 중요한 건 보관함이 두 개의 서로 다른 정리 체계가 아니라는 점입니다. 고객으로 나눈 뒤 그 안을 날짜로 정리한 하나의 체계예요. 복합 인덱스도 정확히 이렇게 동작합니다.

단일 인덱스와 복합 인덱스

기준이 컬럼 하나뿐이면 단일 인덱스입니다.

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)회원 번호 인덱스와 주문일 인덱스를 따로 만든 것이 아닙니다. 두 컬럼을 정해진 순서로 묶은 하나의 인덱스예요.

⭐ 3-4 에서 이미 만난 구조

기본키는 하나이고 그것을 구성하는 컬럼이 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;
DBeaver — SQL Editor (Ctrl + Enter)
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_idmember_idordered_at
3412026-02-15
2712025-12-10
2012025-10-12
3 rows fetched — 김철수의 주문 6건 중 10월 이후 3건

6-2 에서 인덱스 후보를 찾는 자리로 WHERE · JOIN · ORDER BY 세 곳을 봤죠. 이 조회에는 그중 WHEREORDER BY 가 들어 있는데 — WHERE 가 두 갈래입니다.

한 SQL 안의 세 자리
자리이 조회에서역할
WHERE 등호member_id = 1값이 하나로 딱 정해진다
WHERE 범위ordered_at >= '2025-10-01'한 구간을 훑는다
ORDER BYordered_at DESC주문일순으로 줄을 세운다
회원으로 좁히고 · 날짜로 자르고 · 날짜로 정렬 — 두 컬럼이 함께 움직입니다

두 컬럼이 늘 함께 쓰이는데 정리 기준을 하나만 두는 건 아깝죠. 그래서 이 조회에 맞춘 구조를 생각합니다 — 회원 번호를 먼저, 주문일을 두 번째로.

INDEX (member_id, ordered_at) 이 정리해 둔 순서 — 실제 shop 데이터
member_id │ ordered_at │ order_id
───────────┼──────────────┼──────────
1 │ 2025-02-01 │ 1
1 │ 2025-04-10 │ 6
1 │ 2025-08-01 │ 14
1 │ 2025-10-12 │ 20 ◀ 여기부터 아래가 찾는 구간
1 │ 2025-12-10 │ 27
1 │ 2026-02-15 │ 34
2 │ 2025-02-15 │ 2
2 │ 2025-05-05 │ 8
⋮ │ ⋮ │ ⋮

회원 1번 구간으로 한 번에 가고, 그 안은 이미 주문일순이니 10월 1일 지점부터 아래쪽이 우리가 찾는 주문 전부입니다. 최신순으로 달라고 했으니 그 구간을 아래에서 위로 거슬러 읽으면 되고요 — 이미 날짜순으로 정리돼 있어서 줄을 다시 세울 일이 없습니다. 찾기와 정렬이 한 구조 안에서 같이 해결되는 것, 이게 복합 인덱스를 쓰는 이유예요.

왼쪽 컬럼부터 — 첫 기준이 없으면 흩어집니다

INDEX (member_id, ordered_at) 에서 첫 번째 기준은 회원 번호입니다. 그래서 —

이 인덱스를 따라갈 수 있는가
조회 조건첫 기준이 있는가결과
회원 번호만있다✓ 첫 기준부터 따라갈 수 있다
회원 번호 + 주문일있다✓ 첫 기준 → 두 번째 기준 순서로
주문일만없다△ 한 구간으로 바로 가기 어렵다
첫 번째 기준이 있느냐 — 이것이 갈림길입니다

주문일만으로 찾는 경우가 왜 어려운지, 실제 shop 데이터로 보겠습니다. 2025년 10월에 들어온 주문은 세 건인데 — 회원이 전부 다릅니다.

shop DB — 2025년 10월 주문 세 건
ordered_atorder_idmember_id위 인덱스에서의 위치
2025-10-12201회원 1번 묶음 안
2025-10-202112회원 12번 묶음 안
2025-10-302213회원 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 의 기본키죠.

SHOW INDEX FROM order_items; (일부 컬럼만)
SHOW INDEX FROM order_items;
Key_nameSeq_in_indexColumn_name
PRIMARY1order_id
PRIMARY2product_id
fk_oi_product1product_id
PRIMARY 가 두 줄 — 하나의 인덱스를 두 컬럼이 순서대로 구성한다는 표시입니다

Key_name 이 이름, Seq_in_index그 인덱스 안에서 몇 번째 기준인지를 알려줍니다. PRIMARY 라는 이름이 두 번 나오지만 인덱스는 하나예요 — 1번 기준이 order_id, 2번 기준이 product_id 입니다. 앞에서 말한 “인덱스는 하나, 구성 컬럼은 N개” 가 화면에 그대로 찍혀 있는 셈이죠.

왼쪽 컬럼 우선이 실제로 작동하는 자리

세 번째 줄을 보세요. product_id 에는 별도 인덱스가 있는데 order_id 에는 없습니다. order_id 는 이미 PK 인덱스의 첫 번째 기준이라 그대로 쓸 수 있고, product_id 는 두 번째 기준이라 혼자서는 곧바른 길이 아니어서 — 외래키가 자기 인덱스를 따로 만든 겁니다.

이번엔 orders 를 보겠습니다.

SHOW INDEX FROM orders; (일부 컬럼만)
SHOW INDEX FROM orders;
Key_nameSeq_in_indexColumn_name
PRIMARY1order_id
fk_orders_member1member_id
member_id 에는 단일 인덱스가 이미 있고 — ordered_at 은 어디에도 없습니다
이 인덱스는 누가 만들었을까

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);
SHOW INDEX FROM orders; — 만든 뒤
SHOW INDEX FROM orders;
Key_nameSeq_in_indexColumn_name
PRIMARY1order_id
fk_orders_member1member_id
idx_orders_member_ordered1member_id
idx_orders_member_ordered2ordered_at
새 인덱스 이름이 두 줄 — 1번 기준 member_id, 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 에서 실행 계획으로 직접 들여다봅니다.

NEXT LESSON여기까지 — 6-4 부터는 순차 공개됩니다

6-4 EXPLAIN 으로 실행 계획 읽기, 6-5 인덱스의 비용과 함정이 이어집니다. 강의 목차에서 다른 섹션도 둘러보세요.

GO ▸ 강의 목차 보기