본문으로 건너뛰기
김신건의 로그

[DB Internals] B+ Tree 내부: leaf linked list, range scan

· 수정 · 📖 약 2분 · 562자/단어 #btree #b-plus-tree #database #index #range-query
B+ Tree, B+ Tree leaf, clustered index, range scan, B+Tree vs B-Tree, InnoDB B+Tree

정의

B+ Tree = B-Tree 의 변형. 모든 데이터가 leaf 에만, internal node 는 navigation key만. Leaf 들이 linked list 로 연결.

IMPORTANT

현대 RDB 의 표준 인덱스. PostgreSQL, MySQL InnoDB, Oracle, SQL Server 모두 B+ Tree 기반.

B-Tree vs B+ Tree

flowchart TB
    subgraph BT["B-Tree (internal 도 데이터 저장)"]
        BT_R["Root: 30(data) | 60(data)"]
        BT_L1["Leaf: 10(d)|20(d)|25(d)"]
        BT_L2["Leaf: 40(d)|50(d)"]
        BT_R --> BT_L1
        BT_R --> BT_L2
    end
    subgraph BP["B+ Tree (internal 은 key 만)"]
        BP_R["Root: 30 | 60 (navigation)"]
        BP_L1["Leaf: 10,20,25 (data)"]
        BP_L2["Leaf: 40,50 (data)"]
        BP_L3["Leaf: 70,80,90 (data)"]
        BP_R --> BP_L1
        BP_R --> BP_L2
        BP_R --> BP_L3
        BP_L1 -.next.-> BP_L2
        BP_L2 -.next.-> BP_L3
    end
B-TreeB+ Tree
Internal 에 데이터아니오 (key 만)
Leaf 연결 (linked list)아니오
Range scan각 노드 순회linked list scan (빠름!)
Fan-out작음 (data 도 저장) (navigation key 만)
트리 깊이깊음얕음
Point lookupearly exit 가능항상 leaf 까지
공간 효율나쁨 (중복)좋음

Range Scan 의 우월성

SELECT * FROM orders WHERE created_at BETWEEN '2026-06-01' AND '2026-06-30';
sequenceDiagram
    autonumber
    Q->>Root: search '2026-06-01'
    Root->>Internal: navigate
    Internal->>Leaf1: found start
    loop leaf linked list traversal
        Leaf1-->>Q: return rows
        Leaf1->>Leaf2: next pointer
        Leaf2-->>Q: return rows
        Leaf2->>Leaf3: next pointer
        Leaf3-->>Q: return rows until > '2026-06-30'
    end

Leaf 만 순회 → disk sequential I/O. B-Tree 는 매번 root 부터 다시 시작해야 함.

Clustered Index (InnoDB)

MySQL InnoDB 는 Primary Key 의 B+ Tree leaf 가 실제 row 데이터:

flowchart TB
    Root["Root: navigation keys"]
    Root --> L1["Leaf 1: full row data (PK=1..100)"]
    Root --> L2["Leaf 2: full row data (PK=101..200)"]
    Root --> L3["Leaf 3: full row data (PK=201..300)"]
    L1 -.next.-> L2
    L2 -.next.-> L3
    Note["Secondary index → PK 값 저장 → 다시 클러스터 인덱스 조회"]

vs PostgreSQL 은 heap 별도, 인덱스는 heap tuple ID 만. 자세한 건 mysql-innodb, postgresql.

Leaf Linked List

Leaf 1: [10, 20, 25] → Leaf 2: [40, 50] → Leaf 3: [70, 80, 90] → null

각 leaf 페이지 header:

- prev_leaf pointer
- next_leaf pointer
- n_keys
- keys[] + values[] (or row data)

TIP

Reverse range scan 도 O(k). prev_leaf 로 역방향 순회.

Fan-out 계산 (실전)

InnoDB 페이지 = 16KB:

Internal node fan-out:
  key (bigint 8B) + pointer (6B) = 14B
  16KB / 14B ≈ 1170 children

Height 계산:
  N = 10^9, fan-out = 1170
  h = ceil(log_1170(10^9)) ≈ 3
  → 3 disk read 로 도달!

Split 알고리즘 (B+ Tree)

flowchart TB
    Before["Leaf: [10, 20, 30, 40, 50, 60] (full)"]
    Before --> Split
    subgraph Split["Split (median=30)"]
        L["New left: [10, 20, 30]"]
        R["New right: [40, 50, 60]"]
    end
    Split --> Parent["Parent 에 40 push (30이 아님!)"]
    Parent -->|"prev/next 갱신"| Linked["Linked list 유지"]

B+ Tree 는 중간 key 를 복사 해서 parent 에 push (B-Tree 는 이동). Leaf 에도 유지되어야 range scan 가능.

Sequential Insert 의 함정

PK = auto-increment (1, 2, 3, ...) 만 삽입
→ 항상 *마지막 leaf* 에만 삽입
→ 마지막 leaf 만 자꾸 split
→ tree 오른쪽으로 편향 성장

InnoDB 는 change buffer 로 완화. 다른 DB 는 bulk load 시 자동 balance.

Random Insert 의 함정

UUID v4 primary key:

UUID_v4 = random
→ 매 insert 마다 *다른 leaf 페이지*
→ Cache miss + 페이지 분할 폭증
→ 쓰기 성능 5-10배 저하

해결:

  • Auto-increment BIGINT (관계형 정통)
  • ULID / UUID v7 (시간 순 정렬 가능)
  • InnoDB: innodb_autoinc_lock_mode 조정

Reverse Scan + ORDER BY DESC

SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
  • B+ Tree 의 leaf prev pointer 로 마지막에서 역행.
  • MySQL InnoDB 는 자동 지원.
  • PostgreSQL 은 reverse index 옵션 (CREATE INDEX ... DESC).

Concurrency: Latch Coupling

sequenceDiagram
    autonumber
    T->>Root: 잠금
    Root-->>T: children
    T->>Child: 잠금
    T->>Root: 잠금 해제
    T->>Grandchild: 잠금
    T->>Child: 잠금 해제
    Note over T,Grandchild: 항상 위→아래 순서 → deadlock free

InnoDB, PG 모두 latch coupling 기본. 자세한 건 b-tree-internals.

Bulk Loading

COPY orders FROM '...' -- 대량 로드

B+ Tree 를 bottom-up 으로 구축 (sort + fill):

  1. 데이터 정렬 (외부 정렬).
  2. Leaf 페이지 sequential fill (fillfactor 90%).
  3. Internal 페이지 bottom-up 생성.

일반 insert 보다 수배 빠름.

Fillfactor

CREATE INDEX idx_orders ON orders(id) WITH (fillfactor = 90);
  • 90% = 페이지의 10% 여유. Insert 시 분할 지연.
  • 100% = 쓰기 성능 저하 + 잦은 분할.
  • Read-only 인덱스는 100% (공간 절약).

흔한 함정

WARNING

  1. UUID v4 PK = 성능 킬러. UUID v7 / ULID / BIGINT.
  2. Fillfactor 100 = 쓰기 많은 테이블에서 분할 폭증.
  3. Descending index 안 만듦 = 자주 ORDER BY x DESC 하면 성능 손해.
  4. Multi-column index 순서 잘못 = 가장 선택도 높은 컬럼을 앞으로.

관련 위키

이 글의 용어 (5개)
[DB Internals] B-Tree 내부: split, merge, search 알고리즘database-internals
정의 B-Tree (Bayer & McCreight, 1972) = 디스크 I/O 최적화된 balanced multi-way search tree. 모든 leaf 깊이 동일 + …
[DB Internals] B-Tree와 B+Tree 인덱싱database-internals
정의 B-Tree (B+ Tree) 는 거의 모든 RDB 의 인덱스 자료구조. balanced tree, 디스크 I/O 최소화 설계. 특징: - 균형 (balanced): 모든 …
[DB Internals] R-Tree: 공간 인덱스, MBR, splitdatabase-internals
정의 R-Tree (Guttman, 1984) = 공간 데이터 (2D, 3D) 를 인덱싱하는 balanced tree. 각 노드는 Minimum Bounding Rectangle…
[DB] MySQL / InnoDB: clustered index, redo log, MVCCdatabase-internals
정의 MySQL + InnoDB (기본 스토리지 엔진) 의 clustered index + redo log 기반 행 기반 RDBMS. 웹 / SaaS 의 가장 흔한 backbon…
[DB] PostgreSQL: 프로세스 모델, MVCC, WAL, 확장성database-internals
정의 PostgreSQL 은 오픈소스 ORDBMS. 1986 UC Berkeley POSTGRES 의 후예. MVCC, 확장 가능 타입, JSONB, full-text searc…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기