GN⁺: B-Trees: 예상보다 더 많은 정보를 담고 있는 데이터 구조
(benjamincongdon.me)- 최근에 Alex Petrov의 Database Internals를 읽으면서 DBMS 스토리지 엔진 구현 방식, 특히 B-Tree 자료구조 최적화와 관련된 내용을 접함
- 대학에서 B-Tree를 배울 때는 단순히 “더 나은 BST”라는 식으로 이해해서, 실제로 왜 쓰이는지 와닿지 않았음
- 디스크 I/O를 고려해 대규모 데이터를 저장하고 빠르게 검색하기 위해 B-Tree 구조가 적합함
- 이 글은 B-Tree가 왜 필요한지, 어떤 식으로 동작하는지, 그리고 실제 구현에서 어떤 최적화가 가능한지를 소개함
- 키를 한 노드에 많이 모아 디스크 접근 횟수를 줄이는 방식 등이 주요 특징임
디스크로 인해 생기는 제약 사항
- 데이터 전체가 메모리에 들어가지 않는 상황을 가정함
- 디스크는 한 번에 페이지 단위로 읽고 쓰는 특성이 있음
- 디스크 접근은 메모리 접근에 비해 매우 느린 특성이 있음
- 따라서 자료구조는 페이지 중심으로 데이터를 배치하고, 최소한의 디스크 접근으로 최대한 많은 키 비교를 수행할 필요가 있음
- BST를 그대로 디스크에 저장하면 노드들이 흩어져 있어, 검색 횟수만큼 디스크 접근 횟수도 증가하는 문제가 생김
- B-Tree는 노드에 여러 키를 모아 한 번 디스크를 읽어도 여러 키를 비교할 수 있도록 함
슬롯 페이지
- 디스크에 데이터를 배치할 때는 “페이지” 단위로 관리함
- 각 페이지에는 헤더, 가변 길이 데이터를 담는 셀, 셀 위치 정보를 저장하는 오프셋 포인터 배열이 있음
- 슬롯 페이지 구조는 키 크기가 가변적이어도 재배열 부담 없이 정렬 순서를 유지할 수 있는 장점이 있음
- 키 삭제나 삽입 시, 실제 데이터 이동보다 포인터만 재정렬하는 편이 훨씬 효율적임
- 예를 들어 SQLite는 이런 페이지 구조 내에 free list를 두고, 삭제된 셀 공간을 재활용하는 방식을 사용함
B-Tree 조회
- B-트리 조회 알고리듬은 간단함:
- 루트 노드에서 시작
- 현재 노드의 구분 키(Separator Key)를 보고 검색 키가 있을 것으로 예상되는 자식 노드를 찾음
- 트리를 재귀적으로 탐색
- 검색 키가 포함된 리프 노드를 찾으면 완료, 없으면 존재하지 않는 것으로 판단함
- 내부 노드에는 실제 데이터 대신 구분 키만 있어도 되고, 리프 노드에만 실제 데이터가 저장되는 경우가 일반적임
- 이진 검색으로 노드 내 키를 효율적으로 찾기 위해, 각 노드 내 키들은 정렬된 상태를 유지함
구분 키(Separator Key) 축약
- 내부 노드 구분 키는 실제 키 전체가 아니라 범위를 구분할 수 있을 정도면 충분함
- 예를 들어 0xAAAAAA와 0xBBBBBB 사이를 구분하기 위해 반드시 0xBBBBBB 전부를 저장할 필요는 없고, 더 짧은 접두사 형태로 구분 가능함
- 키 길이가 큰 데이터베이스에서는 이러한 축약이 큰 저장 공간 절감을 가져옴
- 구분 키 축약 외에도 접두사(prefix)와 접미사(suffix)를 효율적으로 줄이는 전략이 있음
- 리프 노드가 훨씬 많으므로, 접두사 압축이 성능에 더 크게 기여한다는 견해도 있음
오버플로 페이지
- 어떤 노드가 너무 많은 키를 가져서 한 페이지에 다 들어가지 않을 때, 오버플로 페이지를 활용함
- 오버플로 페이지에 키 전체를 그대로 옮기는 대신, 노드에는 짧은 접두사 형태만 남기고 나머지를 분리해 저장함
- 이렇게 하면 키 존재 여부나 범위 검색 시, 먼저 노드에 있는 접두사만 확인하고 꼭 필요한 경우에만 오버플로 페이지를 읽게 됨
- 페이지를 추가로 할당하더라도 전체 검색 비용을 줄이는 방식임
형제 포인터
- 노드끼리 왼쪽·오른쪽 형제 노드의 포인터를 저장해 두는 구현 방식이 있음
- 이렇게 하면 범위 조회 시, 한 리프 노드에서 바로 형제 노드로 건너가며 연속된 키를 빠르게 탐색할 수 있음
- 만약 형제 포인터가 없으면, 다시 부모 노드로 올라갔다 내려가는 과정을 반복해야 해서 I/O 비용이 증가함
- 형제 노드 간 키 범위는 서로 겹치지 않으므로, 오른쪽 형제 포인터로 이동하면 ‘다음 키 범위’라는 보장이 있음
B-Tree 변형
- B⁺-Tree 외에도 다양한 변형 자료구조가 존재함
- WiredTiger 같은 “Lazy B-Tree”나 Lazy-Adaptive Tree는 쓰기 연산을 버퍼링하여 성능을 높이는 방식을 사용함
- FD-Tree는 SSD 특성에 맞게 설계된 구조로, 블록 단위 쓰기와 같은 물리적 제약을 고려함
- Bw-Tree(Buzzword Tree)는 높은 동시성과 메모리 상의 트리 접근에 최적화된 자료구조임
결론
- 추상적인 “B⁺-Tree” 개념과 실제 구현인 “SQLite의 DB 포맷” 사이에는 많은 최적화와 세부 구현상의 차이가 존재함
- 최적화는 Big-O 복잡도를 바꾸지는 않지만, 실제 환경에서 데이터베이스 성능과 사용성에 큰 영향을 미침
- 여기서 소개된 내용 외에도 특정 스토리지 시스템마다 미세한 튜닝이 많이 필요함
- “조금만 부가 정보가 필요하다”는 표현 뒤에는 구현 복잡도와 디버깅 어려움이 숨어 있음
- B-Tree 구조를 좀 더 실제처럼 그린 예시로, Designing Data Intensive Applications의 B-Tree 다이어그램이 인상적임
Hacker News 의견
-
페이지의 구조가 헤더, 셀, 오프셋 포인터로 구성되어 있으며, 가변 크기의 데이터를 저장할 수 있는 장점이 있음
- 포인터 배열의 위치만 재정렬하면 되므로 비용이 적게 듦
- 포인터가 키 정렬 순서로 배열되어 있으면 실제 페이지 내 키의 위치는 중요하지 않음
-
B-트리에 대한 애니메이션을 포함한 훌륭한 기사임
- 링크: PlanetScale 블로그
-
몇 년 전 Ibrahim Jaluta의 연구를 기반으로 동시 복구 가능한 B-link Tree를 구현했음
- 세부 사항: SimpleDBM 개발자 가이드
- 구현: GitHub 저장소
-
SQLite 디스크 페이지 탐색기를 만들어 B-트리를 더 잘 이해하게 되었음
- 링크: GitHub 저장소
-
B-link 트리, 동시성, 잠금에 대한 내용이 빠져 있지만, 이는 필요 이상의 정보일 수 있음
-
과거 댓글: Hacker News
-
훌륭한 기사로, 세부 사항의 중요성을 잘 설명함
- LSM-Tree와 B-Tree 및 LSM-Tree 간의 비교에 대한 추가 기사를 보고 싶음
-
Golang을 사용한 B-트리 구현에 대한 좋은 자료임
-
이 기사의 열렬한 팬이며, 저자와 비슷한 '모호한 이해'를 가지고 있었음
- 정신적 모델을 확고히 하고 싶은 사람에게 훌륭한 자료임