B-дерево: вставка ключа и разделение узла

B-дерево - сильно ветвящееся сбалансированное дерево поиска, спроектированное Байером и Маккрейтом в 1972 году для индексов на внешних носителях. В отличие от AVL и красно-чёрных деревьев, где в узле один ключ, узел B-дерева содержит десятки или сотни ключей. Высота получается крошечной - единицы уровней даже на миллиардах записей, - а операции укладываются в обращений к диску. Вставка опирается на одну идею: если узел переполнился, он расщепляется надвое, а средний ключ поднимается к родителю.
Определение B-дерева порядка t
B-дерево с минимальной степенью - это сбалансированное по высоте дерево поиска, в котором каждый узел, кроме корня, содержит от до ключей. Корень допускает от до ключей. Если узел содержит ключей, у него ровно ссылок на детей; ключи внутри узла упорядочены по возрастанию.
Для каждого внутреннего узла с ключами и детьми все ключи в поддереве лежат в интервале . Все листья находятся на одной глубине - это и есть свойство сбалансированности высоты, оно поддерживается алгоритмом вставки и удаления автоматически.
Параметр называют минимальной степенью. Иногда вместо задают «порядок» - максимальное число детей; обе формы эквивалентны.
Свойство сбалансированности и оценка высоты
Из инварианта следует жёсткая оценка высоты:
Для и это даёт - пять уровней на миллиард записей. Любая операция спускается по одному пути от корня к листу, число обращений к диску - порядка высоты.
Зачем такое сильное ветвление: блочное чтение с диска
B-дерево создавалось под физику внешней памяти, и форма узла привязана к размеру дискового блока (или страницы СУБД - 4–16 КБ для PostgreSQL, 16 КБ по умолчанию в InnoDB MySQL). Один узел упаковывают так, чтобы он целиком влез в один блок: один seek - десятки и сотни сравнений в RAM. AVL и RB на диске страдают от того, что каждый узел - отдельный блок ради двух-трёх ключей.
Отсюда выбор : его задают так, чтобы ключей плюс служебные ссылки заполняли страницу. На практике - десятки и сотни, а не двойки-тройки из учебных примеров.
Соберите дерево из последовательности вставок или прогоните одну операцию пошагово - tool вернёт промежуточные состояния, точки переполнения и список произошедших разделений.
Алгоритм вставки
Вставка в B-дерево похожа на вставку в BST, но с двумя отличиями: спуск идёт по «толстым» узлам, а переполнение чинится разделением.
- Поиск листа. Идём от корня. В каждом узле находим позицию нового ключа (бинарным поиском) и спускаемся в соответствующего ребёнка. Останавливаемся, дойдя до листа.
- Локальная вставка. Ключ помещается в лист на нужную позицию, существующие ключи сдвигаются вправо.
- Split при переполнении. Если в листе стало ключей, средний ключ поднимается в родителя; узел разбивается на два - левый получает первые ключей, правый - последние .
- Каскад. Если родитель тоже переполнился, операция повторяется выше. В худшем случае разделения доходят до корня - тогда из его среднего ключа создаётся новый корень. Это единственный способ увеличить высоту B-дерева, поэтому все листья остаются на одной глубине.
На практике используют проактивный split: при спуске, прежде чем войти в ребёнка с ключами, его сразу делят. Вставка работает за один спуск, без обратного хода, и хорошо ложится на буфер-менеджер СУБД.
Пример: вставка 10, 20, 5, 6, 12, 30, 7, 17 при
При узел держит от 1 до 3 ключей. Стартуем с пустого дерева.
- Вставки
10, 20, 5- все в корень:[5, 10, 20]. - Вставка
6: получаем 4 ключа - переполнение. Средний ключ10поднимается, корень делится. Новое дерево: корень[10], дети[5, 6]и[20]. - Вставки
12, 30: правый ребёнок становится[12, 20, 30]- заполнен, но ещё не переполнен. - Вставка
7: спуск в левый ребёнок, получаем[5, 6, 7]. - Вставка
17: правый ребёнок переполняется до[12, 17, 20, 30]. Средний ключ20поднимается в корень; правый узел делится на[12, 17]и[30]. Корень становится[10, 20], у него три ребёнка.
Высота выросла на один уровень один раз - при первом split. Дальше дерево расширяется в ширину, что и обеспечивает .
Удаление: краткий обзор
Удаление опирается на зеркальный приём - слияние недополненного узла с соседом. Если ключ лежит в листе с ключами - удаляем напрямую. Иначе перед удалением:
- Rotation (borrow). У соседнего листа есть запас ( ключей) - забираем у него один ключ через родителя.
- Merge. Соседи бедные - два листа сливаются в один с разделяющим их ключом из родителя; в родителе становится на ключ меньше, и проблема может всплыть наверх.
Для внутреннего узла ключ заменяют на предшественника или преемника из листа - и удаляют его из листа. Сложность - .
Варианты: B+-дерево и B*
B-дерево из 1972 года почти не используется в чистом виде - его вытеснили варианты.
- B+-дерево. Полезные данные хранят только в листьях, а внутренние узлы содержат лишь ключи-разделители. Листья соединены в двусвязный список - диапазонный скан
WHERE x BETWEEN a AND bидёт линейным проходом по листьям без возврата к корню. Это формат большинства СУБД-индексов: PostgreSQLbtree, MySQL InnoDB, SQL Server, Oracle. - B*-дерево.** Усиленный B+: узлы перед split пытаются перераспределить ключи с соседом, каждый узел держит не менее от максимума. Дерево ещё плотнее. Используется в HFS+ и Reiser4.
Применение и сравнение с AVL, RB и B+
Где живут B-деревья на практике:
- СУБД-индексы. PostgreSQL (
btreeпо умолчанию), MySQL InnoDB (clustered primary key), Oracle, SQL Server, SQLite - все на B+. - Файловые системы. NTFS хранит каталоги в B+, HFS+ - в B*, APFS - в B-tree с copy-on-write, Btrfs (само имя) и ext4 (htree-каталоги).
- Key-value хранилища. WiredTiger (движок MongoDB), LMDB - на B+ с copy-on-write страницами.
| Свойство | AVL / RB | B-дерево | B+-дерево |
|---|---|---|---|
| Ключей в узле | 1 | то же | |
| Высота для | ~30 | 4–5 (при ) | то же |
| Данные во внутренних узлах | да | да | нет, только в листьях |
| Range-сканы | ин-ордер с возвратами | то же | по связному списку листьев, линейно |
| Где живёт | в RAM | на диске/в RAM | на диске (СУБД) |
AVL и RB выигрывают в RAM на малых за счёт простоты и кэш-локальности, B-дерево и его варианты - на диске и больших объёмах за счёт ширины узла.
Типовые учебные задачи: построить B-дерево с из последовательности 1..10 и нарисовать промежуточные состояния; показать, какие узлы разделятся при вставке очередного ключа и поднимется ли split до корня; найти минимальное и максимальное число ключей в B-дереве высоты при заданном ; различить, когда удаление вызовет merge, а когда rotation у соседа.
Частые ошибки
- Путают порядок и минимальную степень . В одних учебниках - максимальное число детей (), в других - максимальное число ключей (). Перед задачей всегда уточняйте определение.
- Поднимают не средний ключ при split. Должна подниматься именно медиана; иначе ломается инвариант « ключей в каждом узле, кроме корня».
- Считают, что split всегда увеличивает высоту. Высота растёт только когда делится сам корень. На любом другом уровне split увеличивает ширину родителя, а не глубину.
- Сравнивают B-дерево с AVL по числу узлов. Корректная метрика - число обращений к диску, и тут B-дерево заведомо в выигрыше при большом .
- Используют B-дерево в RAM. Для in-memory словарей оно почти всегда проигрывает RB или хеш-таблицам. B-дерево раскрывается только на диске или на NVMe с большими страницами.
FAQ
Чем B-дерево отличается от двоичного дерева поиска? В BST один ключ на узел и две ссылки; в B-дереве - до ключей и до ссылок. Высота B-дерева пропорциональна , а не , и обращений к диску на порядок меньше - это и было мотивацией Байера и Маккрейта в 1972 году.
Почему вставка работает за , если split может всплыть до корня? Число split за одну вставку не превосходит высоты дерева, то есть штук. Каждое разделение делает работы в RAM, но обращений к диску ровно - а на диске именно это считается за «стоимость» операции.
В чём разница между B-деревом и B+-деревом? В B-дереве данные могут лежать как во внутренних узлах, так и в листьях. В B+ данные - только в листьях, а листья связаны в список. Это даёт два выигрыша: внутренние узлы плотнее (туда влезает больше ключей-разделителей), и range-сканы идут линейным проходом по листьям. Поэтому реальные СУБД используют B+.
Коротко
B-дерево - это сбалансированное дерево поиска, где каждый узел хранит от до ключей и упакован в один дисковый блок. Вставка идёт спуском к листу, локальной вставкой и, при переполнении, разделением узла с подъёмом среднего ключа в родителя; в худшем случае split каскадно доходит до корня - это единственный способ увеличить высоту. За счёт большого высота получается крошечной, операции стоят обращений к диску, и поэтому B-дерево и его наследники (B+ в PostgreSQL, MySQL InnoDB, NTFS; B* в HFS+) остаются стандартом индексов для внешней памяти уже полвека.
Читайте также

AVL-дерево: как работает балансировка и ротации
Разбираем, как AVL-дерево восстанавливает баланс после вставки и удаления: инвариант высоты, balance factor и четыре ротации LL, RR, LR, RL за O(log n).

Куча Фибоначчи: ленивая структура и амортизация
Куча Фибоначчи: амортизированный на insert и decrease-key, ленивая консолидация при extract-min, потенциал, каскадный cut и применение в алгоритме Дейкстры.

Биномиальная куча: операции и слияние за O(log n)
Биномиальная куча: как устроен лес деревьев, зачем нужно слияние двух куч за O(log n) и чем она лучше бинарной. Разбираем операции insert, extract-min и merge на примерах.