B-Tree와 B+Tree
데이터베이스의 기본 인덱스가 이진 탐색 트리(BST)가 아니라 B+Tree인 데에는 명확한 이유가 있습니다. 디스크 I/O를 최소화하도록 설계된 자료구조이기 때문입니다. 디스크 접근은 메모리 접근보다 수만~수십만 배 느리므로, 디스크 접근 횟수를 줄이는 것이 데이터베이스 성능의 핵심입니다.
이진 탐색 트리의 한계
이진 탐색 트리는 메모리에서는 효율적이지만 디스크 기반 데이터베이스에서는 문제가 있습니다.
데이터 100만 건 기준
BST (이진 탐색 트리)
깊이 ≈ log₂(1,000,000) ≈ 20
→ 최악 20번 디스크 I/O
B-Tree (차수 100)
깊이 ≈ log₁₀₀(1,000,000) ≈ 3
→ 최대 3번 디스크 I/O
디스크 I/O 1회 ≈ 10ms (SSD: ~0.1ms)
BST: 20 × 10ms = 200ms
B-Tree: 3 × 10ms = 30ms
→ 동일한 검색에 6배 이상 성능 차이!BST는 노드당 키가 1개이므로 트리가 깊어집니다. 디스크에서는 한 번 읽을 때 하나의 블록(보통 4KB~8KB)을 가져오므로, 한 노드에 여러 키를 넣는 B-Tree가 유리합니다.
디스크 블록 크기: 8KB
BST 한 노드: 키 1개 + 포인터 2개 ≈ 수십 바이트
→ 8KB 블록에 키 1개만 사용 → 대부분 낭비
B-Tree 한 노드: 키 수백 개 + 포인터 수백 개
→ 8KB 블록을 꽉 채워 사용
→ 한 번 디스크 접근으로 수백 개 키 비교 가능
핵심: 디스크 블록 활용률을 최대화하여 I/O 횟수를 최소화B-Tree의 구조
B-Tree는 하나의 노드에 여러 키를 저장하는 균형 탐색 트리입니다. 1972년 Rudolf Bayer와 Edward McCreight가 발명했습니다.
B-Tree 용어
차수(Order, m): 한 노드가 가질 수 있는 최대 자식 수
→ 차수 m인 B-Tree
* 루트: 최소 2개 자식 (또는 리프)
* 내부 노드: 최소 ⌈m/2⌉개 자식, 최대 m개 자식
* 각 노드: 최소 ⌈m/2⌉-1개 키, 최대 m-1개 키
* 모든 리프는 같은 깊이
팬아웃(Fan-out): 한 노드에서 나가는 포인터(자식) 수
→ 팬아웃이 클수록 트리가 낮아짐 → 디스크 I/O 감소
노드 종류
* 루트 노드: 트리의 최상위, 최소 2개 자식
* 내부 노드(Internal): 루트와 리프 사이의 중간 노드
* 리프 노드(Leaf): 최하위 노드, 자식 없음B-Tree 구조 예시
[30, 60] ← 루트
/ | \
[10, 20] [40, 50] [70, 80, 90] ← 리프
키 30의 왼쪽: 30보다 작은 키들 (10, 20)
키 30과 60 사이: 30~60 사이의 키들 (40, 50)
키 60의 오른쪽: 60보다 큰 키들 (70, 80, 90)B-Tree의 특성
| 특성 | 설명 |
|---|---|
| 균형 트리 | 모든 리프 노드의 깊이가 같음 |
| 다진 트리 | 한 노드에 여러 키 저장 (차수 m이면 최대 m-1개 키) |
| 정렬 유지 | 키가 정렬된 상태로 저장 |
| 자동 균형 | 삽입·삭제 시 자동으로 균형 유지 (Split, Merge) |
| 데이터 위치 | 모든 노드에 데이터(또는 포인터) 저장 가능 |
| 시간 복잡도 | 검색, 삽입, 삭제 모두 O(log N) |
B-Tree 검색 과정
[30, 60] ← 1단계: 30 < 45 < 60
/ | \
[10, 20] [40, 50] [70, 80] ← 2단계: 40 < 45 < 50
1단계: 루트 [30, 60] 확인 → 45는 30과 60 사이 → 가운데 자식으로
2단계: [40, 50] 확인 → 40 < 45 < 50 → 45가 이 노드에 없음
결과: 45는 트리에 없음 (검색 실패)
디스크 I/O: 2회 (루트 1번 + 내부 1번)B+Tree — 데이터베이스의 표준 인덱스
B+Tree는 B-Tree를 개선한 변형으로, 대부분의 RDB가 기본 인덱스로 사용합니다.
B-Tree와의 핵심 차이: 실제 데이터(또는 ROWID)가 리프 노드에만 있고, 리프 노드끼리 연결 리스트로 연결됩니다.
내부 노드 (키만 저장, 가이드 역할)
[30]
/ \
[10, 20] [30, 40, 50] ← 내부 노드 (키만)
리프 노드 (키 + 데이터 포인터, 연결 리스트)
[10,ptr]─→[20,ptr]─→[30,ptr]─→[40,ptr]─→[50,ptr]
←──── 이중 연결 리스트 ────→
ptr = 실제 데이터 행을 가리키는 ROWID/포인터B+Tree의 특성
1. 데이터는 리프 노드에만 저장
→ 내부 노드는 키만 저장 (라우팅 역할)
→ 내부 노드에 더 많은 키를 넣을 수 있음 → 팬아웃 증가
2. 리프 노드끼리 연결 리스트
→ 범위 검색 시 트리를 다시 올라갈 필요 없음
→ 리프에서 좌→우 순차 탐색만으로 범위 결과 획득
3. 내부 노드의 키는 리프에도 존재
→ 내부 노드의 키는 "라우팅 키"
→ 같은 키 값이 내부 노드와 리프 노드에 모두 나타남
4. 리프 노드의 키는 항상 정렬 상태
→ ORDER BY 최적화에 유리B-Tree vs B+Tree 상세 비교
| 비교 항목 | B-Tree | B+Tree |
|---|---|---|
| 데이터 위치 | 모든 노드에 가능 | 리프 노드에만 |
| 리프 연결 | 없음 | 이중 연결 리스트 |
| 범위 검색 | 중위 순회 필요 (비효율) | 리프에서 순차 탐색 (효율적) |
| 내부 노드 크기 | 데이터 포함으로 큼 | 키만 저장하므로 작음 |
| 같은 블록에 키 수 | 적음 (데이터 크기 차지) | 많음 (팬아웃 높음) |
| 동일 데이터 검색 | 내부 노드에서 끝날 수 있음 | 항상 리프까지 내려감 |
| 검색 시간 일관성 | 불균일 (키 위치에 따라 다름) | 균일 (항상 같은 깊이) |
| 실제 DBMS 사용 | 거의 없음 | Oracle, MySQL, PostgreSQL 등 표준 |
8KB 블록, 키 크기 8바이트, 포인터 8바이트, 데이터 100바이트:
B-Tree
한 노드 키 수 ≈ 8192 / (8 + 8 + 100) ≈ 70개
→ 팬아웃 ≈ 71
B+Tree (내부 노드)
한 노드 키 수 ≈ 8192 / (8 + 8) ≈ 512개
→ 팬아웃 ≈ 513
B+Tree가 약 7배 더 넓은 트리!
→ B-Tree 깊이 4인 곳에서 B+Tree는 깊이 3
→ 디스크 I/O 1회 절약범위 검색 비교
B+Tree가 RDB의 표준이 된 핵심 이유는 범위 검색(Range Scan) 성능입니다.
SELECT * FROM products WHERE price BETWEEN 100 AND 500;
B+Tree
1. 루트에서 100을 찾아 리프까지 내려감 (2~3번 I/O)
2. 리프의 연결 리스트를 따라 500까지 순차 읽기
3. 데이터가 연속된 블록에 저장되어 시퀀셜 I/O 활용
→ 매우 효율적!
B-Tree
1. 100을 찾은 뒤 중위 순회(In-order Traversal)
2. 부모→자식→부모→형제를 왔다 갔다
3. 랜덤 I/O가 발생하여 성능 저하
→ 비효율적
실제 쿼리에서 범위 검색은 매우 빈번
WHERE date BETWEEN '2024-01-01' AND '2024-12-31'
WHERE salary >= 3000000
WHERE name LIKE '김%' (인덱스 범위 스캔)B+Tree 삽입과 분할
B+Tree에 새 키를 삽입할 때 리프 노드가 꽉 차면 Split(분할)이 발생합니다.
삽입 과정
1단계: 적절한 리프 노드 찾기 (루트부터 탐색)
2단계: 리프에 키 삽입 (정렬 순서 유지)
3단계-A: 공간이 있으면 완료
3단계-B: 공간이 없으면 리프 분할 (Split)
리프 분할
* 키를 반으로 나누어 두 리프로 분리
* 오른쪽 리프의 첫 키를 부모에 복사 (Copy-up)
* 부모도 꽉 차면 부모도 분할 (이때는 Push-up)
내부 노드 분할
* 키를 반으로 나누고 중간 키를 부모로 올림 (Push-up)
* 리프 분할은 Copy-up, 내부 분할은 Push-up차수 3 (리프 최대 2개 키, 내부 최대 2개 키):
1. 초기 상태
[30]
/ \
[10, 20] [30, 40]
2. 35 삽입 → 리프 [30, 40]에 35 추가 → [30, 35, 40] 초과!
3. 리프 분할
[30, 35, 40] → [30] | [35, 40]
오른쪽 첫 키 35를 부모에 Copy-up
4. 분할 후
[30, 35]
/ | \
[10, 20] [30] [35, 40]
← 리프 연결 리스트 →삭제 과정
1단계: 키가 있는 리프 노드 찾기
2단계: 리프에서 키 삭제
3단계-A: 최소 키 수를 만족하면 완료
3단계-B: 최소 키 수 미달 → 재분배 또는 병합
재분배 (Redistribution)
* 형제 노드에서 키를 빌려옴
* 부모 노드의 구분 키도 갱신
병합 (Merge)
* 형제 노드와 합침
* 부모 노드에서 구분 키 삭제
* 부모도 최소 키 수 미달이면 연쇄 병합차수 3 (리프 최소 1개 키)
1. 초기
[30, 35]
/ | \
[10, 20] [30] [35, 40]
2. 30 삭제 → 리프 [30]에서 삭제 → [] 비어짐!
3. 형제 [10, 20]에서 20을 빌려옴 (재분배)
[20, 35]
/ | \
[10] [20] [35, 40]
또는 형제와 병합
[35]
/ \
[10, 20] [35, 40]B+Tree 높이와 저장 용량
차수 200 (한 노드에 최대 199개 키), 리프에 100개 레코드
높이 1 (루트만): 199개 키
높이 2 (루트+리프): 200 × 100 = 20,000건
높이 3: 200 × 200 × 100 = 4,000,000건 (400만)
높이 4: 200³ × 100 = 800,000,000건 (8억)
핵심: 높이 3~4면 수억 건의 데이터를 처리 가능!
→ 검색 시 디스크 I/O는 3~4회면 충분
→ 루트와 상위 내부 노드는 보통 버퍼 캐시에 상주
→ 실제 디스크 I/O는 1~2회로 감소DBMS에서의 실제 B+Tree 구현
Oracle
* 인덱스 블록 크기: DB_BLOCK_SIZE (보통 8KB)
* 높이 확인: SELECT blevel FROM dba_indexes WHERE ...
* blevel=0이면 루트만, blevel=2이면 높이 3
MySQL InnoDB
* 페이지 크기: innodb_page_size (기본 16KB)
* 클러스터드 인덱스: PK가 B+Tree 리프에 전체 행 저장
* 세컨더리 인덱스: PK 값을 포인터로 저장
PostgreSQL
* 블록 크기: 8KB (기본)
* B+Tree의 변형: Lehman-Yao B+Tree 사용
* 동시성 제어 최적화: 형제 포인터 + 하이 키(High Key)-- Oracle: B-Tree 높이 확인
SELECT index_name, blevel, leaf_blocks, num_rows
FROM user_indexes
WHERE table_name = 'PRODUCTS';
-- blevel + 1 = B-Tree 높이
-- MySQL: 인덱스 통계
SELECT stat_name, stat_value
FROM mysql.innodb_index_stats
WHERE table_name = 'products'
AND stat_name IN ('size', 'n_leaf_pages');B+Tree 외의 인덱스 자료구조
B+Tree가 범용적이지만 모든 상황에 최적은 아닙니다.
┌───────────────┬───────────────────┬─────────────────────┐
│ 자료구조 │ 장점 │ 적합한 상황 │
├───────────────┼───────────────────┼─────────────────────┤
│ B+Tree │ 범용, 범위 검색 │ 대부분의 인덱스 │
│ Hash Index │ 동등 검색 O(1) │ = 조건 검색 │
│ Bitmap Index │ 낮은 카디널리티 │ 성별, 상태 (OLAP) │
│ R-Tree │ 공간 검색 │ 위치/지도 (GIS) │
│ GIN/GiST │ 전문 검색, 배열 │ PostgreSQL 텍스트 │
│ LSM-Tree │ 쓰기 최적화 │ 로그, 시계열 DB │
└───────────────┴───────────────────┴─────────────────────┘
Hash Index
* 동등 검색(=)에 O(1)이지만 범위 검색 불가
* MySQL Memory 엔진에서 지원, InnoDB는 Adaptive Hash Index
Bitmap Index
* 각 값에 비트맵 할당, AND/OR 연산이 빠름
* Oracle에서 OLAP용으로 활용
* 값의 종류가 적은 컬럼(성별, 상태)에 적합Hash Index 상세
해시 함수: h(key) → 버킷 번호
검색: key=100
1. h(100) = 3 → 버킷 3으로 바로 접근
2. 버킷 3에서 key=100인 행 포인터 반환
→ O(1) 검색!
그러나
WHERE price > 100 → 해시 값으로 대소 비교 불가
WHERE name LIKE 'A%' → 해시 값이 원래 순서와 무관
ORDER BY price → 해시 인덱스는 정렬 순서 제공 불가-- Memory 엔진은 Hash 인덱스가 기본
CREATE TABLE sessions (
session_id VARCHAR(64),
user_id INT,
data TEXT,
INDEX USING HASH (session_id)
) ENGINE = MEMORY;
-- InnoDB Adaptive Hash Index
-- 자주 접근되는 인덱스 페이지를 자동으로 해시 인덱스로 구성
-- 별도 설정 없이 InnoDB가 자동 관리
SHOW VARIABLES LIKE 'innodb_adaptive_hash_index';Bitmap Index 상세
employees 테이블의 gender 컬럼
M: [1, 0, 1, 1, 0, 1, 0, 0, 1, 1] ← 비트맵
F: [0, 1, 0, 0, 1, 0, 1, 1, 0, 0]
WHERE gender = 'M' AND dept = 'IT'
gender_M: [1, 0, 1, 1, 0, 1, 0, 0, 1, 1]
dept_IT: [1, 1, 0, 0, 1, 0, 0, 1, 0, 0]
AND 연산: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0] ← 1번 행만 해당!
장점: 비트 연산으로 여러 조건의 AND/OR를 매우 빠르게 처리
단점: DML이 빈번하면 비트맵 갱신 비용이 큼 → OLTP에 부적합LSM-Tree 개요
쓰기 최적화 자료구조
1. 쓰기 → 메모리의 MemTable에 먼저 기록 (매우 빠름)
2. MemTable이 꽉 차면 → 디스크에 SSTable로 Flush
3. SSTable들이 쌓이면 → 백그라운드에서 Compaction (병합 정렬)
읽기
MemTable → 최신 SSTable → 오래된 SSTable 순서로 검색
→ 읽기는 B+Tree보다 느릴 수 있음
사용처
* RocksDB (MySQL MyRocks 엔진)
* LevelDB (Google)
* Cassandra, HBase
* 로그 수집, 시계열 데이터 등 쓰기 비중이 높은 시스템
B+Tree vs LSM-Tree
┌────────────┬──────────┬──────────┐
│ 항목 │ B+Tree │ LSM-Tree │
├────────────┼──────────┼──────────┤
│ 읽기 성능 │ 우수 │ 보통 │
│ 쓰기 성능 │ 보통 │ 우수 │
│ 공간 효율 │ 보통 │ 우수 │
│ 쓰기 증폭 │ 높음 │ 낮음 │
│ 읽기 증폭 │ 낮음 │ 높음 │
│ 사용 사례 │ OLTP │ 로그/IoT │
└────────────┴──────────┴──────────┘B+Tree 연습 문제
문제: 차수 200인 B+Tree, 리프 노드당 최대 100개 레코드 저장
총 5000만 건의 데이터를 저장하려면 최소 높이는?
풀이
높이 3: 200 × 200 × 100 = 4,000,000건 (부족)
높이 4: 200 × 200 × 200 × 100 = 800,000,000건 (충분)
답: 높이 4 (디스크 I/O 최대 4회)문제: 다음 중 B+Tree의 특성이 아닌 것은?
A. 모든 리프 노드의 깊이가 같다
B. 내부 노드에도 데이터가 저장된다 ← 정답!
C. 리프 노드끼리 연결 리스트로 연결된다
D. 범위 검색에 효율적이다
해설: B+Tree에서 데이터는 리프 노드에만 저장됩니다.
내부 노드는 검색을 위한 라우팅 키만 저장합니다.
B는 B-Tree의 특성입니다.상황별 적합한 인덱스
1. WHERE user_id = 12345 (동등 검색, PK)
→ B+Tree (기본) 또는 Hash Index
2. WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31'
→ B+Tree (범위 검색)
3. WHERE gender = 'M' AND status = 'ACTIVE' AND grade = 'VIP'
→ Bitmap Index (낮은 카디널리티, 다중 조건)
4. WHERE ST_Distance(location, POINT(37.5, 127)) < 1000
→ R-Tree (공간 검색)
5. IoT 센서 데이터 실시간 저장
→ LSM-Tree (쓰기 위주)정리
| 개념 | 설명 |
|---|---|
| B-Tree | 다진 균형 탐색 트리, 모든 노드에 데이터 가능 |
| B+Tree | B-Tree 개선, 데이터는 리프에만, 리프 연결 리스트 |
| 차수(Order) | 한 노드의 최대 자식 수 |
| 팬아웃 | 한 노드에서 나가는 포인터 수, 클수록 트리 낮아짐 |
| Split | 노드 초과 시 분할, 키를 상위로 올림 |
| Merge | 노드 부족 시 형제와 병합 |
| 범위 검색 | B+Tree의 핵심 강점, 리프 순차 탐색 |
| 높이 | 보통 3~4, 수억 건 데이터 처리 가능 |
| Hash Index | 동등 검색 O(1), 범위 검색 불가 |
| Bitmap Index | 비트 연산으로 다중 조건 빠르게 처리 |
| LSM-Tree | 쓰기 최적화, 로그/시계열 데이터에 적합 |
B+Tree는 디스크 I/O를 최소화하도록 설계된 자료구조입니다. 한 노드에 수백 개의 키를 저장하여 트리 높이를 낮추고, 리프 노드의 연결 리스트를 통해 범위 검색을 효율적으로 처리합니다. 대부분의 RDBMS는 B+Tree를 기본 인덱스 구조로 사용하며, InnoDB의 클러스터드 인덱스는 B+Tree의 리프에 전체 행 데이터를 저장하는 특수한 형태입니다.
다음 절에서는 클러스터드 인덱스, 비클러스터드 인덱스 등 인덱스의 종류를 다루겠습니다.