linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть VII — Индексы

$ глава 32 · 55 минут

B-tree изнутри

B-tree - индекс по умолчанию и самый частый. За словами «сбалансированное дерево» стоит конкретная раскладка по 8-КБ страницам, и её можно увидеть тем же pageinspect, что и heap. Мы спустимся от корня к листу за конкретным значением, посмотрим, что лежит в узлах, и поймём, почему дерево из миллиарда ключей имеет всего 3-4 уровня.

Заодно разберём механизмы, которые делают B-tree компактным и быстрым: split при переполнении с правыми ссылками, дедупликацию повторяющихся ключей, обрезку разделителей и payload-колонки INCLUDE. После этой главы «спуск по индексу» перестанет быть абстракцией.

32.1 Сбалансированное дерево по страницам

B-tree - дерево, где все листья на одной глубине, а каждый узел - это страница 8 КБ. Есть три роли страниц:

  • meta page (страница 0) - служебная, хранит указатель на корень и уровень дерева;
  • внутренние страницы - содержат разделители и ссылки вниз (downlinks) на дочерние страницы;
  • листовые страницы - содержат записи индекса: значение ключа и ссылку ctid на строку в таблице.

Поиск всегда начинается с meta page, идёт к корню, спускается по внутренним страницам и заканчивается на листе, где лежит ctid. Сбалансированность означает, что путь от корня до любого листа одинаковой длины - поэтому время поиска предсказуемо.

32.2 Спуск своими глазами

pageinspect показывает страницы индекса так же, как heap. Сначала meta page:

sql
CREATE EXTENSION IF NOT EXISTS pageinspect;
SELECT root, level FROM bt_metap('tickets_pkey');

root - номер страницы корня, level - высота дерева (0 значит, корень и есть лист). Затем смотрим содержимое страницы:

sql
SELECT itemoffset, ctid, data
FROM bt_page_items('tickets_pkey', 1);

На внутренней странице ctid в записи указывает на дочернюю страницу (downlink); на листовой - на строку в таблице. Спускаясь от корня по downlink к нужному диапазону, ты вручную пройдёшь тот же путь, что и планировщик за тебя.

32.3 Почему дерево такое мелкое

Ключевое свойство B-tree - огромный коэффициент ветвления (fanout). На одной 8-КБ внутренней странице помещаются сотни разделителей, значит у каждого узла сотни детей. Глубина дерева - это логарифм по основанию fanout:

глубина ≈ log_fanout(число строк)

При fanout около 200-300 трёх уровней хватает на десятки миллионов строк, четырёх - на миллиарды. Поэтому поиск по индексу - это 3-4 чтения страниц, а не «обход дерева». Отсюда и стоимость в модели планировщика: индексный доступ дёшев, пока строк отбирается мало (см. btree-structure).

32.4 Split и правые ссылки

Когда в листовую страницу вставляют ключ, а места нет, страница делится (split): примерно половина записей уезжает в новую страницу, а наверх, в родителя, поднимается разделитель. Если родитель тоже полон - делится и он, вплоть до корня; split корня повышает дерево на уровень.

Чтобы поиск и вставки работали параллельно без блокировки всего дерева, PostgreSQL использует приём Лемана-Яо: на каждой странице есть high key (верхняя граница её диапазона) и right link - ссылка на правого соседа.

[ ... | high key=50 ] --right--> [ 51 | 52 | ... | high key=99 ]

Если во время спуска страница как раз делилась и нужный ключ уехал вправо, поиск по right link догоняет его, не начиная заново. Так достигается параллельный доступ без тяжёлых блокировок дерева.

32.5 Дедупликация повторяющихся ключей

Если в колонке много повторов (например, status с десятком значений на миллионы строк), наивный индекс хранил бы значение заново в каждой записи. Дедупликация это устраняет: одинаковые ключи хранятся один раз, а к ним прикреплён posting list - список всех ctid с этим значением.

без дедупликации:  (done, ctid1)(done, ctid2)(done, ctid3)...
с дедупликацией:   (done → [ctid1, ctid2, ctid3, ...])

Это резко уменьшает размер индекса на колонках с низким числом различных значений и ускоряет их чтение. Дедупликация работает автоматически, её видно по размеру индекса и в bt_page_items как записи с posting list.

32.6 Suffix truncation

Внутренним страницам не нужен полный ключ - им нужен лишь разделитель, достаточный, чтобы решить «влево или вправо». Поэтому при подъёме разделителя в родителя PostgreSQL обрезает его до минимального различающего префикса (suffix truncation).

Например, чтобы отделить aardvark от azimuth, в разделителе хватит a плюс второй буквы - остаток не нужен. Обрезка делает внутренние записи короче, на странице помещается больше разделителей, fanout растёт, дерево становится ещё мельче. Это одна из причин, по которой B-tree в PostgreSQL такой компактный на длинных ключах.

32.7 INCLUDE: покрывающий индекс

В лист можно положить дополнительные колонки, не делая их частью ключа, - через INCLUDE. Они не участвуют в сортировке и спуске, но доступны для чтения прямо из индекса.

sql
CREATE INDEX ON tickets (flight_id) INCLUDE (passenger);

Теперь SELECT flight_id, passenger WHERE flight_id = 7 может пойти по Index-Only Scan: обе колонки есть в листе, в таблицу идти не надо (при выставленной visibility map, см. главу 27). INCLUDE нужен именно для покрытия запроса, а не для поиска: класть в ключ колонку, по которой не ищешь, было бы лишней нагрузкой на спуск и split. Подробнее про эти механизмы - в btree-internals.

32.7.1 Копнуть глубже: почему глубину почти не нужно считать

На практике глубину B-tree редко считают точно - она почти всегда 3-4. Из-за большого fanout и suffix truncation добавление нулей к числу строк добавляет к глубине доли уровня. Дерево на тысяче строк и на миллиарде различаются по высоте на один-два уровня, не на порядки.

Практический вывод: переживать о «глубоком дереве» не нужно - B-tree остаётся мелким при любом разумном объёме. Реальные проблемы индекса не в глубине, а в другом: раздувание от обновлений, неподходящий operator class, индекс, который не покрывает запрос. Этим и займёмся в главе про проектирование.

Уроки в sandbox

lab-32.1. Спуститься по B-дереву через pageinspect

Пройдём от корня к листу за конкретным ключом и подтвердим путь через ctid. Перед чтением листа предскажи, на какой странице окажется искомое значение.

  1. Подключи линзу: CREATE EXTENSION IF NOT EXISTS pageinspect;.

  2. Узнай корень и высоту: SELECT root, level FROM bt_metap('tickets_pkey');. Запиши номер корневой страницы.

  3. Посмотри содержимое корня: SELECT itemoffset, ctid, data FROM bt_page_items('tickets_pkey', <root>);. На внутренней странице ctid - это downlink на дочернюю.

  4. Спустись на уровень ниже по нужному downlink и читай bt_page_items этой страницы, пока не дойдёшь до листа.

  5. На листе найди запись с искомым ticket_no - её ctid указывает на строку в таблице. Сверь: SELECT ctid, ticket_no FROM tickets WHERE ticket_no = '0000000000007';.

  6. Создай покрывающий индекс CREATE INDEX ON tickets (flight_id) INCLUDE (passenger); и проверь Index Only Scan для SELECT flight_id, passenger ... WHERE flight_id = 7.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • B-tree - сбалансированное дерево из 8-КБ страниц: meta page указывает на корень, внутренние страницы хранят разделители и downlink, листья - ключ и ctid.
  • Спуск идёт meta → корень → внутренние → лист; pageinspect (bt_metap, bt_page_items) позволяет пройти его руками.
  • Большой fanout (сотни детей на странице) делает дерево мелким: 3-4 уровня хватает на миллиарды строк, поиск - 3-4 чтения страниц.
  • При переполнении страница делится (split), разделитель поднимается вверх; high key и right link (Леман-Яо) дают параллельный доступ без блокировки дерева.
  • Дедупликация хранит повторяющийся ключ один раз с posting list из ctid - резко уменьшает индекс на колонках с малым числом различных значений.
  • Suffix truncation обрезает разделители во внутренних страницах до различающего префикса, повышая fanout и делая дерево ещё компактнее.
  • INCLUDE кладёт в лист payload-колонки для покрытия запроса (Index-Only Scan), не делая их частью ключа и не нагружая спуск.

Контрольные вопросы

  1. Почему B-tree из миллиарда строк имеет всего 3-4 уровня?

    Показать ответ

    Из-за большого коэффициента ветвления (fanout). На одной 8-КБ внутренней странице помещаются сотни разделителей, значит у каждого узла сотни детей. Глубина дерева - это логарифм числа строк по основанию fanout. При fanout 200-300 три уровня покрывают десятки миллионов строк, четыре - миллиарды. Suffix truncation ещё повышает fanout, обрезая разделители. Поэтому поиск по индексу - это 3-4 чтения страниц независимо от объёма таблицы.

  2. Что такое high key и right link и зачем они нужны?

    Показать ответ

    High key - верхняя граница диапазона ключей страницы; right link - ссылка на правого соседа того же уровня. Это приём Лемана-Яо для параллельного доступа. Если во время спуска страница как раз делилась и искомый ключ уехал в новую правую страницу, поиск сравнивает ключ с high key, понимает, что нужно правее, и идёт по right link - не начиная спуск заново и не блокируя дерево. Так вставки и поиски работают одновременно без тяжёлых блокировок.

  3. Как дедупликация уменьшает размер B-tree?

    Показать ответ

    Когда в колонке много повторяющихся значений, наивный индекс хранил бы ключ заново в каждой записи. Дедупликация хранит каждое уникальное значение один раз и прикрепляет к нему posting list - список всех ctid строк с этим значением. На колонке с малым числом различных значений (статус, флаг) это сокращает индекс в разы и ускоряет его чтение. Работает автоматически; записи с posting list видны в bt_page_items.

  4. Чем INCLUDE-колонка отличается от обычной колонки индекса?

    Показать ответ

    INCLUDE-колонка лежит только в листовых страницах как payload и не участвует ни в сортировке, ни в спуске по дереву. Обычная (ключевая) колонка участвует в упорядочивании и используется при поиске. INCLUDE нужен, чтобы покрыть запрос для Index-Only Scan: колонка доступна для чтения из индекса, но не нагружает спуск и split. Класть в ключ колонку, по которой не ищешь, было бы лишней нагрузкой - INCLUDE решает это аккуратно.

  5. Стоит ли беспокоиться, что индекс «слишком глубокий»?

    Показать ответ

    Почти никогда. Из-за большого fanout и suffix truncation глубина B-tree остаётся 3-4 уровня при любом разумном объёме - дерево на тысяче и на миллиарде строк отличаются по высоте на уровень-другой, не на порядки. Реальные проблемы индекса не в глубине: это раздувание от частых обновлений, неподходящий operator class (оператор не поддержан), или индекс, не покрывающий запрос. На них и стоит тратить внимание, а не на высоту дерева.

← Предыдущая31-operator-classesСледующая →33-hash
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки