2.3.2.5 — минимизировать не сравнения, а обращения к диску2.3.2.5 — minimize not comparisons, but disk accesses

B-Дерево и B+Tree & B+-Tree

AVL, красно-чёрное и splay-дерево (2.3.2.2-2.3.2.4) — бинарные: у узла максимум 2 ребёнка, а значит высота растёт как log₂ n. Это отличная модель, если чтение узла — это чтение указателя в оперативной памяти, дешёвая операция. Но если каждый узел — это страница на диске или SSD, а её чтение стоит на порядки дороже, чем сравнение в памяти, минимизировать нужно не число сравнений, а число обращений к странице. B-дерево (Байер и МакКрейт, 1972) решает именно эту задачу: узел хранит не один ключ, а сразу много (десятки-сотни), и имеет пропорционально много детей — высота становится log_fanout n, а не log₂ n, что при fanout=100 даёт высоту 3-4 даже для миллиардов записей.AVL, red-black, and splay trees (2.3.2.2-2.3.2.4) are binary: a node has at most 2 children, so height grows as log₂ n. That's a great model if reading a node means reading a pointer in RAM, a cheap operation. But if every node is a page on disk or SSD, and reading it costs orders of magnitude more than an in-memory comparison, the thing to minimize isn't the number of comparisons but the number of page accesses. A B-tree (Bayer and McCreight, 1972) solves exactly this problem: a node holds not one key but many at once (tens to hundreds), and has a proportionally large number of children — height becomes log_fanout n rather than log₂ n, which at fanout=100 gives a height of 3-4 even for billions of records.

worst O(log n) высотаheight = log_fanout n минимум обращений к дискуminimal disk accesses

Идея и инвариантIdea & invariant

B-дерево порядка (минимальной степени) t хранит в каждом узле от t-1 до 2t-1 ключей (кроме корня, у которого минимум 1) и, если узел не лист, от t до 2t детей — на 1 больше числа ключей, по одному ребёнку между и по краям от каждой пары соседних ключей, как в обычном BST, только "растянутом" по горизонтали. Ключевой структурный инвариант — все листья находятся на одной и той же глубине: дерево растёт не удлинением одной ветки, а расщеплением переполненного узла и подъёмом среднего ключа в родителя, что увеличивает высоту сразу и равномерно по всему дереву, а не локально.A B-tree of order (minimum degree) t stores t-1 to 2t-1 keys per node (except the root, which needs at least 1), and, if the node isn't a leaf, t to 2t children — one more than the key count, one child between and on either side of each pair of adjacent keys, just like an ordinary BST, only "stretched" horizontally. The key structural invariant is every leaf sits at the same depth: the tree grows not by lengthening one branch but by splitting an overflowing node and lifting its middle key into the parent, which increases height instantly and uniformly across the whole tree, not locally.

Вставка ключа спускается от корня, на каждом шаге выбирая ребёнка между двумя ключами, между которыми должен оказаться новый ключ, — но, в отличие от бинарного дерева, если узел на пути уже заполнен (2t-1 ключей), он превентивно расщепляется на два узла по t-1 ключей, а средний (t-й) ключ поднимается в родителя, прежде чем спуск продолжится. Такое "расщепление по пути вниз" гарантирует, что родитель расщепляемого узла никогда сам не окажется переполненным в момент, когда в него нужно что-то вставить, — вся операция выполняется за один проход сверху вниз, без отдельного прохода обратно вверх, как в AVL/RB-деревьях.Inserting a key descends from the root, at each step choosing the child between the two keys the new key should fall between — but, unlike a binary tree, if a node along the path is already full (2t-1 keys), it's preemptively split into two nodes of t-1 keys each, and the middle (t-th) key is lifted into the parent, before the descent continues. This "split on the way down" guarantees that the parent of a node being split is never itself full at the moment something needs to be inserted into it — the whole operation runs in a single top-down pass, with no separate bottom-up pass like in AVL/red-black trees.

t=2 (макс. 3 ключа): лист [10,20,30] переполнен вставкой 40 → расщепление 10 | 20 | 30 | 40 4 ключа > максимума (3) — переполнение 20 10 30 | 40 средний ключ (20) поднялся в родителя — высота растёт равномерно

расщепление узла — O(t) работы; поднятие ключа в родителя может каскадом вызвать ещё одно расщепление выше, но не глубже одного уровня за разsplitting a node is O(t) work; lifting a key into the parent can cascade into another split above, but never more than one level deeper per step

struct BNode {
    std::vector<int> keys;              // t-1 .. 2t-1 ключей// t-1 .. 2t-1 keys
    std::vector<BNode*> children;    // пусто, если лист// empty if a leaf
    bool leaf = true;
};

// разбить переполненного ребёнка parent->children[i] на два, поднять средний ключ// split the overflowing child parent->children[i] in two, lift the middle key
void split_child(BNode* parent, int i, int t) {
    BNode* child = parent->children[i];
    BNode* sibling = new BNode{}; sibling->leaf = child->leaf;
    int mid = child->keys[t - 1];
    sibling->keys.assign(child->keys.begin() + t, child->keys.end());
    child->keys.resize(t - 1);
    if (!child->leaf) {
        sibling->children.assign(child->children.begin() + t, child->children.end());
        child->children.resize(t);
    }
    parent->children.insert(parent->children.begin() + i + 1, sibling);
    parent->keys.insert(parent->keys.begin() + i, mid);
}

void insert_nonfull(BNode* node, int key, int t) {
    if (node->leaf) {
        auto pos = std::upper_bound(node->keys.begin(), node->keys.end(), key);
        node->keys.insert(pos, key);
    } else {
        int i = std::upper_bound(node->keys.begin(), node->keys.end(), key) - node->keys.begin();
        if ((int)node->children[i]->keys.size() == 2 * t - 1) {   // расщепить превентивно, до спуска// split preemptively, before descending
            split_child(node, i, t);
            if (key > node->keys[i]) i++;
        }
        insert_nonfull(node->children[i], key, t);
    }
}

Демонстрация вживуюLive demonstration

Дерево с t=2 (не больше 3 ключей в узле). insert проигрывается автоматически: спуск, и если узел на пути заполнен — расщепление прямо перед спуском в него, подсвеченное отдельным шагом.A tree with t=2 (at most 3 keys per node). insert auto-plays: descend, and if a node on the path is full — a split right before descending into it, highlighted as its own step.

вставляйте ключи — расщепление проигрывается автоматически при переполненииinsert keys — a split auto-plays on overflow

B+Tree: специализация, на которой держится почти каждая база данныхB+Tree: the specialization nearly every database relies on

Обычное B-дерево хранит значения (не только ключи) во внутренних узлах — если искомый ключ найден на полпути, поиск завершается раньше. B+Tree меняет это намеренно: значения (или указатели на строки таблицы) хранятся только в листьях, а внутренние узлы содержат исключительно ключи-разделители для навигации, без данных. Это увеличивает fanout внутренних узлов (они компактнее, значит помещают больше ключей на ту же страницу — а значит и высота дерева меньше) и, что более важно на практике, позволяет связать все листья в односвязный список слева направо: последовательный обход диапазона (range scan) — "найти всё между X и Y" — становится линейным проходом по листьям, без единого лишнего обращения к внутренним узлам после первого спуска.A plain B-tree stores values (not just keys) in internal nodes too — if the target key is found halfway down, the search ends early. A B+Tree deliberately changes this: values (or pointers to table rows) live only in the leaves, while internal nodes hold exclusively separator keys for navigation, no data. This increases the fanout of internal nodes (they're more compact, so more keys fit on the same page — hence a shorter tree), and, more importantly in practice, lets all the leaves be linked into a singly linked list left to right: a sequential range scan — "find everything between X and Y" — becomes a linear walk over the leaves, with not a single extra internal-node access after the initial descent.

Это ровно тот компромисс, который нужен базе данных: точечный поиск по ключу всё ещё O(log n), но диапазонный запрос (крайне частый в SQL — BETWEEN, ORDER BY с LIMIT, JOIN по диапазону) на B+Tree — это одно приведение к листу плюс последовательное чтение, а не O(log n) отдельных спусков от корня. Именно поэтому InnoDB (MySQL), PostgreSQL, Oracle и большинство файловых систем с B-tree-подобными индексами (NTFS, ext4's htree, XFS) используют вариант B+, а не классический B-Tree Байера-МакКрейта.This is exactly the trade-off a database needs: a point lookup by key is still O(log n), but a range query (extremely common in SQL — BETWEEN, ORDER BY with LIMIT, a range JOIN) on a B+Tree is one descent to a leaf plus a sequential read, not O(log n) separate descents from the root. This is exactly why InnoDB (MySQL), PostgreSQL, Oracle, and most filesystems with B-tree-like indexes (NTFS, ext4's htree, XFS) use the B+ variant rather than the classic Bayer-McCreight B-Tree.

Где встречаетсяWhere it shows up

01

InnoDB (MySQL) и стандартный индекс PostgreSQL(MySQL) and PostgreSQL's default index

Кластерный индекс InnoDB (по первичному ключу) и обычный CREATE INDEX в PostgreSQL используют B+Tree — данные (или указатели на них) в листьях, связанных между собой, дают эффективный как точечный поиск, так и сканирование диапазона одновременно.InnoDB's clustered index (on the primary key) and a plain CREATE INDEX in PostgreSQL both use a B+Tree — data (or pointers to it) in linked leaves gives efficient point lookup and range scanning at the same time.

02

NTFS, ext4 (htree), XFS — файловые системы на B-деревьях— filesystems built on B-trees

Индексация записей директорий и метаданных файлов в этих файловых системах устроена как B-дерево (или его вариант) именно из-за той же исходной мотивации, для которой оно и создавалось в 1972 году, — минимизировать число обращений к медленному носителю при поиске файла по имени в директории с миллионами записей.Indexing directory entries and file metadata in these filesystems is built as a B-tree (or a variant of one) for exactly the original motivation it was created for in 1972 — minimizing the number of accesses to slow storage when looking up a file by name in a directory with millions of entries.