콘텐츠로 건너뛰기

DB에 인덱스를 걸면 무조건 빨라진다고?

데이터베이스를 공부하다 보면 성능 최적화와 관련해 가장 먼저 접하는 기술이 인덱스(Index)다.

쿼리가 느리다면 인덱스를 추가하라는 조언도 흔히 듣는다. 실제로 인덱스를 적용했더니 몇 초 걸리던 조회가 수십 밀리초 만에 끝나는 경우도 있다.

그런데 여기서 의문이 생겼다.

인덱스가 그렇게 좋은 기술이라면 모든 컬럼에 인덱스를 생성하면 되지 않을까?

오히려 조회 성능이 저하되거나 데이터 수정 속도가 느려질 수 있다.

이를 이해하려면 데이터베이스가 데이터를 찾는 방식부터 살펴볼 필요가 있다.

1. 데이터가 100만 개라면 어떻게 찾을까?

100만 명의 회원 정보가 저장된 테이블이 있다고 가정하자.

CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(255),
    age INT
);

여기서 특정 이메일을 사용하는 회원을 조회하려고 한다.

SELECT *
FROM users
WHERE email = 'gomin@example.com';

email에 인덱스가 없다면 DB는 조건에 맞는 회원을 찾기 위해 많은 행을 확인해야 할 수 있다.

이를 흔히 Full Table Scan이라고 한다.

users 테이블

1번 회원  → 일치하지 않음
2번 회원  → 일치하지 않음
3번 회원  → 일치하지 않음
...
999999번 회원 → 일치

100만 개의 행을 모두 확인해야 하는 상황이라면 상당한 비용이 발생할 것이다.

그렇다면 인덱스를 추가하면 무엇이 달라질까?

CREATE INDEX idx_users_email
ON users(email);

데이터베이스는 별도의 인덱스 자료구조를 활용해 이메일에 해당하는 행을 더 효율적으로 찾을 수 있다.

그렇다면 그 자료구조는 무엇일까?

2. 인덱스의 핵심은 B+Tree다

MySQL의 InnoDB에서 일반적으로 사용하는 인덱스 구조는 B+Tree다.

B+Tree는 균형 잡힌 트리 자료구조로, 검색 대상의 범위를 단계적으로 좁혀나갈 수 있다.

예를 들어 다음과 같은 구조를 생각해 보자.

                 [ M ]
                /     \
               /       \
          [ D, H ]    [ R, W ]
          /  |   \     /  |   \
        A-C D-G H-L  M-Q R-V W-Z

gomin@example.com을 찾는다고 가정하면 루트 노드에서 시작해 해당 문자열이 속한 범위로 이동한다.

모든 데이터를 처음부터 검사할 필요가 없다.

B+Tree의 핵심 특징은 다음과 같다.

  • 트리의 높이가 균형을 유지한다.
  • 내부 노드는 탐색 경로를 안내한다.
  • 실제 인덱스 항목은 리프 노드에 저장된다.
  • 리프 노드는 순차적으로 연결되어 범위 조회에 유리하다.

이 때문에 단건 조회뿐 아니라 범위 검색에도 효과적이다.

SELECT *
FROM users
WHERE age BETWEEN 20 AND 29;

적절한 인덱스가 있다면 해당 범위의 시작점을 찾은 뒤 리프 노드를 따라가며 값을 읽을 수 있다.

여기서 하나 더 알아둘 것이 있다.

InnoDB의 기본 키는 일반적으로 클러스터드 인덱스로 관리된다. 반면 보조 인덱스의 리프에는 인덱스 키와 기본 키 값이 저장된다.

따라서 보조 인덱스로 조건에 맞는 기본 키를 찾고, 실제 필요한 컬럼을 읽기 위해 클러스터드 인덱스를 추가 조회하는 과정이 발생할 수 있다.

즉, 인덱스를 사용한다고 해서 항상 한 번의 탐색으로 모든 데이터가 조회되는 것은 아니다.

3. 그런데 인덱스가 있어도 사용하지 않는다고?

다음과 같은 쿼리를 생각해 보자.

SELECT *
FROM users
WHERE age >= 0;

age 컬럼에 인덱스를 만들었다고 가정하자.

CREATE INDEX idx_users_age
ON users(age);

그런데 거의 모든 회원의 나이가 0 이상이라면 어떨까?

인덱스를 통해 대부분의 회원을 찾고, 각 회원의 나머지 컬럼을 조회하는 것보다 테이블을 직접 스캔하는 편이 더 효율적일 수 있다.

DB는 이러한 비용을 고려하여 실행 계획을 선택한다.

이 역할을 담당하는 것이 **쿼리 옵티마이저(Query Optimizer)**다.

옵티마이저는 테이블 통계, 인덱스의 선택도, 예상되는 읽기 비용 등을 고려해 실행 계획을 결정한다.

이를 직접 확인할 수 있는 명령어가 EXPLAIN이다.

EXPLAIN
SELECT *
FROM users
WHERE age >= 0;

실행 계획에서 다음 항목을 확인해 보자.

항목의미
type테이블 접근 방식
possible_keys사용 가능한 인덱스 후보
key실제 선택한 인덱스
rows확인할 것으로 추정하는 행 수
Extra추가적인 실행 정보

type = ALL이라면 일반적으로 전체 테이블 스캔을 의미한다.

다만 전체 스캔이 항상 잘못된 실행 계획은 아니다.

조회할 데이터가 대부분이라면 전체 스캔이 오히려 합리적일 수 있기 때문이다.

MySQL 8에서는 EXPLAIN ANALYZE를 사용해 실제 실행 과정과 시간도 살펴볼 수 있다. 일반 EXPLAIN의 비용 추정과 실제 수행 결과를 구분해서 보는 것이 좋다.

4. 인덱스를 많이 생성하면 왜 느려질까?

지금까지는 조회 성능만 살펴봤다.

하지만 데이터베이스는 조회만 하는 시스템이 아니다.

실제 서비스에서는 INSERT, UPDATE, DELETE도 끊임없이 발생한다.

인덱스가 있는 테이블에 데이터를 INSERT하면 어떤 일이 벌어질까?

DB는 테이블에 데이터를 저장하는 동시에 관련 인덱스도 갱신해야 한다.

INSERT 요청
    |
    ├── 테이블 데이터 저장
    ├── 이메일 인덱스 갱신
    ├── 나이 인덱스 갱신
    └── 이름 인덱스 갱신

인덱스가 많을수록 관리해야 할 자료구조도 늘어난다.

인덱스는 저장 공간을 차지하고, 관련 컬럼을 변경하는 UPDATE에도 추가 비용을 발생시킨다.

결국 조회를 빠르게 만들기 위해 추가한 인덱스가 데이터 변경 성능에는 부담이 될 수 있다.

인덱스가 있어도 비효율적인 쿼리

다음 사례도 살펴보자.

-- created_at에 일반 인덱스가 있다고 가정
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-11-04';

DATE() 함수로 컬럼을 가공했다.

이 경우 일반적인 created_at B+Tree 인덱스를 범위 탐색에 효과적으로 활용하지 못할 수 있다.

반면 다음처럼 작성한다면 인덱스 범위 탐색에 더 적합하다.

SELECT *
FROM orders
WHERE created_at >= '2026-11-04'
  AND created_at < '2026-11-05';

두 쿼리는 날짜 조건이 같아 보이지만, 옵티마이저가 선택할 수 있는 실행 계획은 달라질 수 있다.

물론 MySQL의 함수 기반 인덱스 등 별도 기능을 사용하면 상황이 달라질 수 있다.

중요한 것은 인덱스의 존재 자체가 아니라 쿼리가 그 인덱스를 효율적으로 활용할 수 있는 구조인지다.

5. 인덱스 설계에도 정답은 없다

인덱스는 분명 강력한 성능 최적화 수단이다.

하지만 인덱스를 추가하는 것이 무조건 정답은 아니다.

데이터의 규모와 분포, 실제 쿼리 패턴, 조회와 수정의 비율에 따라 적절한 인덱스 구성이 달라진다.

실무에서 중요한 순서는 다음과 같다고 생각한다.

  1. 어떤 쿼리가 느린지 먼저 확인한다.
  2. EXPLAIN과 실행 통계를 통해 병목을 분석한다.
  3. 실제 필터링 조건과 정렬 조건을 고려한다.
  4. 필요한 인덱스를 설계한다.
  5. 변경 전후 성능을 측정한다.

인덱스가 좋다는 사실을 아는 것과, 어떤 인덱스가 필요한지 판단하는 것은 전혀 다른 문제다.

최적화란 무언가를 더 추가하는 과정이 아니라, 실제 병목이 어디에 있는지 찾아 가장 적절한 방법으로 해결하는 과정이다.

생각해 볼 문제

Q1. 데이터가 100만 건인 테이블에서 인덱스를 추가했는데도 옵티마이저가 전체 테이블 스캔을 선택했다. 어떤 이유가 있을까?

Q2. WHERE age = 25 AND city = 'Seoul' 쿼리를 자주 실행한다면 단일 인덱스 두 개와 복합 인덱스 중 어떤 선택을 고려해야 할까?

Q3. 조회 속도는 빨라졌지만 INSERT 성능이 떨어졌다. 인덱스와 어떤 관련이 있을까?

참고 자료

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다