B-Tree로 설계하는 대용량 인덱스와 범위 질의

B-Tree의 페이지 기반 균형 구조, 분할·병합, 범위 질의와 PostgreSQL 인덱스 설계 활용을 정리한다.

2026-08-14 · 최초 발행 2024-04-29

페이지 접근을 줄이기 위해 높이를 낮춘다

B-Tree는 대규모 데이터 집합에서 탐색·삽입·삭제를 수행하기 위한 균형 다진 탐색 트리다. 관계형 데이터베이스, 키-값 스토어, 파일시스템의 메타데이터 인덱싱에서 널리 쓰인다. 핵심은 정렬된 키를 페이지에 담고, 트리의 높이를 낮춰 랜덤 디스크 I/O를 줄이는 데 있다.

내부 노드는 정렬된 키 집합과 하위 노드 포인터를 보유하며, 모든 리프는 같은 깊이를 유지한다. 차수(order, branching factor), 페이지 크기(page size), 키와 포인터의 크기가 팬아웃(fan-out)을 결정한다. 루트를 제외한 내부 노드는 키 개수의 하한과 상한을 지켜야 하고, 삽입 시에는 분할(split), 삭제 시에는 병합 또는 재분배(rebalance)로 이 규칙을 유지한다.

노드 안의 키와 포인터가 만드는 탐색 경로

내부 노드는 키 K1..Kn과 자식 포인터 P0..Pn으로 이루어진 다진 검색 구조다. 실제 페이지에는 페이지 헤더와 슬롯 디렉터리 같은 메타데이터도 함께 들어간다.

리프에는 B-Tree의 경우 레코드 자체를, B+Tree의 경우 레코드 포인터를 둘 수 있다. 리프를 양방향으로 연결하면 첫 리프를 찾은 뒤 순차 스캔을 이어가기 좋다.

4–16KB 페이지에서는 수백 단위의 팬아웃을 만들 수 있고, 트리 높이를 3–4 레벨로 유지할 수 있다. 점 탐색은 O(log_f N) 페이지 접근 패턴을 따른다. 범위 질의는 첫 리프를 찾은 뒤 연속 페이지를 스캔하므로 대역폭을 활용할 수 있다.

키가 정렬돼 있으므로 BETWEEN, ORDER BY, prefix 매칭처럼 범위 또는 순차 접근이 필요한 질의에 적합하다. 복합키에서는 컬럼 순서에 따라 정렬과 필터 성능이 달라지며, 선택 컬럼을 포함한 커버링 인덱스는 테이블 접근을 줄일 수 있다.

구조 성능(점/범위) 확장성(높이) 일관성/복구 안정성/파편화 운영 편의
B-Tree 점/범위 모두 우수, 분할 시 약간의 쓰기 증폭 수백 팬아웃로 3–4 레벨 유지 용이 WAL/체크포인트로 안정 분할·병합으로 장기 균형 유지 범용 기본값으로 운영 단순
B+Tree 리프만 데이터, 순차 스캔 최적 유사 유사 리프 체인으로 스캔 효율 우수 대다수 DBMS 기본
Hash Index 점 탐색 특화, 범위 비효율 버킷 재해시 비용 고려 재해시·크래시 복구 구현 난도 버킷 편향 시 성능 저하 특정 워크로드 한정 유용

삽입과 삭제에서 균형을 보존하는 방식

삽입은 리프에 여유 공간이 있으면 해당 위치에 엔트리를 추가하는 것으로 끝난다. 공간이 부족하면 리프를 분할하고 분할 키를 상위 노드로 승격(promotion)한다. 상위 노드 역시 가득 차 있다면 분할은 연쇄될 수 있다.

삭제 후 점유율이 하한 아래로 떨어지면 형제 노드에서 엔트리를 재분배하거나 병합한다. 이 과정에서 트리의 높이가 줄어들 수도 있다.

동시성 제어에서는 래치 커플링(latch coupling)으로 상향·하향 잠금을 전파해 구조 변경을 안전하게 처리한다. 레코드 잠금과 구조 래치는 분리해 운용한다. WAL(Write-Ahead Logging)은 로그를 먼저 기록한 뒤 페이지를 수정해 충돌 복구와 일관성을 보장한다.

SearchYesNoInsertYesNoYesNoDeleteNoYesInput: op, key, valueOp?Root latch(S)Descend by key rangeLeaf probeFound?Output: recordDoneOutput: not foundRoot latch(S)Latch-coupling descendLeaf latch(X)Leaf has space?Insert entry, WAL logOutput: okSplit leaf, promoteseparatorParent latch(X)Parent has space?Insert separator, WAL logSplit parent, cascade upDescend to leafDelete entry, WAL logUnderflow?Output: okRedistribute or Merge

중복 키를 허용하는 경우에는 다중 엔트리 체인으로 처리할 수 있고, 허용하지 않으면 고유 제약 위반을 반환한다. 공간 부족은 분할 연쇄로 이어질 수 있으며 트리 높이도 증가할 수 있다. 동시 갱신 충돌에서는 레코드 잠금 대기 또는 타임아웃을 처리하고, 래치 보유 시간은 짧게 유지한다.

인덱스 설계와 스토리지에서의 선택

고빈도 점·범위 질의가 발생하는 테이블에서는 B-Tree 계열을 기본 인덱스 구조로 쓸 수 있다. 복합키는 선택도가 높은 컬럼을 앞에 배치하고, 정렬 요구가 있는 쿼리에는 인덱스의 정렬 순서를 활용한다. DESC/ASC 혼합, 부분 인덱스, 표현식 인덱스도 적용할 수 있다.

InnoDB, PostgreSQL, SQLite 등의 B+Tree 기반 스토리지 엔진은 클러스터형 인덱스와 보조 인덱스를 구현한다. HFS+/APFS와 NTFS는 디렉터리 및 메타데이터 인덱싱에 이 구조를 사용한다.

로그·타임시리즈·멀티테넌시 환경에서는 시간 컬럼을 후행키로 두어 테넌트별 최신 N개 조회를 최적화할 수 있다. Prefix 압축과 커버링 인덱스는 I/O 절감과 캐시 효율 향상에 활용된다.

-- 최신 주문 조회 최적화: 사용자별 + 최신순
CREATE INDEX CONCURRENTLY ix_orders_user_created_at
ON orders USING btree (user_id, created_at DESC)
INCLUDE (status, total_amount);  -- 커버링 컬럼

-- 쿼리 예시: 인덱스 순차 스캔과 LIMIT 적용
SELECT order_id, created_at, status, total_amount
FROM orders
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT 50;

페이지 크기와 팬아웃으로 보는 탐색 비용

탐색 높이를 근사하면 8KB 페이지에서 오버헤드를 제외한 유효 공간은 8,064B다. 내부 키 16B와 포인터 8B를 사용하면 팬아웃 f는 약 8,064 / 24, 즉 약 336이 된다.

N=100,000,000 레코드에서는 log_f(N) ≈ ln(1e8)/ln(336) ≈ 18.42/5.82 ≈ 3.16이므로 높이는 약 4 레벨이다. 점 탐색에는 약 4 페이지의 랜덤 I/O가 필요하고, 범위 스캔은 최초 4 페이지 접근 뒤 리프 연속 스캔 비용이 더해진다.

무작위 삽입에서는 평균 페이지 점유율이 2/3 수준을 유지하는 경향이 있으며, 리프 분할 확률은 O(1/엔트리수) 수준이다. 분할과 병합은 쓰기 증폭을 만들지만 로그·체크포인트 I/O의 예측 가능성을 높이는 효과도 있다.

범용 워크로드에서는 일관된 지연시간 특성을 확보할 수 있고, OLTP/OLAP 혼합 환경에서도 안정적으로 처리할 수 있다. 정렬과 범위 중심 쿼리는 계획을 단순하게 만들며 튜닝 비용을 줄인다. 운영 관점에서는 기본값 인덱스를 선택할 때 안전한 선택지가 된다.

B-TreeB+Tree인덱스범위 질의디스크 I/O