Практическое занятие №6: ключевые поля, индексы, связи

1. Проблема производительности: жизнь таблицы в куче (Heap)

Физическое хранение без порядка

В реляционной СУБД кортежи таблицы хранятся в неупорядоченном пространстве дисковых страниц (Heap / Куча):

  • Новые строки записываются в первое попавшееся свободное место страницы.
  • Физический адрес строки задается координатой TID (Block, Offset).
  • Без специальных указателей найти конкретную запись можно лишь сплошным чтением всех страниц.

Цена полного сканирования (Seq Scan)

При росте объема таблицы до миллионов строк поиск превращается в катастрофу:

  • Сложность поиска: O(N) операций дискового чтения.
  • Утилизация накопителя: 100% Random / Sequential I/O.
  • Блокировка буферного пула (Buffer Pool) и вытеснение горячих данных.

2. Что такое индекс: анатомия быстрого доступа

Определение и физическая суть

Индекс — это отдельная от таблицы физическая структура данных на диске, которая содержит отсортированные ключи и прямые указатели на строки в куче.

  • Позволяет сократить чтение с миллионов блоков до 3–4 страниц.
  • Снижает алгоритмическую сложность выборки с O(N) до O(log N).
  • Обеспечивает принудительный контроль бизнес-ограничений (уникальность).

Метафора предметного указателя

Индекс в базе данных аналогичен предметному указателю в конце 1500-страничной энциклопедии:

  • Вместо перелистывания книги от корки до корки читатель смотрит алфавитный указатель терминов.
  • По алфавиту мгновенно находится номер страницы.
  • Читатель сразу открывает нужную страницу и читает искомый абзац.

3. Структура сбалансированного дерева B-Tree

Почему именно B-Tree?

B-Tree (Balanced Tree) — основной тип индекса в реляционных базах данных благодаря трем физическим свойствам:

  • Строгая сбалансированность: Все листья находятся на абсолютно одинаковом расстоянии от корня.
  • Огромный Fan-out (ветвление): В блоке 8 КБ помещаются сотни ключей, поэтому при глубине всего в 3 уровня адресуются сотни миллионов строк.
  • Двусвязный список листьев: Листовые блоки сцеплены вперед и назад указателями.

Идеально для диапазонов и сортировки

Благодаря связности листьев B-Tree выполняет запросы диапазона за минимальное время:

  • СУБД находит левую границу диапазона через корень за 3 шага.
  • Дальнейшие строки считываются последовательно по горизонтальным ссылкам листьев без возврата к корню.
  • Клауза ORDER BY выполняется бесплатно (без фазы оперативной сортировки).

4. Пошаговый алгоритм поиска ключа в B-Tree

Маршрутизация от корня к листу

  1. Корневой узел: Считывается корневая страница, определяется ветвь диапазона (например, 0 \le K < 50$).
  2. Внутренний узел: Считывается промежуточный узел, уточняется указатель на листовой блок.
  3. Листовой узел: Бинарным поиском внутри 8 КБ блока локализуется точный ключ и берется TID.
  4. Выборка из кучи: Точечное чтение ровно одной страницы данных таблицы с диска.

Математика эффективности

Для таблицы из 20 000 000 записей:

  • Seq Scan: Чтение ~200 000 страниц кучи = ~1.6 ГБ ввода-вывода (время ~1.5–5 секунд).
  • Index Scan: Чтение 3 страниц B-Tree + 1 страница кучи = 4 страницы (32 КБ) (время < 0.5 миллисекунды).
  • Ускорение: В 50 000 раз!

5. Уникальные vs Неуникальные индексы

Уникальные индексы (UNIQUE)

CREATE UNIQUE INDEX idx_u ON T (code);

  • Гарантируют уникальность значений в масштабе всей таблицы.
  • Создаются автоматически для PRIMARY KEY и ограничений UNIQUE.
  • Требуют обязательной проверки коллизий при каждой вставке (Unique Constraint Overhead).
  • Идеальны для паспортов, ИНН, трек-номеров, email.

Неуникальные индексы (Non-Unique)

CREATE INDEX idx_nu ON T (fk_id);

  • Допускают дублирование ключей (одному ключу соответствует список разных TID).
  • Используются для ускорения фильтрации и соединения связанных сущностей.
  • Обязательны для внешних ключей (Foreign Keys), дат создания, категорий и фильтров.

6. Составные индексы (Composite Index) и левый префикс

Лексикографический порядок сортировки

Индекс CREATE INDEX idx_abc ON T (A, B, C); физически упорядочивает записи как в энциклопедическом словаре:

  • Строки упорядочены строго по A.
  • При одинаковых A — по B.
  • При одинаковых A и B — по C.

Правило левого префикса (Left-Prefix Rule)

СУБД может использовать B-Tree индекс только начиная с самого левого столбца:

  • WHERE A = 1 AND B = 2 — работает эффективно!
  • WHERE A = 1 — работает эффективно!
  • WHERE B = 2 AND C = 3 — НЕ работает! Ведущий столбец A отсутствует, поэтому СУБД выберет Seq Scan.

7. Селективность колонок и золотое правило порядка

Селективность (Cardinality / Total Rows)

Определяет, какую долю строк отсекает условие фильтрации:

  • Высокая селективность (~1.0): id клиента, номер телефона, email — отсекают 99.99% таблицы.
  • Низкая селективность (<0.05): пол, флаг активности, статус заказа.

Золотая формула порядка колонок

В составном индексе столбцы должны располагаться строго в следующем порядке:

  1. Предикаты точного равенства (=) с высокой селективностью.
  2. Диапазонные предикаты (<, >, BETWEEN) — строго после равенств!
  3. Столбцы сортировки (ORDER BY) — для ликвидации сортировки в RAM.

8. Кластеризованные vs Некластеризованные индексы

Кластеризованный индекс (Clustered)

  • Сама таблица физически упорядочена на диске по ключу индекса.
  • Листья B-Tree содержат реальные данные всех колонок строки.
  • У таблицы может быть только один кластеризованный индекс.
  • Примеры: InnoDB в MySQL (всегда PK), MS SQL Server (по умолчанию PK).

Некластеризованный индекс (Secondary)

  • Отдельная B-Tree структура, не меняющая порядок строк в куче.
  • Листья содержат только индексируемые поля и физический указатель (TID / PK).
  • У таблицы может быть множество некластеризованных индексов.
  • Требует фазы извлечения данных из кучи (Table Fetch / Bookmark Lookup).

9. Накладные расходы при модификации (DML Overhead)

Цена каждого нового индекса

Создание индекса — это не бесплатное ускорение, а строгий архитектурный компромисс:

  • INSERT: Требует синхронного добавления записей во ВСЕ индексы таблицы. Вызывает расщепление страниц (Page Split).
  • UPDATE: При изменении проиндексированного поля влечет перестройку узлов B-Tree и генерацию WAL-журнала.
  • DELETE: Удаление записей из каждого дерева.

Расход RAM и дискового пространства

  • При 8–10 индексах на таблицу объем индексов превышает саму таблицу в 2–3 раза.
  • Индексы вытесняют данные из оперативной памяти (Buffer Pool), приводя к деградации всей СУБД.
  • Правило: Индексировать только частотные запросы и внешние ключи!

10. Внешние ключи (Foreign Keys) и проблема блокировок

Главный миф начинающих разработчиков

«Раз я объявил FOREIGN KEY, база данных сама всё оптимизирует!» — НЕТ, ЭТО ОШИБКА!

  • PostgreSQL, Oracle и MS SQL Server НЕ создают индекс на внешний ключ автоматически.
  • Автоматический индекс создается только на PRIMARY KEY.

Почему индекс на FK жизненно необходим?

  1. Ускорение JOIN: Соединение таблиц по FK выполняется через мгновенный Index Scan вместо многократного Seq Scan.
  2. Предотвращение Table Lock: При удалении или обновлении строки родителя (DELETE FROM clients) СУБД без индекса на FK блокирует всю дочернюю таблицу целиком для проверки ссылок.

11. Частичные индексы (Partial Index)

Оптимизация асимметричных данных

Когда в таблице из 10 000 000 строк активными являются лишь 1% записей, индексировать всю таблицу — преступление.

CREATE INDEX idx_active_orders ON orders (client_id, created_at) WHERE status = 'ACTIVE';

  • Индексируются только строки, удовлетворяющие условию WHERE.

Преимущества Partial Index

  • Компактность: Размер индекса сокращается в 50–100 раз (помещается в L3-кэш CPU).
  • Нулевой оверхед на архив: Вставка завершенных заказов не трогает индекс.
  • Мгновенный поиск: Обеспечивает колоссальную скорость для оперативных очередей.

12. Чтение планов выполнения запросов (EXPLAIN)

Ключевые типы узлов сканирования

  • Seq Scan — последовательное чтение всей таблицы от начала до конца.
  • Index Scan — прямой поиск по B-Tree с последующим точечным извлечением строк из кучи.
  • Index Only Scan — идеальный случай (покрывающий индекс): все запрошенные поля есть в индексе, куча не читается вообще!
  • Bitmap Index Scan — сбор битовой карты страниц по индексу при выборке сотен строк.

Критические метрики плана

  • cost=0.00..45210.00 — ожидаемые затраты СУБД (startup cost .. total cost).
  • actual time=0.021..0.045 — реальное время выполнения при EXPLAIN ANALYZE.
  • Rows Removed by Filter: 1999980 — катастрофа: прочитаны миллионы строк ради единиц!

13. Инженерный чек-лист проектирования индексов

Что ОБЯЗАТЕЛЬНО нужно сделать:

  • Проиндексировать каждый FOREIGN KEY отдельным индексом.
  • Создать уникальные индексы на бизнес-ключи (трек-номера, паспорта).
  • Построить составные индексы под критические частотные запросы: [Равенства] -> [Диапазоны] -> [ORDER BY].
  • Использовать Partial Index для оперативных статусов и очередей.

Чего делать КАТЕГОРИЧЕСКИ НЕЛЬЗЯ:

  • Создавать индексы на все поля подряд «на всякий случай».
  • Индексировать низкоселективные поля (булевы флаги, пол) обычным B-Tree.
  • Ставить диапазонный столбец левее столбца точного равенства в составном индексе.
  • Забывать тестировать запросы через EXPLAIN ANALYZE.

14. Инженерная культура: цена архитектурных ошибок

Little Bobby Tables (xkcd #327)
Тематический интернет-мем: «Exploits of a Mom» (культовый веб-комикс про «Little Bobby Tables», xkcd №327).
Автор: Randall Munroe. Первоисточник: xkcd.com/327. Лицензия: Creative Commons Attribution-NonCommercial 2.5 Generic (CC BY-NC 2.5).
Связь с темой занятия: Проектирование ключевых полей, ограничений целостности и индексов требует абсолютной дисциплины. Отсутствие валидации данных или небрежное управление ключами и связями приводит к катастрофам — от паралича всей СУБД из-за каскадных блокировок таблиц до знаменитого инцидента с удалением данных школы через имя Robert'); DROP TABLE Students;--.
1 / 15