데이터베이스 인덱스 선택 기준: B트리·비트맵·해시와 복합 인덱스


1장. 인덱스를 만들었는데 왜 조회가 여전히 느릴까#

주문 테이블에 1천만 건의 데이터가 있습니다.

개발자는 다음 조회가 느리다는 이야기를 듣습니다.

SELECT *
FROM orders
WHERE customer_id = 101;

그래서 customer_id에 인덱스를 만듭니다.

CREATE INDEX idx_orders_customer
ON orders(customer_id);

조회 속도가 크게 좋아졌습니다.

이번에는 운영자가 다음 화면이 느리다고 합니다.

SELECT *
FROM orders
WHERE order_date >= '2026-09-01'
  AND order_date < '2026-10-01';

개발자는 생각합니다.

인덱스가 있으니 이것도 빨라야 하지 않을까?

하지만 customer_id 인덱스는 주문일 검색을 위한 구조가 아닙니다.

이번에는 다음 인덱스를 만듭니다.

CREATE INDEX idx_orders_date
ON orders(order_date);

그런데 월 전체 주문 대부분을 조회하는 보고서에서는 DBMS가 인덱스를 사용하지 않고 전체 테이블을 읽기도 합니다.

또 다른 화면에서는 (customer_id, order_date) 복합 인덱스를 만들었지만 주문일 조건만 주었을 때 기대만큼 빠르지 않습니다.

여기서 중요한 사실이 하나 드러납니다.

인덱스가 존재한다는 사실과 그 인덱스가 현재 질문에 적합하다는 것은 다른 문제다.

인덱스 설계는 컬럼 목록에서 시작하는 것이 아닙니다.

실제 SQL이 어떤 데이터를 어떤 조건과 순서로 찾는지에서 시작해야 합니다.


2장. 인덱스는 데이터의 복사본이 아니라 별도의 접근 구조다#

인덱스는 테이블 데이터를 더 빨리 찾기 위해 별도로 유지하는 자료구조입니다.

책을 예로 들면 본문 전체를 처음부터 읽는 대신 뒤쪽 색인에서 특정 단어의 페이지를 찾는 것과 비슷합니다.

테이블:

1천만 건의 주문 데이터

인덱스:

customer_id
→ 해당 주문을 찾을 수 있는 위치 정보

개념적으로 다음과 같이 생각할 수 있습니다.

flowchart LR
    Q["검색 조건"] --> I["인덱스 탐색"]
    I --> L["후보 행 위치"]
    L --> T["테이블 데이터 접근"]
    T --> R["결과 반환"]

하지만 모든 조회에서 마지막 테이블 접근이 필요한 것은 아닙니다.

필요한 모든 열을 인덱스에서 얻을 수 있다면 일부 DBMS에서는 테이블 접근을 줄이는 방식도 가능합니다.


3장. 인덱스의 가장 큰 장점은 읽어야 할 범위를 줄이는 것이다#

다음 주문 테이블이 있다고 하겠습니다.

총 주문 건수 = 10,000,000

그중 고객 M001의 주문은 50건입니다.

인덱스가 없다면 상황에 따라 많은 주문을 확인해야 할 수 있습니다.

10,000,000건
↓
M001 찾기
↓
50건 반환

적절한 인덱스가 있다면 처음부터 M001이 있는 범위로 접근할 수 있습니다.

인덱스 탐색
↓
M001 구간
↓
50건 반환

인덱스의 핵심은 단순히 “검색을 빠르게 한다”가 아닙니다.

필요하지 않은 데이터를 얼마나 덜 읽게 해 주는가가 중요합니다.


4장. 인덱스가 있다고 항상 사용되는 것은 아니다#

다음 테이블에 상태 값이 있다고 하겠습니다.

상태 주문 수
배송완료 9,000,000
배송중 500,000
오류 10,000
기타 490,000

status 인덱스가 있다고 하겠습니다.

다음 조회는 어떨까요?

SELECT *
FROM orders
WHERE status = '오류';

전체 1천만 건 가운데 1만 건 정도라면 인덱스로 후보를 좁히는 것이 유리할 가능성이 있습니다.

반면:

SELECT *
FROM orders
WHERE status = '배송완료';

는 900만 건을 반환합니다.

인덱스에서 900만 개의 위치를 찾은 뒤 테이블 본문을 대량으로 읽는 것보다 처음부터 테이블을 넓게 읽는 것이 더 저렴할 수 있습니다.

따라서 DBMS가 전체 스캔을 선택할 수도 있습니다.


5장. 선택도만 외워서는 인덱스를 설계할 수 없다#

흔히 이런 설명을 접합니다.

선택도가 높은 컬럼에 인덱스를 만들어라.

도움이 되는 원칙이지만 이것만으로는 부족합니다.

같은 선택도라도 다음에 따라 비용이 달라집니다.

  • 반환하는 열의 수
  • 실제 반환 행 수
  • 행이 물리적으로 흩어진 정도
  • 인덱스 크기
  • 테이블 크기
  • 데이터 캐시 상태
  • ORDER BY 여부
  • LIMIT 여부
  • 다른 조건과의 조합
  • DBMS의 실행 계획
  • 병렬 처리 가능 여부

따라서 특정 비율을 기준으로 다음처럼 판단해서는 안 됩니다.

10% 미만
→ 무조건 인덱스

20% 이상
→ 무조건 전체 스캔

고정된 손익분기점은 없습니다.


6장. B트리 계열 인덱스는 왜 가장 널리 사용될까#

관계형 데이터베이스에서 가장 흔하게 접하는 것이 B트리 계열 인덱스입니다.

개념적으로 다음처럼 여러 단계로 구성됩니다.

flowchart TD
    R["Root"] --> B1["Branch"]
    R --> B2["Branch"]
    B1 --> L1["Leaf"]
    B1 --> L2["Leaf"]
    B2 --> L3["Leaf"]
    B2 --> L4["Leaf"]

검색값을 이용해 루트에서 적절한 하위 영역을 선택하고 리프까지 내려갑니다.

핵심은 키 순서가 유지된다는 점입니다.

이 특성 덕분에 등가 조건뿐 아니라 범위 조회에도 사용할 수 있습니다.


7장. B트리는 등가 검색과 범위 검색 모두에 강하다#

다음 조회를 보겠습니다.

SELECT *
FROM orders
WHERE order_id = 'O10001';

정확한 하나의 키를 찾는 등가 검색입니다.

B트리 계열 인덱스가 잘 맞습니다.

이번에는:

SELECT *
FROM orders
WHERE order_date >= '2026-09-01'
  AND order_date < '2026-10-01';

날짜 범위 검색입니다.

정렬된 키에서 시작점을 찾은 뒤 해당 범위를 이어서 읽을 수 있습니다.

개념적으로:

2026-08-31
2026-09-01 ← 시작
2026-09-02
2026-09-03
...
2026-09-30
2026-10-01 ← 종료

처럼 처리할 수 있습니다.

이 때문에 B트리 계열 인덱스는 범용적인 인덱스로 널리 사용됩니다.


8장. 정렬에서도 인덱스 순서를 활용할 수 있다#

다음 SQL이 있다고 하겠습니다.

SELECT
    order_id,
    order_date
FROM orders
WHERE customer_id = 101
ORDER BY order_date DESC;

복합 인덱스가 다음 순서라면:

customer_id
order_date DESC

고객 101의 구간 안에서 주문일 순서대로 읽을 수 있는 구조가 될 수 있습니다.

따라서 별도의 정렬 작업을 줄일 가능성이 있습니다.

하지만 실제 사용 여부는 DBMS 실행 계획을 확인해야 합니다.


9장. 해시 인덱스는 등가 검색에 초점을 맞춘다#

해시는 키 값을 해시 함수에 넣어 특정 버킷으로 연결합니다.

개념적으로 다음과 같습니다.

flowchart LR
    K["user@example.com"] --> H["Hash"]
    H --> B["Bucket"]
    B --> R["행 위치"]

이 구조는 다음과 같은 등가 조건과 잘 맞습니다.

WHERE email = 'user@example.com'

반면 다음 조건은 해시가 정렬 순서를 유지하지 않으므로 같은 방식으로 처리하기 어렵습니다.

WHERE order_date BETWEEN ...

또는:

ORDER BY order_date

해시 인덱스의 핵심은 다음과 같습니다.

같은 값 찾기
→ 유리할 수 있음

범위 찾기
→ 정렬 순서를 이용하기 어려움

10장. 해시가 B트리보다 무조건 빠른 것은 아니다#

자료구조를 배울 때 해시 탐색을 매우 빠른 구조로 설명합니다.

하지만 실제 DBMS에서는 다음 요소가 있습니다.

해시 충돌

버킷 확장

페이지 접근

캐시 상태

동시 변경

인덱스 크기

제품별 구현 차이

따라서:

해시는 O(1)이니까 무조건 B트리보다 빠르다.

라고 단정해서는 안 됩니다.

실제 질의와 DBMS 구현에서 측정해야 합니다.


11장. 비트맵 인덱스는 조건 조합 방식이 다르다#

이번에는 성별과 지역 같은 범주형 데이터가 있다고 하겠습니다.

행 성별 지역
R1 남 서울
R2 여 서울
R3 남 부산
R4 여 부산

성별을 비트로 표현하면 다음처럼 생각할 수 있습니다.

남
1010

지역 서울은:

1100

두 조건을 동시에 만족하는 행을 찾으려면:

1010
AND
1100
=
1000

첫 번째 위치만 1입니다.

즉 R1만 다음 조건을 만족합니다.

남성
AND
서울

12장. 비트맵의 장점은 여러 조건을 조합할 때 드러날 수 있다#

분석 시스템에서는 다음과 같은 질의를 자주 사용할 수 있습니다.

성별 = 여성

지역 = 서울

등급 = VIP

가입연도 = 2025

여러 범주 조건을 AND와 OR로 조합하는 분석에서는 비트맵 방식이 유용할 수 있습니다.

하지만 단순히 고유값 개수가 적다는 이유만으로 무조건 비트맵 인덱스를 선택해서는 안 됩니다.

다음도 고려해야 합니다.

동시 변경이 많은가?

분석 중심 시스템인가?

값 분포는 어떤가?

조건을 여러 개 조합하는가?

DBMS가 어떤 구현을 제공하는가?

13장. OLTP와 분석 시스템은 인덱스 선택 기준이 다를 수 있다#

주문 시스템처럼 INSERT와 UPDATE가 계속 발생하는 환경과 대규모 분석 시스템은 요구가 다릅니다.

OLTP에서는 다음 특성이 중요할 수 있습니다.

짧은 조회

빈번한 쓰기

높은 동시성

분석 시스템에서는:

대량 데이터

다수 조건 조합

상대적으로 적은 실시간 변경

이 중요할 수 있습니다.

비트맵 방식은 후자에서 유용할 수 있지만 제품별 구현과 동시성 특성을 확인해야 합니다.


14장. 복합 인덱스가 단일 인덱스 여러 개와 같은 것은 아니다#

다음 두 개의 단일 인덱스가 있다고 하겠습니다.

customer_id 인덱스

order_date 인덱스

그리고 다음 복합 인덱스가 있습니다.

(customer_id, order_date)

둘은 같은 구조가 아닙니다.

복합 인덱스는 두 값을 하나의 정렬된 키 구조로 함께 관리합니다.

개념적으로:

customer 1
 ├─ 2026-09-01
 ├─ 2026-09-05
 └─ 2026-09-20

customer 2
 ├─ 2026-09-03
 └─ 2026-09-22

처럼 생각할 수 있습니다.


15장. 복합 인덱스의 열 순서는 중요하다#

다음 인덱스가 있다고 하겠습니다.

CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);

주요 정렬 구조는:

customer_id
→ 그 안에서 order_date

입니다.

다음 질의와 잘 맞을 수 있습니다.

SELECT *
FROM orders
WHERE customer_id = 101
  AND order_date >= '2026-09-01'
  AND order_date < '2026-10-01';

먼저 고객 101의 구간으로 이동합니다.

그 안에서 필요한 날짜 범위를 읽습니다.


16장. 같은 두 컬럼이어도 순서를 바꾸면 다른 인덱스다#

이번에는 다음 인덱스를 보겠습니다.

(order_date, customer_id)

구조는 다음처럼 볼 수 있습니다.

2026-09-01
 ├─ customer 1
 ├─ customer 4
 └─ customer 9

2026-09-02
 ├─ customer 2
 └─ customer 8

이 인덱스는 특정 날짜의 모든 고객 주문을 찾는 조회에 더 자연스러운 후보가 될 수 있습니다.

즉:

(customer_id, order_date)

(order_date, customer_id)

는 같은 컬럼 두 개를 사용하더라도 서로 다른 접근 경로를 제공합니다.


17장. 고객별 최근 주문 화면을 설계해 보자#

사용자 화면의 요구사항이 다음과 같다고 하겠습니다.

고객 한 명의 최근 주문 20건을 최신순으로 보여준다.

SQL은 다음과 비슷할 수 있습니다.

SELECT
    order_id,
    created_at,
    total_amount
FROM orders
WHERE customer_id = 101
ORDER BY created_at DESC
LIMIT 20;

이 경우 다음 복합 인덱스를 검토할 수 있습니다.

(customer_id, created_at DESC)

동일한 시각의 주문에 안정적인 추가 정렬이 필요하다면:

(customer_id, created_at DESC, order_id DESC)

같은 구조도 고려할 수 있습니다.


18장. 전체 고객의 날짜별 주문 화면은 요구가 다르다#

운영자가 다음 조회를 사용한다고 하겠습니다.

SELECT *
FROM orders
WHERE created_at >= '2026-10-01'
  AND created_at < '2026-10-02';

여기에는 customer_id 조건이 없습니다.

따라서 고객별 최근 주문을 위해 만든:

(customer_id, created_at)

인덱스가 동일한 방식으로 효율적인 좁은 범위를 제공한다고 기대해서는 안 됩니다.

모든 고객 그룹에 날짜가 흩어져 있기 때문입니다.

이 화면에서는:

(created_at)

또는:

(created_at, ...)

계열의 인덱스가 더 직접적인 후보가 될 수 있습니다.


19장. 선두 열이 없으면 인덱스를 절대 못 쓰는 것은 아니다#

복합 B트리 인덱스가 다음과 같다고 하겠습니다.

(customer_id, created_at)

다음 SQL을 실행합니다.

WHERE created_at >= ...

전통적인 설명에서는 다음처럼 말하기 쉽습니다.

선두 컬럼이 없으니 인덱스를 사용할 수 없다.

너무 단정적인 설명입니다.

DBMS에 따라 전체 인덱스 스캔이나 skip scan과 유사한 접근을 선택할 수도 있습니다.

중요한 것은 다음입니다.

선두 컬럼 조건이 없으면 같은 방식으로 좁은 연속 범위를 찾기 어려울 가능성이 크다.

실제 사용 여부와 비용은 실행 계획을 확인해야 합니다.


20장. WHERE 절에 조건을 적는 순서는 인덱스 열 순서를 바꾸지 않는다#

다음 두 SQL은 논리적으로 같은 조건을 가집니다.

WHERE customer_id = 101
  AND order_date >= '2026-09-01';
WHERE order_date >= '2026-09-01'
  AND customer_id = 101;

WHERE 절에 order_date를 먼저 적었다고 해서 복합 인덱스의 선두 열이 주문일로 바뀌는 것은 아닙니다.

인덱스 구조는 생성할 때 결정됩니다.

(customer_id, order_date)

이면 저장된 키 순서 자체가 이 순서를 기준으로 만들어집니다.


21장. “선택도가 높은 열을 무조건 앞에”도 위험한 규칙이다#

복합 인덱스의 열 순서를 정할 때 흔히 다음 말을 듣습니다.

선택도가 높은 컬럼을 먼저 넣어라.

경우에 따라 유용하지만 절대적인 규칙은 아닙니다.

예를 들어 실제 조회가 항상 다음과 같다고 하겠습니다.

customer_id = ?
AND
order_date BETWEEN ? AND ?
ORDER BY order_date

이 경우 고객 등가 조건과 날짜 범위, 정렬 요구를 함께 고려해야 합니다.

단순히 각 컬럼의 고유값 개수만 비교해서 순서를 결정하면 실제 질의와 맞지 않을 수 있습니다.


22장. 등가 조건과 범위 조건을 나누어 생각하면 이해가 쉽다#

다음 복합 인덱스를 보겠습니다.

(customer_id, order_date, status)

질의는 다음과 같습니다.

WHERE customer_id = 101
  AND order_date >= '2026-09-01'
  AND status = '완료';

customer_id는 등가 조건입니다.

customer_id = 101

그다음 order_date는 범위 조건입니다.

order_date >= ...

인덱스에서 고객 101의 구간까지 좁힌 뒤 날짜 범위를 읽을 수 있습니다.

status 조건도 검사할 수 있지만 해당 열이 실제 탐색 범위를 얼마나 더 줄이는지는 별도로 봐야 합니다.


23장. 인덱스 열을 많이 넣는다고 무조건 좋은 것은 아니다#

다음 인덱스를 만들어 보겠습니다.

(
 customer_id,
 order_date,
 status,
 total_amount,
 payment_type,
 shipping_type
)

많은 조회가 인덱스만으로 해결될 것처럼 보입니다.

하지만 인덱스가 커집니다.

결과적으로 다음 비용이 증가할 수 있습니다.

저장 공간

버퍼 캐시 사용량

INSERT 비용

UPDATE 비용

DELETE 비용

페이지 분할 가능성

따라서 인덱스 열은 필요한 만큼만 설계해야 합니다.


24장. 커버링 인덱스는 테이블 재접근을 줄이기 위한 설계다#

다음 조회가 매우 자주 실행된다고 하겠습니다.

SELECT
    order_date,
    total_amount
FROM orders
WHERE customer_id = 101;

인덱스에 다음 정보가 모두 있다면:

customer_id
order_date
total_amount

DBMS가 테이블 본문을 다시 읽지 않고 결과를 제공할 수 있는 상황이 생길 수 있습니다.

이런 관점의 인덱스를 커버링 인덱스라고 부릅니다.


25장. 커버링 인덱스라고 항상 테이블 접근이 0은 아니다#

이론적으로 필요한 열이 모두 인덱스에 있어도 제품 내부 구현에 따라 추가 작업이 필요할 수 있습니다.

예를 들어 PostgreSQL에서는 MVCC 가시성 확인 때문에 인덱스 전용 스캔에서도 상황에 따라 힙 페이지 확인이 필요할 수 있습니다.

따라서 다음 등식은 항상 성립하지 않습니다.

필요 열이 모두 인덱스에 존재
=
테이블 접근 완전 제거

실제 실행 계획과 버퍼 통계를 확인해야 합니다.


26장. 표현식 인덱스는 가공된 조건을 빠르게 찾기 위한 방법이다#

회원 이메일을 대소문자 구분 없이 찾는다고 하겠습니다.

SQL은 다음과 같습니다.

SELECT member_id
FROM member
WHERE lower(email) = 'user@example.com';

일반 이메일 컬럼의 인덱스만 존재하면 DBMS가 해당 표현식을 그대로 효율적으로 사용할 수 없는 상황이 있을 수 있습니다.

PostgreSQL에서는 표현식 인덱스를 만들 수 있습니다.

CREATE INDEX idx_member_email_lower
ON member(lower(email));

이제:

lower(email)

의 결과 자체를 색인한 구조를 이용할 수 있습니다.


27장. 표현식 인덱스는 SQL 조건과 맞아야 한다#

다음 인덱스를 만들었습니다.

lower(email)

그런데 SQL은 다음과 같습니다.

WHERE email = 'user@example.com'

또는:

WHERE upper(email) = 'USER@EXAMPLE.COM'

표현이 다릅니다.

DBMS의 최적화 능력과 지원 방식에 따라 사용 여부가 달라질 수 있습니다.

표현식 인덱스를 만들 때는 실제 SQL이 어떤 표현식을 반복적으로 사용하는지 확인해야 합니다.


28장. 함수나 연산 결과도 인덱스 대상이 될 수 있다#

업무에 따라 다음과 같은 표현식을 자주 검색할 수 있습니다.

lower(email)

date(created_at)

quantity * unit_price

일부 DBMS에서는 이런 표현식이나 계산 결과에 인덱스를 구성할 수 있습니다.

하지만 다음을 확인해야 합니다.

DBMS가 지원하는가?

함수가 인덱스에 적합한가?

SQL 표현식이 일치하는가?

값 변경 때 유지 비용은 어느 정도인가?

29장. 클러스터링 정도도 인덱스 비용에 영향을 준다#

고객 101의 주문 100건이 물리적으로 비슷한 페이지에 모여 있다고 하겠습니다.

인덱스에서 100개의 위치를 찾은 뒤 몇 개의 테이블 페이지만 읽으면 될 수 있습니다.

반대로 100개의 주문이 테이블 전체에 흩어져 있다면:

행 1 → 페이지 10
행 2 → 페이지 782
행 3 → 페이지 1190
...

처럼 많은 페이지를 방문할 수 있습니다.

같은 100행 조회라도 실제 비용은 달라집니다.

따라서 인덱스에서 찾은 행 수만으로 비용을 판단할 수 없습니다.


30장. 전체 스캔이 더 빠른 상황을 이해해야 한다#

보고서가 지난 5년의 모든 주문을 합산한다고 하겠습니다.

SELECT SUM(total_amount)
FROM orders
WHERE order_date >= '2022-01-01';

전체 데이터의 대부분이 조건에 포함된다면 인덱스로 각 행 위치를 하나씩 찾아 테이블을 방문하는 것보다 큰 범위를 순차적으로 읽는 것이 더 효율적일 수 있습니다.

따라서 옵티마이저가 인덱스를 사용하지 않았다고 해서 자동으로 잘못된 계획이라고 볼 수 없습니다.


31장. 인덱스는 조회 속도와 쓰기 속도를 교환한다#

주문 하나를 추가한다고 하겠습니다.

인덱스가 하나도 없다면 주로 테이블 데이터만 추가하면 됩니다.

하지만 다음 인덱스가 있다고 해보겠습니다.

customer_id

order_date

status

(customer_id, order_date)

(payment_type, order_date)

주문 한 건을 INSERT할 때 테이블뿐 아니라 관련 인덱스 구조도 갱신해야 합니다.

즉 인덱스가 많아질수록 쓰기 부담이 커질 수 있습니다.

읽기 이득
↕
쓰기 비용

의 관계입니다.


32장. UPDATE도 인덱스 컬럼을 바꾸면 비용이 증가한다#

다음 인덱스가 있습니다.

status

주문 상태가 다음처럼 바뀝니다.

접수
↓
결제완료
↓
배송중
↓
배송완료

상태가 바뀔 때마다 상태 인덱스도 변경해야 합니다.

변경이 매우 빈번한 컬럼에 인덱스를 추가할 때는 조회 이득뿐 아니라 지속적인 유지 비용도 봐야 합니다.


33장. 인덱스 페이지 분할도 쓰기 비용의 일부다#

정렬된 인덱스 페이지에 새로운 키를 넣으려고 하는데 공간이 부족할 수 있습니다.

DBMS는 페이지를 나누거나 다른 방식으로 공간을 확보해야 합니다.

이런 작업에는 추가적인 I/O와 관리 비용이 발생할 수 있습니다.

다만 페이지 분할의 구체적인 동작과 비용은 DBMS 구현에 따라 차이가 있습니다.

따라서 특정 패턴을 모든 제품에 동일하게 적용해서는 안 됩니다.


34장. 인덱스를 무조건 많이 만드는 전략은 좋지 않다#

다음과 같은 생각이 들 수 있습니다.

검색할 수 있는 모든 컬럼에 인덱스를 만들자.

그러면 SELECT 일부는 빨라질 수 있습니다.

하지만 다음 문제가 발생할 수 있습니다.

INSERT 느려짐

UPDATE 느려짐

DELETE 느려짐

스토리지 증가

버퍼 캐시 압박

유사 인덱스 중복

통계·유지보수 비용 증가

인덱스는 많을수록 좋은 것이 아닙니다.

실제 질의를 지원하는 만큼 필요한 구조가 좋습니다.


35장. 외래키라고 항상 인덱스를 만들어야 하는 것은 아니다#

주문 테이블에 다음 외래키가 있습니다.

customer_id

고객과 주문을 자주 조인하고 한 고객의 주문을 자주 찾는다면 인덱스가 매우 유용할 수 있습니다.

하지만 모든 외래키에 무조건 인덱스를 만들어야 한다는 보편 법칙은 아닙니다.

다음 요소를 봐야 합니다.

조인 빈도

부모 삭제·수정 패턴

자식 행 수

쓰기가 얼마나 많은지

DBMS의 제약 검사 방식

36장. 사용되지 않는 인덱스를 바로 삭제해서도 안 된다#

운영 통계를 보니 어떤 인덱스의 스캔 횟수가 0이라고 하겠습니다.

바로 삭제해도 될까요?

주의해야 합니다.

다음 가능성이 있습니다.

통계가 최근 초기화됨

월말 작업에서만 사용됨

연말 배치에서 사용됨

장애 복구 쿼리에서 사용됨

제약조건과 관련됨

특정 드문 관리자 기능에서 사용됨

따라서 충분한 업무 주기를 관찰한 뒤 제거해야 합니다.


37장. 인덱스 하나의 목적을 문서로 남겨 두는 것이 좋다#

다음처럼 이름만 적어두는 것보다:

idx_orders_03

목적까지 기록하는 편이 좋습니다.

고객별 최근 주문 20건 조회용

지원 SQL:
customer_id = ?
ORDER BY created_at DESC
LIMIT 20

이렇게 해두면 해당 화면이 없어졌을 때 인덱스를 유지해야 하는지도 판단하기 쉽습니다.


38장. 인덱스 후보는 SQL 전체를 보고 선택해야 한다#

다음 SQL이 있다고 하겠습니다.

SELECT
    order_id,
    total_amount
FROM orders
WHERE customer_id = 101
  AND status = '완료'
ORDER BY created_at DESC
LIMIT 20;

인덱스 설계에서는 WHERE 조건만 보는 것으로 부족합니다.

다음을 모두 봐야 합니다.

customer_id
→ 검색 조건

status
→ 추가 조건

created_at
→ 정렬

LIMIT 20
→ 소량 결과

order_id, total_amount
→ 반환 열

이 전체 요구를 기준으로 인덱스 후보를 설계합니다.


39장. 가장 자주 실행되는 SQL과 가장 비싼 SQL은 다를 수 있다#

쿼리 A:

1ms
하루 1천만 번

쿼리 B:

10초
하루 한 번

어느 쪽을 먼저 최적화해야 할까요?

상황에 따라 A가 시스템 전체 비용에 훨씬 큰 영향을 줄 수 있습니다.

따라서 인덱스 우선순위는 한 번의 실행 시간만으로 정하지 않습니다.

실행 빈도
×
실행 비용

을 함께 봐야 합니다.


40장. PostgreSQL에서 복합 인덱스를 시험해 보기#

다음과 같은 실습 테이블을 만들 수 있습니다.

CREATE TABLE orders_demo (
    order_id BIGINT PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    created_at TIMESTAMP NOT NULL
);

2만 건을 넣습니다.

INSERT INTO orders_demo
SELECT
    g,
    1 + (g % 1000),
    TIMESTAMP '2026-09-01'
      + g * INTERVAL '1 minute'
FROM generate_series(1, 20000) AS g;

통계를 갱신합니다.

ANALYZE orders_demo;

복합 인덱스를 만듭니다.

CREATE INDEX orders_customer_recent_idx
ON orders_demo (
    customer_id,
    created_at DESC,
    order_id DESC
);

41장. 실제 실행 계획에서 무엇을 봐야 할까#

다음 쿼리를 실행합니다.

EXPLAIN (ANALYZE, BUFFERS)
SELECT
    order_id,
    created_at
FROM orders_demo
WHERE customer_id = 101
ORDER BY
    created_at DESC,
    order_id DESC
LIMIT 20;

확인할 내용은 다음과 같습니다.

어떤 Scan이 선택됐는가

예상 행 수는 얼마인가

실제 행 수는 얼마인가

loops는 몇 번인가

Buffers는 얼마나 사용됐는가

별도 Sort가 발생했는가

실제 실행 시간은 어느 정도인가

인덱스를 만들기 전과 후를 비교하면 접근 경로가 어떻게 달라졌는지 볼 수 있습니다.


42장. 같은 인덱스로 날짜 조건만 검색해 보자#

이번에는 고객 조건을 제거합니다.

EXPLAIN (ANALYZE, BUFFERS)
SELECT
    order_id,
    created_at
FROM orders_demo
WHERE created_at >= TIMESTAMP '2026-09-10';

인덱스는:

(customer_id, created_at, order_id)

입니다.

고객 조건이 없으므로 고객별 구간마다 날짜가 흩어져 있습니다.

DBMS가 어떤 계획을 선택하는지 확인할 수 있습니다.

핵심은 다음입니다.

같은 테이블에 같은 날짜 조건이 있어도 인덱스 열 순서에 따라 접근 비용이 달라질 수 있다.


43장. 작은 테이블에서는 인덱스를 만들어도 전체 스캔이 선택될 수 있다#

테이블에 100행밖에 없다고 하겠습니다.

인덱스 루트와 리프를 탐색하고 다시 테이블 데이터를 찾는 것보다 100행 전체를 한 번 읽는 편이 더 저렴할 수 있습니다.

따라서 테스트용 작은 데이터에서 전체 스캔이 나왔다고:

인덱스가 작동하지 않는다.

라고 결론 내리면 안 됩니다.

실제 운영 규모와 비슷한 데이터 분포에서 검증해야 합니다.


44장. 데이터 분포가 바뀌면 좋은 인덱스도 달라질 수 있다#

처음에는 주문 상태가 다음처럼 균등했다고 하겠습니다.

접수 25%
배송중 25%
완료 25%
취소 25%

몇 년 뒤 대부분의 주문이 완료 상태로 쌓여:

완료 95%
나머지 5%

가 되었습니다.

동일한 상태 인덱스라도 완료 검색에서는 매력이 크게 줄 수 있습니다.

데이터 분포와 통계가 바뀌면 옵티마이저의 판단도 달라질 수 있습니다.


45장. 인덱스 설계는 하나의 SQL만 보고 끝내면 안 된다#

다음 세 화면이 있다고 하겠습니다.

고객 화면#

고객 한 명의 최근 주문 20건

운영 화면#

오늘 모든 고객 주문

장애 대응 화면#

오류 상태 주문

각 화면의 접근 패턴은 다릅니다.

따라서 인덱스 후보도 달라질 수 있습니다.

(customer_id, created_at)

(created_at)

(status, created_at)

등 여러 후보가 생길 수 있습니다.

그러나 세 개를 모두 무조건 만들기 전에 실제 호출 빈도와 쓰기 비용을 비교해야 합니다.


46장. 중복 인덱스도 살펴봐야 한다#

다음 인덱스 두 개가 있다고 하겠습니다.

(customer_id)

(customer_id, created_at)

두 번째 복합 인덱스가 첫 번째 인덱스의 주요 역할을 상당 부분 대신할 수 있는 상황이 있을 수 있습니다.

그렇다고 첫 번째가 항상 불필요하다고 단정할 수는 없습니다.

인덱스 크기와 실제 실행 계획, 특정 질의 패턴을 비교해야 합니다.

하지만 비슷한 인덱스가 계속 추가되면 쓰기와 저장 비용만 늘어날 수 있으므로 중복 여부를 정기적으로 검토하는 것이 좋습니다.


47장. 인덱스 구조별 특징 정리#

인덱스 유형 강점 주의할 점
B트리 계열 등가·범위·정렬에 폭넓게 활용 쓰기 유지 비용과 페이지 관리
해시 등가 검색 범위·정렬에 부적합할 수 있음
비트맵 여러 범주 조건의 조합 동시 변경이 많은 환경은 제품 특성 확인
표현식 인덱스 함수·계산식 조건 검색 실제 SQL 표현식과 지원 조건 확인
복합 인덱스 여러 조건과 정렬을 하나의 구조로 지원 열 순서가 접근 범위를 바꿈
커버링 목적 인덱스 테이블 재접근 감소 가능 인덱스 크기와 쓰기 비용 증가

인덱스 유형의 이름만 보고 선택해서는 안 됩니다.

각 구조가 어떤 질문을 효율적으로 해결하는지 연결해야 합니다.


48장. 인덱스를 추가할 때 반드시 같이 측정해야 할 것#

인덱스 추가 전후에 다음 항목을 비교하면 좋습니다.

영역 확인 항목
조회 응답 시간
실행 계획 스캔 종류와 예상·실제 행 수
I/O 버퍼와 페이지 접근량
정렬 별도 Sort 발생 여부
쓰기 INSERT·UPDATE·DELETE 지연
저장 인덱스 크기
캐시 추가 인덱스가 사용하는 메모리
운영 실제 사용 빈도

조회 시간만 줄었다고 끝내면 안 됩니다.

쓰기 지연이 크게 늘었다면 전체 시스템에는 오히려 손해일 수도 있습니다.


49장. 인덱스 선택을 위한 실전 질문#

새 인덱스를 만들기 전에 다음 질문에 답해 보는 것이 좋습니다.

  1. 어떤 SQL을 빠르게 만들려는가?
  2. 그 SQL은 하루 몇 번 실행되는가?
  3. 결과는 몇 행인가?
  4. 조건은 등가 검색인가 범위 검색인가?
  5. ORDER BY가 있는가?
  6. LIMIT가 있는가?
  7. 반환 열은 무엇인가?
  8. 기존 인덱스로 해결할 수 없는가?
  9. 데이터 분포는 어떤가?
  10. 해당 컬럼은 얼마나 자주 변경되는가?
  11. 새 인덱스 크기는 어느 정도인가?
  12. INSERT와 UPDATE가 얼마나 느려지는가?
  13. 실행 계획에서 실제로 새 인덱스를 사용하는가?
  14. 인덱스를 제거해도 영향을 받지 않는 기존 업무가 있는가?

이 질문에 답하지 않고 만드는 인덱스는 시간이 지나면 목적을 알 수 없는 구조가 되기 쉽습니다.


50장. 핵심 정리#

인덱스는 특정 검색과 정렬을 빠르게 하기 위한 별도의 접근 구조입니다.

하지만 인덱스가 존재한다고 모든 조회가 빨라지는 것은 아닙니다.

가장 중요한 것은 실제 SQL의 질문과 인덱스 구조를 맞추는 것입니다.

B트리 계열은 키 순서를 유지하기 때문에 등가 검색과 범위 검색, 특정 정렬에 폭넓게 사용할 수 있습니다.

해시 인덱스는 등가 검색에 초점을 맞춘 구조입니다.

비트맵 방식은 여러 범주 조건을 조합하는 분석 환경에서 강점을 가질 수 있습니다.

복합 인덱스에서는 열 순서가 매우 중요합니다.

(customer_id, order_date)

와:

(order_date, customer_id)

는 같은 두 컬럼을 사용하지만 서로 다른 검색 경로를 만듭니다.

그러나 선두 열 규칙이나 선택도 같은 한 가지 기준만으로 인덱스를 설계해서는 안 됩니다.

다음 요소를 함께 봐야 합니다.

WHERE 조건

등가와 범위 조건

ORDER BY

LIMIT

반환 열

데이터 분포

실제 행 수

실행 빈도

쓰기 빈도

또한 인덱스는 공짜가 아닙니다.

인덱스를 추가하면:

저장 공간 증가

INSERT 비용 증가

UPDATE 비용 증가

DELETE 비용 증가

버퍼 캐시 사용량 증가

가 발생할 수 있습니다.

그래서 인덱스 설계의 핵심 질문은 이것입니다.

이 인덱스가 줄여 주는 읽기 비용이 새롭게 발생하는 쓰기와 저장 비용보다 충분히 큰가?

그리고 그 답은 경험칙만으로 정하지 않습니다.

실행 계획, 실제 행 수, 버퍼 접근량, 응답 시간과 쓰기 부하를 직접 비교해 판단해야 합니다.

좋은 인덱스는 단순히 존재하는 인덱스가 아닙니다.

특정 업무의 특정 질문을 더 적은 작업으로 해결하고, 그 대가로 발생하는 유지 비용까지 설명할 수 있는 인덱스입니다.

이 페이지의 목차