B-Tree와 B+Tree
데이터베이스의 기본 인덱스가 이진 탐색 트리(BST)가 아니라 B+Tree 계열인 데에는 명확한 이유가 있습니다. 디스크와 SSD, 버퍼 캐시, 페이지 단위 I/O에 맞춰 트리 높이를 낮추고 범위 탐색을 쉽게 만드는 자료구조이기 때문입니다. 실제 성능은 캐시 적중률과 저장 장치에 따라 달라지지만, 페이지 접근 횟수를 줄이는 것이 데이터베이스 인덱스 설계의 핵심입니다.
이진 탐색 트리의 한계
이진 탐색 트리는 메모리에서는 효율적이지만 디스크 기반 데이터베이스에서는 문제가 있습니다.
BST는 노드당 키가 1개라서 같은 데이터 수에서도 트리가 깊어지기 쉽습니다. 디스크 기반 DBMS는 보통 페이지나 블록 단위로 데이터를 읽으므로, 한 페이지에 여러 키와 포인터를 담아 fan-out을 높이는 B-Tree 계열이 유리합니다.
디스크 블록 크기: 8KB
BST 방식: 키 1개 + 좌/우 포인터 중심
→ 트리 깊이가 커지고 페이지 접근이 늘기 쉬움
B-Tree 한 노드: 작은 키 기준으로 키 수십~수백 개 + 포인터 수십~수백 개
→ 한 페이지 안에서 많은 분기 후보를 비교
→ fan-out이 커져 루트에서 리프까지 단계 수 감소
핵심: 페이지당 분기 수를 늘려 트리 높이와 페이지 접근 횟수를 줄임B-Tree의 구조
B-Tree는 하나의 노드에 여러 키를 저장하는 균형 탐색 트리입니다. 1972년 Rudolf Bayer와 Edward McCreight가 발명했습니다.
B-Tree 용어
B-Tree 구조 예시
B-Tree의 특성
| 특성 | 설명 |
|---|---|
| 균형 트리 | 모든 리프 노드의 깊이가 같음 |
| 다진 트리 | 한 노드에 여러 키 저장 (차수 m이면 최대 m-1개 키) |
| 정렬 유지 | 키가 정렬된 상태로 저장 |
| 자동 균형 | 삽입·삭제 시 자동으로 균형 유지 (Split, Merge) |
| 데이터 위치 | 모든 노드에 데이터(또는 포인터) 저장 가능 |
| 시간 복잡도 | 검색, 삽입, 삭제 모두 트리 높이에 비례 |
여기서 시간 복잡도는 이론적으로는 O(log n)에 가깝지만, DBMS 관점에서는 몇 개의 페이지를 읽고 쓰는가가 더 중요합니다. 루트와 상위 브랜치 페이지는 버퍼 캐시에 머무는 경우가 많고, 실제 병목은 리프 페이지와 테이블 행 접근에서 발생하기 쉽습니다.
B-Tree 검색 과정
B+Tree — 데이터베이스의 표준 인덱스
B+Tree는 B-Tree를 데이터베이스 인덱스에 맞게 변형한 구조입니다. 제품 문서에서는 단순히 B-Tree라고 부르더라도, 실제 구현은 리프 연결, 페이지 분할, 동시성 제어가 결합된 B+Tree 계열인 경우가 많습니다.
B-Tree와의 핵심 차이: 행 위치 정보나 실제 행 데이터가 리프 노드에 모이고, 리프 노드끼리 순서대로 연결됩니다. 이때 리프에 저장되는 것은 DBMS에 따라 ROWID, TID, 기본 키 값, 전체 행 데이터처럼 달라집니다.
B+Tree의 특성
B-Tree vs B+Tree 상세 비교
| 비교 항목 | B-Tree | B+Tree 계열 |
|---|---|---|
| 데이터 위치 | 내부/리프 노드 모두 가능 | 리프 노드 중심 |
| 리프 연결 | 일반적으로 없음 | 리프 페이지가 순서대로 연결 |
| 범위 검색 | 트리 순회가 필요 | 리프에서 순차 탐색 |
| 내부 노드 크기 | 데이터 포함 시 커질 수 있음 | 라우팅 키 중심이라 fan-out이 커짐 |
| 같은 블록에 키 수 | 상대적으로 적을 수 있음 | 상대적으로 많음 |
| 동일 데이터 검색 | 내부 노드에서 끝날 수 있음 | 보통 리프까지 내려감 |
| 검색 시간 일관성 | 키 위치에 따라 달라질 수 있음 | 리프 깊이가 같아 접근 경로가 안정적 |
| 실제 DBMS 사용 | 이론 설명에 자주 사용 | RDB 인덱스 구현의 기본 계열 |
범위 검색 비교
B+Tree 계열이 RDB 인덱스 구현에 널리 쓰이는 핵심 이유 중 하나는 범위 검색(Range Scan) 성능입니다.
B+Tree 삽입과 분할
B+Tree에 새 키를 삽입할 때 리프 노드가 꽉 차면 Split(분할)이 발생합니다.
삽입 과정
1단계: 적절한 리프 노드 찾기 (루트부터 탐색)
2단계: 리프에 키 삽입 (정렬 순서 유지)
3단계-A: 공간이 있으면 완료
3단계-B: 공간이 없으면 리프 분할 (Split)
리프 분할
* 키를 반으로 나누어 두 리프로 분리
* 오른쪽 리프의 첫 키를 부모에 복사 (Copy-up)
* 부모도 꽉 차면 부모도 분할 (이때는 Push-up)
내부 노드 분할
* 키를 반으로 나누고 중간 키를 부모로 올림 (Push-up)
* 리프 분할은 Copy-up, 내부 분할은 Push-up
실제 DBMS에서는 fill factor, page split 정책, redo/undo 로그, latch/lock, 동시성 제어가 함께 개입합니다. Copy-up/Push-up은 개념 설명용 모델이며, 실제 페이지 분할 세부 동작은 저장 엔진마다 다를 수 있습니다.삭제 과정
삭제 후 노드가 비어도 DBMS가 항상 즉시 병합하는 것은 아닙니다. 빈 공간을 재사용하거나, 나중에 정리하거나, 구조 안정성을 위해 병합을 늦출 수 있습니다.
B+Tree 높이와 저장 용량
트리 높이는 차수뿐 아니라 키 길이, 포함 컬럼, 페이지 크기, fill factor, 삭제 후 빈 공간, 압축 여부에 따라 달라집니다. 그래서 “높이 3~4면 충분하다”는 말은 작은 키와 잘 채워진 페이지를 가정한 감각적 기준이며, 운영에서는 실제 인덱스 페이지 수와 실행 계획을 함께 확인해야 합니다.
DBMS에서의 실제 B-Tree/B+Tree 계열 구현
Oracle
* 문서상 B-tree index
* 인덱스 블록 크기: DB_BLOCK_SIZE
* 일반 인덱스 엔트리는 키와 ROWID를 저장
* 높이 확인: SELECT blevel FROM dba_indexes WHERE ...
* blevel=0이면 루트만, blevel=2이면 높이 3
MySQL InnoDB
* 페이지 크기: innodb_page_size (기본 16KB)
* 클러스터드 인덱스: 기본 키 B+Tree 리프에 전체 행 저장
* 세컨더리 인덱스: 리프에 보조 키와 기본 키 컬럼 저장
* 보조 인덱스 조회 후 기본 키로 클러스터드 인덱스를 다시 탐색
PostgreSQL
* 블록 크기: 8KB (기본)
* 문서상 B-tree access method, 구현은 Lehman-Yao 계열
* 리프 엔트리는 heap tuple 위치인 TID를 가리킴
* 동시성 제어 최적화: 형제 포인터 + 하이 키(High Key)
SQL Server
* clustered index 리프에는 데이터 행 저장
* nonclustered index 리프에는 row locator 저장
* row locator는 heap RID 또는 clustered key
SQLite
* rowid table은 rowid B-tree를 중심으로 저장
* WITHOUT ROWID table은 primary key B-tree로 저장-- 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 database_name = 'shop'
AND table_name = 'products'
AND stat_name IN ('size', 'n_leaf_pages');B+Tree 외의 인덱스 자료구조
B+Tree가 범용적이지만 모든 상황에 최적은 아닙니다.
Hash Index 상세
-- 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
-- 자주 접근되는 인덱스 페이지를 자동으로 해시 인덱스로 구성
-- 사용자가 직접 만드는 Hash Index가 아니라 InnoDB 내부 최적화이며 설정으로 끌 수 있음
SHOW VARIABLES LIKE 'innodb_adaptive_hash_index';Bitmap Index 상세
LSM-Tree 개요
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는 캐시된 루트/브랜치 페이지와 테이블 행 접근 여부에 따라 달라짐문제: 다음 중 B+Tree의 특성이 아닌 것은?
A. 모든 리프 노드의 깊이가 같다
B. 내부 노드에도 데이터가 저장된다 ← 정답!
C. 리프 노드끼리 연결 리스트로 연결된다
D. 범위 검색에 효율적이다
해설: B+Tree에서 실제 행으로 이어지는 위치 정보나 행 데이터는 리프 노드 중심에 저장됩니다.
내부 노드는 검색을 위한 라우팅 키와 자식 포인터 중심으로 구성됩니다.
B는 B-Tree의 특성입니다.정리
| 개념 | 설명 |
|---|---|
| B-Tree | 다진 균형 탐색 트리, 모든 노드에 데이터 가능 |
| B+Tree | B-Tree 개선, 행 위치/행 데이터는 리프 중심, 리프 연결 |
| 차수(Order) | 한 노드의 최대 자식 수 |
| 팬아웃 | 한 노드에서 나가는 포인터 수, 클수록 트리 낮아짐 |
| Split | 노드 초과 시 분할, 경계 키를 부모에 반영 |
| Merge | 노드 부족 시 재분배·병합·공간 재사용 중 선택 |
| 범위 검색 | B+Tree의 핵심 강점, 리프 순차 탐색 |
| 높이 | 보통 3~4, 수억 건 데이터 처리 가능 |
| Hash Index | 동등 검색 평균 O(1), 범위 검색에 부적합 |
| Bitmap Index | 저카디널리티 분석 조건에 강하지만 갱신 비용 큼 |
| LSM-Tree | 쓰기 최적화, compaction과 read amplification 존재 |
B+Tree 계열은 페이지 접근 횟수를 줄이도록 설계된 자료구조입니다. 한 노드에 많은 키를 저장해 fan-out을 높이고, 리프 노드의 연결을 통해 범위 검색을 효율적으로 처리합니다. 대부분의 RDBMS 인덱스는 B+Tree 계열 구현을 기본으로 삼으며, InnoDB의 클러스터드 인덱스처럼 리프에 전체 행 데이터를 저장하는 변형도 있습니다.
다음 절에서는 클러스터드 인덱스, 비클러스터드 인덱스 등 인덱스의 종류를 다루겠습니다.