1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 2 из 6

B-дерево: почему индексы БД живут на диске так

Коротко

B-дерево - это 2-3 дерево, выросшее вширь: узел хранит не 1-2, а СОТНИ ключей и имеет столько же плюс один детей. Зачем так много? Потому что оно живёт на ДИСКЕ, а диск читается БЛОКАМИ (страницами по несколько килобайт), и одно чтение блока в тысячи-миллионы раз дороже сравнения в памяти. Поэтому цена дерева на диске - это не число сравнений, а число ЧТЕНИЙ БЛОКОВ, то есть высота. Раз узел = блок и вмещает сотни ключей, дерево ветвится в сотни раз на каждом уровне - и высота выходит крошечной: миллиарды ключей укладываются в 3-4 уровня. Значит любой поиск - 3-4 обращения к диску вместо log₂(миллиард) ≈ 30. На B-деревьях стоят индексы всех баз данных и файловые системы.

Если ты понимаешь, почему на диске цена - это число чтений блоков (высота), почему узел B-дерева делают размером с блок (сотни ключей) и почему это даёт высоту 3-4 на миллиардах ключей, листай до «Теперь сам».

Как думаешь, что дорого, когда данных больше, чем памяти

Всё, что мы считали до сих пор, предполагало данные В ПАМЯТИ, где любое сравнение стоит примерно одинаково. Но индекс базы данных - это гигабайты, он лежит на ДИСКЕ. А диск устроен иначе: прочитать один байт нельзя - читается сразу целый БЛОК (страница), и это чтение чудовищно медленное по меркам процессора (миллионы тактов против единиц). Значит на диске «дорого» - это не сравнение и не арифметика, а само ОБРАЩЕНИЕ К ДИСКУ. Красно-чёрное дерево на миллиард ключей имеет высоту ~30 - это ~30 чтений диска на один поиск, и каждое чтение вытягивает целый блок, из которого мы используем один узел. Расточительно. Раз блок всё равно читается целиком - надо НАБИТЬ в него как можно больше ключей.

На что это похоже

Библиотека, где заказ книги с дальнего склада едет час, а полистать её на столе - секунды. Глупо заказывать книги по одной, если можно одним рейсом привезти целую ТЕЛЕЖКУ. Ты просишь сразу коробку каталожных карточек (блок), просматриваешь все за столом (в памяти - быстро) и лишь изредка заказываешь следующую коробку (чтение диска - долго). Чем больше карточек в коробке, тем меньше рейсов на склад. B-дерево так и устроено: узел - это «коробка» размером с то, что диск привозит за один рейс, и в неё кладут максимум ключей, чтобы рейсов (высоты) было как можно меньше.

Механизм: узел = блок, ветвление в сотни раз

B-дерево порядка t (минимальная степень): узел хранит от t−1 до 2t−1 ключей и от t до 2t детей (кроме корня). На практике t подбирают так, чтобы узел ЗАПОЛНЯЛ дисковый блок:

дисковый блок ~ 4-16 КБ
ключ + указатель ~ несколько десятков байт
=> в узел влезает порядка 100-1000 ключей
=> ветвление (число детей) ~ 100-1000 на КАЖДОМ уровне

Всё как в 2-3 дереве, только «широкое»: поиск спускается от корня, на каждом узле выбирая нужный из сотен диапазонов; вставка идёт в лист, переполнение расщепляет узел и поднимает средний ключ; листья всегда на одном уровне. Но теперь оценка высоты другая. При ветвлении b на уровень дерево из n ключей имеет высоту ≈ log_b(n):

b = 256, n = миллиард:  log_256(10^9) ≈ 3.7  →  высота 3-4
для сравнения log_2(10^9) ≈ 30

Число ЧТЕНИЙ ДИСКА на поиск = высота = 3-4 вместо 30. Внутри узла ключи ищут в памяти (двоичным поиском) - это «бесплатно» на фоне чтения блока.

|||…|| корень: сотни ключей (1 блок) · · · сотни узлов · · · · · · листья · · · ветвление ~256 → миллиард ключей в 4 уровня поиск = 3-4 чтения диска (высота), а не 30 как у двоичного дерева
Узел B-дерева = дисковый блок с сотнями ключей. Высокое ветвление даёт крошечную высоту: миллиард ключей - 3-4 уровня, то есть 3-4 чтения диска на поиск.

Правило

B-дерево - многоключевое сбалансированное дерево, где узел размером с ДИСКОВЫЙ БЛОК хранит сотни ключей и ветвится в сотни раз. Оно живёт на диске, где цена - число ЧТЕНИЙ БЛОКОВ, то есть ВЫСОТА. Высокое ветвление b даёт высоту ≈ log_b(n) - крошечную: миллиарды ключей укладываются в 3-4 уровня, значит поиск - 3-4 обращения к диску. Внутри узла ключи ищут в памяти (дёшево). Балансировка - как в 2-3 дереве: вставка в лист, расщепление переполненных, все листья на одном уровне. На B-деревьях стоят индексы баз данных и файловые системы.

Разбор: почему считают чтения диска, а не сравнения

Вся конструкция B-дерева следует из одного факта: на диске операции НЕ равноценны. В памяти сравнение и чтение стоят примерно одинаково, поэтому там оптимизируют число сравнений (log₂ n). На диске одно чтение блока - это миллионы тактов (механическое позиционирование у HDD, задержки контроллера у SSD), а сравнение ключей уже в прочитанном блоке - единицы тактов. Разница в миллионы раз. Значит правильная «стоимость» - это число обращений к диску, а всё, что происходит ВНУТРИ прочитанного блока, практически бесплатно. Отсюда стратегия: минимизировать число уровней (высоту), даже ценой того, что внутри узла придётся просмотреть сотни ключей - эти просмотры идут в памяти и не в счёт. B-дерево доводит эту логику до предела: набивает узел под завязку блоком, чтобы каждый спуск на уровень «окупал» дорогое чтение максимумом информации. Это тот же принцип, что локальность памяти и кеш в разделе про массивы, только на уровне «память против диска» - иерархия та же, просто разрыв в скорости ещё больше.

Разбор: почему высота выходит 3-4, а не 30

Всё решает основание логарифма. Высота дерева с ветвлением b на n ключей - это log_b(n), а не log₂(n). У двоичного дерева b = 2, поэтому log₂(10^9) ≈ 30 уровней. У B-дерева b доходит до сотен: при b = 256 высота log_256(10^9) = log₂(10^9)/log₂(256) = 30/8 ≈ 3.7 - около четырёх уровней. Ветвление входит в ЗНАМЕНАТЕЛЬ через свой логарифм: высота делится на log₂ b, а не на b. Увеличив ветвление с 2 до 256, мы поделили высоту на 8 - с 30 до 4. А высота на диске = число медленных чтений, поэтому поиск ускорился в те же ~8 раз (и это ещё до учёта того, что верхние 1-2 уровня обычно закешированы в памяти - тогда реальных чтений диска всего 1-2). Вот почему индексы делают именно B-деревьями, а не красно-чёрными: на диске важна не глубина в сравнениях, а глубина в БЛОКАХ, и широкий узел её минимизирует.

Что ломается без этого

Первая ошибка - использовать двоичное дерево (красно-чёрное) для дискового индекса: высота ~30 даст ~30 чтений диска на поиск вместо 3-4 - в разы медленнее, хотя в ПАМЯТИ оно ничем не хуже. На диске нужен широкий узел. Вторая - делать узел B-дерева произвольного размера, не привязанного к блоку: слишком мелкий узел теряет весь смысл (мало ветвления, высокое дерево), слишком крупный - читается за несколько блоков; узел подгоняют ПОД размер блока. Третья - считать стоимость поиска в сравнениях, а не в чтениях блоков: тогда B-дерево кажется «медленнее» (внутри узла сотни сравнений), хотя на диске оно кратно быстрее - сравнения в памяти почти бесплатны. Четвёртая - путать B-дерево и B+-дерево: в B+-дереве все ДАННЫЕ в листьях, а листья связаны в список для быстрых диапазонных сканов - это распространённый в БД вариант, но идея та же.

Зачем это в работе

B-деревья (и их вариант B+-деревья) - фундамент хранения данных: индексы во ВСЕХ реляционных базах (PostgreSQL, MySQL, Oracle), ключи в файловых системах (NTFS, ext4, HFS+), встраиваемые хранилища (SQLite, BerkeleyDB). Когда ты создаёшь индекс в базе и запрос по нему летает - это B-дерево свело поиск среди миллиардов строк к 3-4 чтениям диска. Понимать B-дерево - значит понимать, ПОЧЕМУ индекс ускоряет запрос (высота вместо полного скана), почему он занимает место (хранит копию ключей в дереве), почему диапазонные запросы WHERE x BETWEEN a AND b по индексу дёшевы (спуск + обход листьев по порядку) и почему структура данных для диска устроена иначе, чем для памяти. Это прямое применение всего раздела: то же дерево поиска, та же балансировка, но цена операции переопределена под реальное железо - и оптимум сместился к широким узлам. Умение видеть, что «стоимость» зависит от уровня памяти (кеш, ОЗУ, диск, сеть), и подбирать структуру под неё - одна из самых практичных идей курса.

Теперь сам

Индекс на n = 10^9 ключей. Оцени высоту (число чтений диска на поиск) для двоичного дерева и для B-дерева с ветвлением 200. Во сколько раз B-дерево быстрее по обращениям к диску?

Ответ. Высота двоичного дерева (ветвление 2): log₂(10^9) ≈ 30 уровней - значит ~30 чтений диска на поиск. Высота B-дерева с ветвлением b = 200: log_200(10^9) = log₂(10^9)/log₂(200) ≈ 30 / 7.6 ≈ 4 уровня - ~4 чтения диска. B-дерево быстрее примерно в 30/4 ≈ 7-8 раз по числу обращений к диску - а именно они и есть узкое место (каждое чтение блока в миллионы раз дороже сравнения в памяти). На практике ещё лучше: верхние уровни (корень и следующий) обычно закешированы в ОЗУ, поэтому реальных ОБРАЩЕНИЙ К ДИСКУ на поиск остаётся 1-2. Причина выигрыша - основание логарифма: широкий узел (200 детей вместо 2) делит высоту на log₂200 ≈ 7.6, а на диске высота и есть цена.

Главное

B-дерево - 2-3 дерево, выросшее вширь: узел размером с ДИСКОВЫЙ БЛОК хранит сотни ключей и ветвится в сотни раз. Оно живёт на диске, где цена - число ЧТЕНИЙ БЛОКОВ (одно чтение в миллионы раз дороже сравнения в памяти), то есть ВЫСОТА. Высота ≈ log_b(n): при ветвлении b ≈ 200-256 миллиард ключей укладывается в 3-4 уровня → поиск = 3-4 чтения диска вместо ~30 у двоичного дерева (основание логарифма делит высоту). Внутри узла ключи ищут в памяти - почти бесплатно. Балансировка как в 2-3 дереве (вставка в лист, расщепление, листья на одном уровне). На B/B+-деревьях стоят индексы всех БД и файловые системы. Мораль раздела: стоимость операции зависит от уровня памяти (кеш/ОЗУ/диск), и структуру подбирают под неё - для диска оптимум - широкий узел.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.