메인 콘텐츠로 건너뛰기

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples on this page work on VillageSQL. Install Now →
인덱스는 MySQL에서 가장 영향력 있는 성능 도구이며 — 가장 흔히 오해받는 도구이기도 합니다. 개발자들은 쿼리가 느릴 때 인덱스를 추가하고, 디스크 공간이 부족할 때 제거하며, 인덱스가 왜 도움이 되거나 해가 되는지 모르는 경우가 많습니다. 이 가이드는 추측 대신 좋은 결정을 내릴 수 있도록 그 메커니즘을 설명합니다.

멘탈 모델

800페이지짜리 책을 생각해 보세요. “TCP/IP”가 언급된 모든 곳을 찾으려면 모든 페이지를 읽을 수 있습니다 — 그것이 전체 테이블 스캔입니다. 아니면 책 뒤의 색인을 확인해서 “TCP/IP — 42, 107, 391페이지”를 찾아 그 페이지로 바로 갈 수 있습니다. 데이터베이스 인덱스가 하는 일이 그것입니다. MySQL의 기본 인덱스 구조는 B-tree(균형 트리)입니다. 값을 정렬된 상태로 유지하여 빠른 조회(이진 탐색), 범위 스캔(BETWEEN, >, <), 정렬된 출력(ORDER BY)을 가능하게 합니다 — 모두 전체 테이블을 읽지 않고 말입니다.

MySQL이 인덱스를 사용하는 방식

쿼리를 실행하면 MySQL의 옵티마이저는 인덱스가 읽어야 할 행을 좁힐 수 있는지 확인합니다. 쿼리가 인덱스된 열로 필터링하거나 정렬할 때 인덱스를 사용합니다.

EXPLAIN: MySQL이 실제로 하는 일 보기

EXPLAIN은 MySQL이 쿼리를 어떻게 실행할 계획인지 보여줍니다. key 열은 어떤 인덱스를 사용했는지(없으면 NULL) 알려주고, rows는 검사해야 했던 행 수를 추정합니다.
type: ref는 MySQL이 인덱스 조회를 사용했다는 뜻입니다. type: ALL은 전체 테이블 스캔을 뜻합니다 — 보통 피하려고 하는 것입니다.

복합 인덱스: 열 순서가 중요합니다

복합 인덱스는 여러 열을 포함합니다. 열 순서가 인덱스를 언제 사용할 수 있는지 결정합니다.
MySQL은 이 인덱스를 다음에 사용할 수 있습니다:
  • WHERE user_id = 42 — 예, 가장 왼쪽 열
  • WHERE user_id = 42 AND status = 'shipped' — 예, 두 열 모두
  • WHERE status = 'shipped' — 아니요, 가장 왼쪽 열을 건너뜀
규칙: MySQL은 복합 인덱스를 왼쪽부터 사용합니다. 쿼리가 이득을 보려면 선행 열을 포함해야 합니다. 이것은 (user_id, status)와 (status, user_id)가 서로 다른 용도를 가진 서로 다른 인덱스라는 뜻입니다. 가장 흔한 쿼리 패턴을 중심으로 복합 인덱스를 설계하세요.

커버링 인덱스: 인덱스만으로 쿼리 처리하기

커버링 인덱스는 쿼리에 필요한 모든 열을 포함합니다. MySQL은 실제 행 데이터를 읽지 않고 인덱스만으로 쿼리에 완전히 답할 수 있습니다 — 큰 테이블에서 상당한 속도 향상입니다.
MySQL이 커버링 인덱스를 사용하면 EXPLAIN은 Extra: Using index를 보여줍니다. 행 조회가 필요하지 않습니다.

인덱스를 추가하지 말아야 할 때

인덱스가 많다고 항상 좋은 것은 아닙니다. 모든 인덱스에는 비용이 듭니다:
  • 쓰기 오버헤드 — 모든 INSERT, UPDATE, DELETE가 테이블의 모든 인덱스를 갱신해야 함
  • 저장 공간 — 쓰기가 많은 데이터에서는 인덱스가 테이블 자체만큼 커질 수 있음
  • 옵티마이저 복잡성 — 인덱스가 너무 많으면 옵티마이저가 가끔 잘못된 것을 고름
인덱스가 가장 도움이 되는 때:
  • 열의 카디널리티가 높을 때(서로 다른 값이 많을 때) — user_id, email, order_id
  • 쿼리가 테이블의 작은 부분만 필터링할 때
인덱스가 가장 도움이 되지 않는 때:
  • 낮은 카디널리티 — 가능한 값이 3개인 status, 불리언 is_active
  • 전체 스캔이 어차피 빠를 만큼 테이블이 작을 때
  • 열에 쓰기가 많고 거의 쿼리되지 않을 때

기본 키는 항상 인덱스입니다

InnoDB 테이블은 기본 키를 중심으로 구성됩니다 — 기본 키는 별도의 인덱스가 아니라 행의 물리적 저장 순서입니다. 이것은 다음을 뜻합니다:
  • 기본 키 조회는 가능한 가장 빠른 쿼리입니다
  • 기본 키 값이 순차적이면 행이 물리적으로 인접합니다
  • 무작위 기본 키(UUID v4 같은)는 삽입 시 단편화를 일으킵니다 — MySQL의 UUID를 보세요

흔한 인덱스 실수

인덱스된 열에 함수 사용하기 — 열을 함수로 감싸면 MySQL은 인덱스를 사용할 수 없습니다:
LIKE의 선행 와일드카드 — 패턴이 %로 시작하면 인덱스가 도움이 되지 않습니다:
서로 다른 열에 걸친 OR — MySQL의 옵티마이저는 OR 조건을 처리하기 위해 여러 단일 열 인덱스를 병합할 수 있을 때도 있지만, 신뢰할 수 없습니다. 큰 테이블에서는 UNION이 더 예측 가능합니다:

누락된 인덱스 찾기

슬로우 쿼리 로그는 시간 임계값을 넘는 쿼리를 기록합니다 — 무엇에 인덱스를 만들지 찾는 좋은 출발점입니다:
그다음 느린 쿼리에 EXPLAIN을 실행하세요. type: ALL과 수백만 단위의 rows를 찾으세요.

요약

  • B-tree 인덱스는 빠른 조회와 범위 스캔을 위해 값을 정렬된 상태로 유지합니다
  • MySQL은 쿼리가 선행 인덱스 열로 필터링하거나 정렬할 때 인덱스를 사용합니다
  • 복합 인덱스의 열 순서가 중요합니다 — 가장 흔한 쿼리를 중심으로 설계하세요
  • 커버링 인덱스가 가장 빠릅니다 — 쿼리가 인덱스만으로 완전히 답해집니다
  • 인덱스는 쓰기에 비용이 듭니다. 읽기 이득이 정당화하는 곳에만 추가하세요
  • 열을 함수로 감싸면 그 인덱스를 비활성화한 것입니다 — 대신 범위 조건을 사용하세요
인덱스는 MySQL의 드롭인 대체품이자 확장 기능을 갖춘 VillageSQL에서 동일하게 동작하므로, 이 전략 가이드는 변경 없이 적용됩니다.

함께 보기