복합 인덱스에서 = 조건 컬럼의 순서는 왜 상관없을까?
복합 인덱스를 설계하다 보면 흔히 "선택도(Selectivity)가 높은 컬럼을 앞에 둬라"는 조언을 듣는다. 하지만 이 말이 항상 맞는 건 아니다. 적어도 = 조건으로 묶인 컬럼들 사이에서는, 어떤 컬럼이 앞에 오든 인덱스 스캔 효율에 영향을 주지 않는다
이 글에서는 그 이유를 B-Tree 인덱스의 수직 탐색 / 수평 탐색 관점에서 데이터를 직접 펼쳐보며 정리한다
예제 상황
다음과 같은 조건절을 처리한다고 하자.
WHERE 고객등급 = :V1 -- = 조건
AND 고객번호 = :V2 -- = 조건
AND 거래일자 >= :V3 -- 범위(BETWEEN) 조건
고객등급과 고객번호는 항상 = 조건이고 거래일자는 범위 조건이다. 이때 인덱스를 다음 둘 중 어떻게 구성해도 결과가 같은지 살펴본다
- 인덱스 A:
(고객등급, 고객번호, 거래일자) - 인덱스 B:
(고객번호, 고객등급, 거래일자)
B-Tree 인덱스 검색의 두 단계
먼저 인덱스 검색이 어떻게 동작하는지 짚고 가자. B-Tree 인덱스 검색은 두 단계로 이뤄진다
- 수직 탐색: 루트 → 브랜치 → 리프 블록까지 내려가며 스캔 시작점을 찾는 과정
- 수평 탐색: 리프 블록을 따라 옆으로 읽으며 데이터를 훑는 과정
핵심 질문은 이거다. "컬럼 순서가 바뀌면 이 두 탐색의 비용이 달라지는가?"
데이터를 직접 펼쳐보자
동일한 데이터를 두 인덱스로 각각 정렬해본다. 복합 인덱스는 사전(辭典) 순서처럼 앞 컬럼을 우선으로 정렬하고 그 값이 같은 행끼리 다음 컬럼을 정렬한다
인덱스 A: (고객등급, 고객번호, 거래일자)
A - 100 - 2024-01-15 ← 등급=A, 번호=100 묶음 시작
A - 100 - 2024-03-20
A - 100 - 2024-05-10 ← 묶음 끝 (3건)
A - 200 - 2024-02-10
A - 200 - 2024-04-05
B - 100 - 2024-01-05
B - 100 - 2024-06-20
B - 150 - 2024-04-12
조건 등급=A AND 번호=100을 만족하는 행은 위에서 3건이 연속으로 뭉쳐 있다
인덱스 B: (고객번호, 고객등급, 거래일자)
100 - A - 2024-01-15 ← 번호=100, 등급=A 묶음 시작
100 - A - 2024-03-20
100 - A - 2024-05-10 ← 묶음 끝 (3건)
100 - B - 2024-01-05
100 - B - 2024-06-20
150 - B - 2024-04-12
200 - A - 2024-02-10
200 - A - 2024-04-05
순서가 바뀌었는데도 조건 `번호=100 AND 등급=A`를 만족하는 행은 여전히 똑같은 3건이 연속으로 뭉쳐 있다
왜 같은 3건이 뭉칠까?
등급=A 그리고 번호=100인 행은 원본 테이블에 어차피 3건밖에 없다.
두 = 조건이 모두 만족되는 행의 개수는 데이터 자체의 특성이지 인덱스 컬럼 순서가 만들어내는 특성이 아니다. 컬럼 순서를 바꿔도 이 교집합 자체는 변하지 않는다
그리고 복합 인덱스의 성질상 앞쪽 = 조건 컬럼들이 모두 같은 값을 가지는 행들은 인덱스 내에서 반드시 연속된 위치에 모인다 그 묶음 안에서 마지막 컬럼(거래일자)은 항상 정렬되어 있다
인덱스 A: [A-100-?, A-100-?, A-100-?] ← 3건 뭉침, 거래일자 정렬
인덱스 B: [100-A-?, 100-A-?, 100-A-?] ← 3건 뭉침, 거래일자 정렬비유: 학생 정렬
학생들을 정렬한다고 생각해보자.
- 방법 1:
학년 → 반 → 번호순 → "3학년 1반" 학생들이 한 곳에 뭉친다 (30명) - 방법 2:
반 → 학년 → 번호순 → "1반 3학년" 학생들이 한 곳에 뭉친다 (30명)
3학년 1반 학생은 어차피 30명이다 어떻게 정렬하든 30명이 한 묶음으로 뭉치고, 그 안에서 번호순으로 정렬된다. 정렬 기준의 순서를 바꿔도 "특정 학년 + 특정 반" 학생 수는 그대로다
그래서 수직 탐색도, 수평 탐색도 동일하다
두 인덱스를 같은 조건으로 검색하면 다음이 모두 같다.
| 항목 | 인덱스 A | 인덱스 B |
|---|---|---|
| 수직 탐색 블록 수 | 루트 1 → 브랜치 1 → 리프 1 | 루트 1 → 브랜치 1 → 리프 1 |
| 묶음(스캔 대상) 크기 | 3건 | 3건 |
| 거래일자 정렬 여부 | 정렬됨 | 정렬됨 |
| 수평 탐색 범위 | 동일 | 동일 |
범위 조건은 왜 반드시 뒤로 보내야 할까?
= 조건끼리는 순서가 무관하지만, 범위 조건은 다르다 범위 조건은 한 점이 아니라 구간을 만들기 때문이다 만약 인덱스를 `(고객등급, 거래일자, 고객번호)`로 구성하면 `거래일자 >= ...`로 점프한 구간 안에서는 `고객번호`가 정렬되어 있지 않다A - 2024-02-01 - 200
A - 2024-02-05 - 100 ← 번호=100? 맞음
A - 2024-03-10 - 200 ← 번호=100? 아님 (버림)
A - 2024-04-05 - 100 ← 번호=100? 맞음
A - 2024-05-22 - 200 ← 번호=100? 아님 (버림)
이 경우 객번호=100은 인덱스 스캔 범위를 좁히지 못하고 스캔한 행 중에서 버릴지 말지를 판단하는 필터 조건으로 전락한다 범위 구간 전체를 다 읽어야 하므로 비효율적이다
액세스 조건 vs 필터 조건
| 구분 | 의미 | 인덱스 스캔 범위 |
|---|---|---|
| 액세스 조건 | 인덱스 스캔의 시작점/끝점을 결정 | 좁힘 |
| 필터 조건 | 스캔한 행에서 버릴지 결정 | 못 좁힘 |
=조건이 연속되는 동안 → 액세스 조건- 첫 번째 범위 조건 → 액세스 조건 (자기 자신까지는 범위를 좁힘)
- 범위 조건 뒤의 컬럼들 → 필터 조건으로 전락
IDX: 고객등급(=) + 고객번호(=) + 거래일자(BETWEEN) + 거래유형 + 상품번호
└──── 모두 액세스 조건 ────┘ └─ 필터 ─┘
정리
- 조건들의 교집합 행 개수는 데이터의 특성이지 인덱스 컬럼 순서의 특성이 아니다
- 어떤 순서로 정렬하든 그 교집합 행들은 연속으로 뭉치며 마지막 컬럼은 그 묶음 안에서 항상 정렬되어 있다
- 따라서
=조건 컬럼들끼리는 순서를 바꿔도 수직 탐색 비용, 수평 탐색 범위가 모두 동일하다 - 범위 조건은 반드시 액세스 조건 뒤로 보내야 한다 범위 뒤의 컬럼은 정렬 효과를 잃고 필터 조건으로 전락하기 때문이다
"선택도 높은 컬럼을 앞에"라는 조언은 범위 조건이나 IN 조건이 섞일 때 의미가 생긴다 = 조건만으로 묶인 컬럼들 사이에서는 순서를 고민할 필요가 없다
'[sqlp] > 헷갈리는 부분 정리' 카테고리의 다른 글
| 소트 머지 조인 (0) | 2026.05.25 |
|---|---|
| NL조인, 인덱스, 디스크 I/O (1) | 2026.05.16 |