언제 인덱스를 만들까 —
WHERE · JOIN · ORDER BY 세 단서
6-1에서 인덱스가 무엇인지는 봤습니다. 그런데 책의 모든 단어를 찾아보기에 넣지는 않죠. 그럼 어떤 단어를 고를까요? 답은 컬럼 이름이 아니라 실제로 자주 실행하는 SQL 안에 있습니다.
이번 섹션의 여정
완료 ✓
지금 여기
컬럼 순서
EXPLAIN · 비용과 함정
찾아보기에 넣을 단어는 어디서 고를까
6-1에서 인덱스를 두꺼운 책 맨 뒤의 찾아보기에 비유했습니다. 그리고 마지막에 한 마디를 남겼죠 — 모든 단어를 찾아보기에 넣지는 않는다고요.
그럼 어떤 단어를 골라야 할까요? 책 본문의 목차를 다시 읽어봐야 답이 나오는 게 아닙니다. 사람들이 실제로 무엇을 찾는지를 봐야 해요.
데이터베이스도 똑같습니다. 인덱스 후보는 컬럼 이름과 중요도를 보고 고르는 것이 아니라 — 실제로 자주 실행하는 SQL 에서 찾습니다.
| 보는 곳 | 판단 |
|---|---|
| 컬럼 이름과 중요도 | ✕ 중요한 정보라고 자주 찾는 것은 아니다 |
| 테이블 정의서 | ✕ 구조는 알려주지만 사용 습관은 알려주지 않는다 |
| 자주 실행하는 SQL | ✓ WHERE · JOIN · ORDER BY — 세 자리를 본다 |
인덱스는 중요한 컬럼에 무조건 만드는 것이 아니라 — 자주 찾고, 연결하고, 정렬하는 길 가운데 확인하거나 정렬할 일을 많이 줄여 주는 곳에 만듭니다.
시작 전에 — shop 에는 이미 인덱스가 있습니다
후보를 고르기 전에 6-1에서 확인한 것을 한 번 더 짚고 갑니다. 우리 init.sql 에는 CREATE INDEX 도, INDEX 선언도 한 줄도 없습니다. 그런데 인덱스가 하나도 없는 건 아니에요. 기본키와 외래키가 이미 인덱스를 깔아 뒀고, 여기에 6-1에서 products.product_name 에 하나를 직접 만들었죠.
| 테이블 | 인덱스가 있는 컬럼 | 어디서 생겼나 |
|---|---|---|
| members | member_id | 기본키 |
| categories | category_id | 기본키 |
| products | product_id / category_id / product_name | 기본키 / 외래키를 만들 때 자동 생성 / 6-1 에서 직접 생성 |
| orders | order_id / member_id | 기본키 / 외래키를 만들 때 자동 생성 |
| order_items | (order_id, product_id) / product_id | 복합 기본키 / 외래키를 만들 때 자동 생성 |
다른 데이터베이스를 기준으로 쓴 글에서 자주 보이는 조언이지만,
MySQL(InnoDB)에서는 외래키 제약을 만들 때 쓸 인덱스가 없으면 자식 컬럼에
인덱스가 자동으로 생깁니다. orders.member_id, products.category_id,
order_items.product_id 는 그래서 이미 인덱스를 갖고 있어요. 같은 컬럼에
인덱스를 하나 더 만들면 — 얻는 것 없이 저장 공간과 관리 비용만 늘어납니다.
order_items 의 기본키는 (order_id, product_id) 복합키입니다. order_id 가
맨 왼쪽 컬럼이라 기본키 인덱스가 그대로 커버해요. 그래서 따로 인덱스를
만들지 않습니다. 맨 왼쪽 컬럼이 왜 특별한지는 6-3 에서 다룹니다.
그러니까 오늘 후보를 고를 무대는 — 아직 인덱스가 없는 컬럼들입니다.
| 테이블 | 인덱스가 없는 컬럼 |
|---|---|
| members | member_name · gender · age · joined_at |
| categories | category_name |
| products | price |
| orders | ordered_at |
| order_items | quantity |
첫 번째 단서 — WHERE, 찾는 기준
WHERE 는 많은 행 중에서 원하는 것만 골라내는 자리입니다. 5-2 에서 내내 썼던 그 절이죠.
-- 5-2 에서 실행했던 쿼리
SELECT
*
FROM
members
WHERE
age >= 30;회원 20명 중 12명이 통과했습니다. 여기서 검색 기준으로 쓰인 컬럼은 age 하나예요. 이런 조회를 매일 한다면 members.age 가 후보로 올라옵니다.
주문 쪽은 더 현실적입니다. 실무에서 하루에도 몇 번씩 나오는 질문 — “올해 주문만 보여줘.”
SELECT
*
FROM
orders
WHERE
ordered_at BETWEEN '2026-01-01' AND '2026-12-31';주문 40건 중 11건이 남습니다. 주문일이 계속 검색 기준으로 쓰이니 orders.ordered_at 이 후보예요.
| 자주 실행하는 질문 | WHERE 에 쓰이는 컬럼 | 지금 인덱스 |
|---|---|---|
| 30세 이상 회원만 보고 싶다 | members.age | 없음 → 후보 |
| 올해 주문만 보고 싶다 | orders.ordered_at | 없음 → 후보 |
| 5만 원 이상인 상품만 보고 싶다 | products.price | 없음 → 후보 |
| 김철수 회원의 주문만 보고 싶다 | orders.member_id | 있음 (외래키 자동) |
| 상품 이름으로 찾고 싶다 | products.product_name | 있음 (6-1 에서 직접 생성) |
다만 SQL 에 한 번 등장했다고 바로 후보가 되는 것은 아닙니다. 두 가지가 같이 성립해야 해요.
같은 WHERE 조건인데 왜 하나만 후보일까
어쩌다 한 번연말에만 돌려보는 조회
거의 안 줄어든다조건을 걸어도 대부분의 행이 그대로 남는다
결과찾아보기를 펴는 수고가 그냥 넘겨보는 것보다 클 수 있다
매일 수백 번같은 형태의 조건이 반복해서 실행된다
많이 줄어든다조건 하나로 확인할 행이 크게 줄어든다
결과확인할 일이 줄어든 만큼 이득이 남는다
인덱스는 자주 실행되고, 확인할 일을 많이 줄여 주는 조건에서 값을 합니다.
두 번째 단서 — JOIN, 연결하는 기준
두 번째 단서는 JOIN 입니다. 조인은 서로 다른 두 테이블을 연결하죠. 5-6 에서 회원표와 주문표를 이었던 쿼리입니다.
SELECT
m.member_name,
o.order_id,
o.ordered_at
FROM
members m
INNER JOIN orders o
ON m.member_id = o.member_id;ON 뒤에 적힌 두 컬럼이 연결의 기준입니다. 데이터베이스는 한쪽 행마다 반대쪽에서 짝을 찾아야 하니, 연결에 쓰는 컬럼은 자연스럽게 인덱스 후보가 돼요.
그런데 여기서 중요한 말은 후보입니다. 조인에 보인다고 무조건 새 인덱스를 만든다는 뜻은 아니에요. 5-6 의 4테이블 체인으로 확인해 보겠습니다.
-- 회원 → 주문 → 주문상세 → 상품
SELECT
m.member_name,
o.ordered_at,
p.product_name,
oi.quantity
FROM
members m
JOIN orders o
ON m.member_id = o.member_id
JOIN order_items oi
ON o.order_id = oi.order_id
JOIN products p
ON oi.product_id = p.product_id;| ON 조건 | 왼쪽 컬럼 | 오른쪽 컬럼 |
|---|---|---|
| m.member_id = o.member_id | members 기본키 | orders 외래키 자동 인덱스 |
| o.order_id = oi.order_id | orders 기본키 | order_items 복합 기본키의 맨 왼쪽 |
| oi.product_id = p.product_id | order_items 외래키 자동 인덱스 | products 기본키 |
JOIN 은 후보 자리를 알려주지만, MySQL 에서는 그 자리가 이미 채워져 있는 경우가 많습니다.
그래서 순서는 언제나 — 만들기 전에 이미 있는지부터 확인.
외래키 제약 없이 두 테이블을 이어 쓰는 설계에서는 이야기가 완전히 달라져요.
연결에 쓰는 컬럼에 아무 인덱스도 없다면, 짝을 찾을 때마다 반대쪽 테이블을 훑게
됩니다. 그럴 때는 ON 에 적힌 컬럼이 1순위 후보예요.
세 번째 단서 — ORDER BY와 LIMIT, 정렬하는 기준
세 번째 단서는 ORDER BY 입니다. 5-3 에서 만든 그 패턴이죠.
-- 최근 가입자 5명
SELECT
*
FROM
members
ORDER BY
joined_at DESC
LIMIT 5;회원 20명을 가입일 순으로 줄 세운 뒤 위에서 5명만 가져옵니다. 결과는 5행인데 — 줄을 세우는 일은 20명 전체에 대해 일어납니다. 결과 행 수와 실제로 한 일이 다르다는 건 6-1에서 이미 본 이야기예요.
이게 ORDER BY 가 단서가 되는 이유입니다. 인덱스는 값이 정렬된 상태로 정리되어 있어요. 가입일 순서로 정리된 인덱스를 따라갈 수 있다면, 데이터베이스는 앞에서 5개만 읽고 멈출 수도 있습니다. 별도로 정렬하는 일을 줄일 수 있는 거죠.
주문 쪽도 모양이 같습니다.
-- 최근 주문 10건
SELECT
*
FROM
orders
ORDER BY
ordered_at DESC
LIMIT 10;주문 40건을 줄 세워 10건만 가져옵니다. orders.ordered_at 이 여기서 또 나왔죠 — WHERE 에서 한 번, ORDER BY 에서 또 한 번. 같은 컬럼이 두 단서에 겹쳐 나오면 후보 순위가 올라갑니다.
| 쿼리 | 실제로 하는 일 | 인덱스로 줄어드는 정도 |
|---|---|---|
| ORDER BY joined_at DESC LIMIT 5 | 앞에서 5행만 읽고 멈출 수 있다 | 큼 |
| ORDER BY joined_at DESC (LIMIT 없음) | 어차피 20행 전부 결과로 나간다 | 작음 |
인덱스로 정렬을 건너뛸 수 있는지는 정렬 컬럼, 정렬 방향, 앞에 붙은 다른 조건에
따라 달라집니다. 데이터베이스가 실제로 어떤 길을 골랐는지는 짐작하지 말고
확인해야 해요. 그 도구가 EXPLAIN 이고, 읽는 법은 6-4 에서 다룹니다.
반대로 — 효과가 작을 수 있는 세 가지 경우
단서를 찾는 눈만큼 중요한 게 우선순위를 낮추는 눈입니다. 세 가지 상황을 보겠습니다.
첫째, 찾는 조건으로 거의 쓰지 않는 컬럼. order_items.quantity 를 생각해 보세요. 수량은 분명 중요한 정보입니다. 매출을 계산하려면 반드시 필요하죠. 그런데 “수량이 2인 주문상세만 찾아줘” 같은 조회를 매일 하나요? 거의 안 합니다. 중요한 정보인 것과, 찾는 기준으로 자주 쓰는 것은 다른 이야기예요.
둘째, 데이터가 아주 적은 테이블. categories 는 다섯 행입니다. 5쪽짜리 책에 찾아보기를 붙이는 셈이에요. 그냥 처음부터 읽어도 부담이 거의 없습니다.
회원 20 · 상품 30 · 주문 40 · 주문상세 64. 이 크기에서는 어떤 인덱스를 만들어도 체감 차이를 만들기 어렵고, 6-1에서 본 것처럼 데이터베이스가 인덱스를 만들어 놓고도 전체 스캔을 고를 수 있어요. 그래서 이 강의에서 얻어 갈 것은 지금 만들 인덱스가 아니라 — 데이터가 수십만 행이 됐을 때 어디를 볼지 아는 눈입니다.
셋째, 조건을 걸어도 결과가 거의 줄지 않는 경우. members.gender 는 값이 'M' 과 'F' 둘뿐입니다. 우리 shop 은 남성 10명, 여성 10명 — 정확히 반씩이에요.
| 조건 | 남는 행 | 전체 대비 |
|---|---|---|
| gender = 'F' | 10 | 20명 중 절반 |
| gender = 'M' | 10 | 20명 중 절반 |
값의 종류가 적으면 조건 하나로 걸러도 많은 행이 그대로 남습니다. 인덱스를 거친 다음 결국 그 많은 행을 다시 확인해야 하니, 이득이 작아지는 거예요.
값의 종류가 적은 컬럼도 다른 컬럼과 함께 묶으면 쓸모가 생길 수 있습니다.
예를 들어 “여성 회원을 가입일 최신순으로” 같은 요구가 매일 반복된다면
gender 와 joined_at 을 하나로 묶는 선택지가 생겨요. 여러 컬럼을 묶는 방법은
바로 다음 6-3 에서 다룹니다. 오늘은 “좋은 후보인지 한 번 더 생각해 보라”
정도로만 기억하세요.
후보를 만나면 물어볼 세 가지 질문
여기까지를 세 질문으로 압축할 수 있습니다.
셋 다 “예”라면 만들 이유가 분명합니다. 하나라도 “아니오”라면 — 만들지 말라는 뜻이 아니라 한 번 더 생각해 보라는 신호예요.
그리고 그 후보는 SQL 의 세 자리에서 찾습니다.
| SQL 의 자리 | 알려주는 것 | shop 에서 본 예 |
|---|---|---|
| WHERE | 찾는 기준 | orders.ordered_at · members.age |
| JOIN ... ON | 연결하는 기준 | m.member_id = o.member_id (이미 인덱스 있음) |
| ORDER BY (+ LIMIT) | 정렬하는 기준 | members.joined_at · orders.ordered_at |
SQL 에서 세 단서를 찾고 → 세 질문으로 거르고 → 이미 있는지 확인한다. 컬럼 이름만 보고 “이건 중요하니까 인덱스” 라고 결정하는 순간부터 잘못된 인덱스가 쌓이기 시작합니다.
한눈에 정리
| 구분 | 무엇을 보나 | shop 에서는 |
|---|---|---|
| 단서 ① WHERE | 자주 거르는 컬럼 | orders.ordered_at · members.age · products.price |
| 단서 ② JOIN … ON | 연결에 쓰는 컬럼 | 기본키 · 외래키 자동 인덱스가 이미 전부 커버 |
| 단서 ③ ORDER BY + LIMIT | 정렬해서 앞 몇 개만 뽑는 컬럼 | members.joined_at · orders.ordered_at |
| 효과가 작을 때 | 안 찾는 컬럼 · 작은 테이블 · 거의 안 줄어드는 조건 | order_items.quantity · categories(5행) · members.gender |
| 세 가지 질문 | 많은가 · 자주인가 · 많이 줄어드는가 | 셋 다 “예”일 때 만들 이유가 분명해진다 |
인덱스 후보는 컬럼의 중요도가 아니라 사용 습관에서 나옵니다. 자주 찾고, 자주 연결하고, 자주 정렬하는 컬럼 — 그리고 그 일을 실제로 많이 줄여 줄 수 있는 자리. 우리 shop 처럼 작은 데이터베이스에서는 이미 있는 인덱스만으로 충분하다는 것도 오늘 함께 확인했습니다.
그런데 조건에 컬럼이 하나만 나오는 경우는 드물죠. “여성 회원을 가입일 최신순으로” 처럼 두 컬럼이 함께 등장하면 — 인덱스를 두 개 만들까요, 하나로 묶을까요? 묶는다면 어느 컬럼을 앞에 둘까요?