B-Tree vs LSM-Tree
인덱스는 왜 이렇게 갈라지나

정렬을 제자리에 유지할 것인가, 메모리에서 하고 순차로 흘려보낼 것인가 — 디스크의 물리적 성질이 만든 두 갈래.

백엔드 > DB & 스토리지 깊이: 딥-다이브 소크라테스 + 실무 드릴 2026-07-16
01 · 토대
순차 vs 랜덤 I/O
02 · B-Tree
정렬을 제자리에
03 · LSM 쓰기
memtable → SSTable
04 · LSM 읽기
블룸필터 + sparse index
05 · 삭제/컴팩션
툼스톤과 병합
06 · 토론
DBA vs 쓰기폭주파
07 · 트레이드오프
워크로드 → 선택
08 · 실무 드릴
3가지 함정

01디스크의 성질 — 모든 것의 뿌리

인덱스 자료구조를 논하기 전에, 그 밑에 깔린 물리 법칙 하나부터. append(순차 쓰기)는 싸고, random in-place(제자리 수정)는 비싸다. HDD든 SSD든.

시연 · 같은 1건을 쓰는 두 방식

저장매체:

SSD엔 물리적 헤드 이동(seek)이 없는데도 제자리 수정이 비싸다. 페이지(4~16KB)는 덮어쓰기 전에 블록을 통째로 지워야(erase) 하기 때문 — read-modify-write가 터진다. 이 부풀려진 실제 쓰기량을 쓰기 증폭(write amplification)이라 한다.

🔑 한 줄 토대: append는 싸다. random in-place는 비싸다. 두 인덱스의 모든 설계 차이는 이 한 문장에서 갈라진다.

02B-Tree — 정렬을 제자리에 유지

빠른 조회엔 정렬이 필요하다. B-Tree(실제로는 B+Tree)는 데이터를 디스크에 key 순 정렬 상태로 유지한다. 대가는 삽입 시의 random 쓰기.

시연 · 꽉 찬 페이지에 삽입 → 페이지 분할(page split)

한 페이지에 4개까지. 250을 넣어보자.

전체를 밀지 않는다. 꽉 찬 페이지를 둘로 쪼개 절반을 새 페이지로 옮기고, 부모에 포인터(구분키)를 꽂는다. 하지만 페이지 재작성 + 새 페이지 + 부모 수정 = 여러 곳 random 쓰기. 최악엔 부모로 위로 전파된다.

시연 · 팬아웃(fan-out) → 트리 높이 → 단건 조회 비용

내부 노드는 데이터 없이 이정표(key+포인터)만 → 팬아웃 극대화 → 트리가 얕다(3~4단). 그래서 단건 조회 = 디스크 read 3~4번(상위는 캐시), 예측 가능한 낮은 지연. 리프끼리 링크드리스트로 이어져 범위 스캔은 "한 번 내려가 옆으로 훑기".

⚠️ 정렬은 물리적 인접이 아니라 포인터로 유지된다. 그래서 시간이 지나면 논리적으론 연속인데 물리적으론 흩어진다(단편화) → 범위 스캔이 random I/O로 느려질 수 있다.

03LSM — 정렬을 메모리에서 하고 순차로 flush

발상을 뒤집는다. "절대 제자리 수정 안 한다. append만." 쓰기는 폭발적으로 빨라지지만, 순서 없는 로그는 조회가 풀스캔이 된다. 해법: 정렬은 메모리에서, 디스크엔 순차로.

시연 · 쓰기 경로 (memtable이 차면 SSTable로 flush)

memtable 4개 차면 자동 flush

memtable = 정렬을 유지하는 인메모리 버퍼(스킵리스트/균형트리). 차면 얼려서 SSTable(정렬 파일)로 한 번에 순차 flush 후 비운다. 내구성은 WAL(commit log)이 보강 — 크래시 나도 WAL replay로 memtable 복원.

04LSM 읽기 — 블룸필터 + sparse index

데이터가 여러 SSTable에 흩어지면 어떻게 풀스캔 없이 찾나? 파일마다 블룸필터(없는 파일 스킵)와 sparse index(블록당 첫 key)를 붙인다.

시연 · key 조회 — 어떤 파일을 열고 어떤 파일을 건너뛰나

SSTable 1개 = [블룸필터] + [sparse index] + [정렬 블록]. 조회는 최신 파일부터: 블룸필터가 "없음"이면 디스크를 안 열고 스킵(FN 없으니 안전), "있을 수도"면 sparse index로 블록 1개 특정 → 이진탐색. 블룸필터의 false positive는 헛되이 여는 파일을 늘려 read amplification이 된다.

05갱신·삭제·컴팩션 — append-only 세계의 청소

제자리 수정을 안 하니 갱신 = 새로 쌓기(최신 우선), 삭제 = 툼스톤(마커+타임스탬프). 죽은 데이터가 쌓이면 컴팩션(병합정렬)으로 정리한다.

🪦 툼스톤

옛 SSTable은 불변이라 지울 수 없다. 삭제는 최신 파일에 key → [삭제됨] 마커를 쌓는 것. 조회는 최신부터 보다 툼스톤을 먼저 만나면 "없음" 반환.

⚠️ 툼스톤은 옛 값과 병합되기 전엔 못 버린다(gc_grace_seconds). 일찍 버리면 삭제된 key가 부활(좀비).

🔀 컴팩션

정렬된 SSTable들을 병합정렬(순차 read+write)로 합쳐 key별 최신만 남기고 죽은 값·툼스톤을 물리적으로 버린다.

대가 = write amplification. 같은 데이터가 레벨을 타고 내려가며 여러 번 재기록된다.

SSTable ⟷ 파티션 = 다대다. 한 파일에 여러 파티션이 섞이고, 한 파티션이 여러 파일에 흩어진다. 이게 read amplification의 근원이자 컴팩션이 존재하는 이유.

06토론 — DBA vs 쓰기폭주파

정민B-Tree · RDB DBA
대부분의 서비스는 읽기가 많고 point read가 잦아. 트리 3~4홉으로 지연이 딱 예측되는 게 얼마나 소중한데. 괜히 분산 LSM 끌어다 컴팩션 튜닝 지옥 만들지 마.
하람LSM · 쓰기폭주 서비스
그건 쓰기가 한가할 때 얘기지. 초당 수십만 이벤트가 꽂히면 B-Tree의 페이지 분할 random 쓰기가 먼저 무릎 꿇어. LSM은 그걸 전부 순차로 흡수해.
소연질문자
근데 LSM은 읽을 때 여러 파일 뒤진다면서요. 그럼 읽기가 항상 손해 아니에요?
하람LSM
손해라기보단 분산이 커. 평균은 블룸필터·캐시로 괜찮은데, 드문 key나 없는 key가 여러 파일을 다 훑어서 p99 꼬리가 튀지. 지연 민감 서비스엔 그게 아파.
정민B-Tree
그래서 결론은 하나야. "읽기 많음"이 아니라 "쓰기 부담이 내 random 쓰기를 아프게 하느냐"가 갈림길이라는 거. 쓰기가 희박하면 읽기 많아도 나(B-Tree)를 써.
하람LSM
동의해. 그리고 이미 날 쓰고 있다면, 읽기가 나쁘다고 엔진을 갈아엎지 말고 컴팩션 전략(STCS→LCS)부터 봐. 나는 "하나"가 아니라 전략으로 성격이 바뀌는 도구니까.

07트레이드오프 — 워크로드가 결정한다

시연 · 워크로드 → 추천 엔진

읽기:쓰기
p99 지연 민감
B-TreeLSM-Tree
쓰기 패턴random in-place (페이지 분할)순차 append (flush)
쓰기 증폭적음많음 (컴팩션 재기록)
단건 읽기3~4홉, 예측가능·낮은 지연여러 SSTable → read amp, p99 취약
강점 워크로드읽기·point read, 쓰기 희박쓰기 폭주(로그/잦은 갱신)
대표MySQL InnoDB, PostgreSQLRocksDB, Cassandra, HBase, ScyllaDB

LSM 컴팩션 전략 — 셋 다 못 가진다

write / read / space amplification은 동시에 다 줄일 수 없다. 워크로드가 무엇을 희생해도 되는지가 선택 기준.

08실무 드릴 — 3가지 함정

🔥 함정 1 · "스케일 될 거니까 Cassandra 깔자"

읽기:쓰기 = 50:1, 단건 조회, p99 50ms, 수천만 건. → 쓰기가 희박해 LSM의 유일한 강점을 안 쓰면서 read amp·p99 리스크만 떠안음. 게다가 RDB 몇 대면 될 걸 분산 운영복잡도로 감쌈.

✅ 판단: B-Tree RDB. 진짜 쓰기가 병목일 때 그 부분만 분리.

🔥 함정 2 · "탈퇴 회원 로그 싹 DELETE"

하루 수십만 row DELETE → 툼스톤 폭증. 읽히는 파티션에 툼스톤이 고이면 range read가 그걸 다 스캔 → tombstone_failure_threshold(10만) 초과 시 쿼리 실패. gc_grace 10일간 잔존.

✅ 판단: 삭제 단위를 파티션 키(member_id)로 모델링 → 파티션 툼스톤 몇 개 / 나이 만료는 TTL+TWCS로 SSTable 통째 drop. "누구를 지우나"와 "언제 만료하나"는 독립된 두 축.

🔥 함정 3 · "읽기 느리고 디스크 부풀어, 노드 늘리자"

잦은 갱신 + 잦은 읽기인데 컴팩션이 기본값(STCS). 옛 버전이 여러 tier에 흩어져 read amp↑·space amp↑. 노드 추가는 노드마다 같은 병증을 복제하는 헛발질.

✅ 판단: 엔진 교체 말고 LCS로 컴팩션 전략 전환(read/space amp 급감, 대가는 write amp). 단 순수 쓰기 파이어호스엔 오히려 STCS/TWCS.

💡 세 함정을 관통하는 통찰: 표면의 "읽기 많음"이 아니라 쓰기 축이 진짜 결정 변수. 그리고 도구를 갈아엎기 전에 튜닝(모델링·컴팩션 전략)부터 — LSM은 하나가 아니라 전략으로 성격이 바뀐다.

더 파고들 키워드

WAL / 체크포인트

memtable의 내구성 보강. 크래시 복구 = 체크포인트 + WAL replay.

RUM conjecture

Read·Update·Memory 오버헤드는 동시에 최적화 불가 — write/read/space amp의 이론적 배경.

B-ε tree / Fractal tree

B-Tree와 LSM 사이 절충 구조 (TokuDB 등).

스킵리스트 vs RB-tree

memtable에 쓰는 인메모리 정렬 자료구조 선택.

gc_grace_seconds

툼스톤 부활 방지 유예기간. 분산 삭제의 정합성.

CDC

인덱스·검색엔진을 원본 DB와 동기화(change data capture).

📖 용어집