Теоретический конспект: Физическое индексирование, связи и планы запросов

ОП.08 Основы проектирования баз данных — Тема 3. Этапы проектирования баз данных — Практическое занятие №6 (Занятие №18)

1. Роль индексов в архитектуре реляционных СУБД

В реляционной теории отношения математически не имеют определенного порядка строк (множество кортежей неупорядочено). На физическом уровне данные таблицы хранятся на диске в виде страниц (блоков размером от 8 КБ до 64 КБ) в так называемой куче (Heap), куда новые строки записываются в свободные пространства без какой-либо сортировки.

Если таблица содержит 10 000 000 строк и не имеет индексов, то выполнение простейшего запроса:

SELECT * FROM clients WHERE passport_number = '4510 123456';

вынуждает СУБД выполнить последовательное сканирование (Sequential Scan, Seq Scan) — прочитать с накопителя абсолютно все страницы таблицы от первой до последней, проверить каждую запись в памяти процессора и отбросить неподходящие. При размере таблицы в десятки гигабайт это занимает десятки секунд и блокирует дисковую подсистему ввода-вывода.

Индекс (Index) — это вспомогательная структура данных на диске, физически отделенная от таблицы, которая хранит отсортированные значения определенных столбцов (ключей) и прямые физические указатели (TID / RowID) на местоположение соответствующих строк в куче.

2. Пошаговые схемы поиска по индексу B-Tree

Наиболее распространенным и универсальным типом индекса в мире реляционных баз данных является B-Tree (сбалансированное дерево). Его листовые страницы содержат отсортированные ключи и указатели на данные, а внутренние узлы направляют поиск от корня к нужному листу.

[ КОРЕНЬ: 50 | 100 ] / | / | [ ВНУТРЕННИЙ: 20 | 35 ] [ 70 | 85 ] [ 120 | 150 ] / | / | [ЛИСТ: 10, 15, 18] <--> [ЛИСТ: 20, 25, 30] <--> [ЛИСТ: 35, 40, 45] ... | | | (TID: блок 4, поз 1) (TID: блок 7, поз 3) (TID: блок 2, поз 9)

Пошаговый алгоритм поиска значения (на примере поиска ключа 25):

  1. Шаг 1. Обращение к корневому узлу (Root Node): СУБД считывает корень дерева. Корневой узел содержит границы диапазонов: < 50, 50..100, > 100. Значение 25 меньше 50, поэтому СУБД переходит по первому левому указателю.
  2. Шаг 2. Обращение к промежуточному узлу (Internal Node): Узел содержит ключи [20 | 35]. Значение 25 попадает в диапазон между 20 и 35 (0 \le 25 < 35$). СУБД переходит по среднему указателю на листовую страницу.
  3. Шаг 3. Чтение листового узла (Leaf Node): Листовой узел содержит ключи [20, 25, 30]. Методом бинарного поиска внутри блока процессор мгновенно находит точное совпадение ключа 25 и извлекает физический указатель строки: TID = (блок 7, позиция 3).
  4. Шаг 4. Точечное чтение строки из кучи (Table Fetch): СУБД запрашивает у операционной системы ровно один блок данных с диска (блок №7) и считывает всю строку таблицы.
Сложность операции: Для таблицы из 10 000 000 записей глубина сбалансированного B-Tree составляет всего 3 или 4 уровня. Вместо чтения 500 000 блоков кучи при Seq Scan, СУБД считывает 3 блока индекса + 1 блок таблицы = 4 операции ввода-вывода (I/O). Запрос выполняется за 0.2 миллисекунды!

3. Сравнение стоимости: Seq Scan vs Index Scan

Оптимизатор запросов СУБД (Query Optimizer) при получении любого SQL-запроса выполняет стоимостную оценку (Cost-based Optimization). Стоимость измеряется в условных единицах дискового ввода-вывода (где 1.0 — чтение одного блока данных последовательно, а 4.0 — случайное чтение блока).

Параметр сравнения Seq Scan (Последовательное сканирование) Index Scan (Поиск по индексу B-Tree)
Характер чтения диска Последовательный (Sequential I/O). Диск читает блоки подряд на максимальной скорости. Случайный (Random I/O). Чтение разрозненных страниц индекса и кучи.
Эффективен при выборке Более 10–20% от общего числа строк таблицы (высокая доля возвращаемых строк). Менее 1–5% от общего объема таблицы (точечный или узкодиапазонный поиск).
Зависимость времени от размера Линейная (N)$. С ростом таблицы время растет прямо пропорционально. Логарифмическая (\log N)$. При росте таблицы в 100 раз время поиска почти не меняется.
Сортировка данных Требует дополнительной фазы тяжелой сортировки в оперативной памяти (Sort). Данные возвращаются уже упорядоченными по ключу индекса без затрат CPU.
Парадокс селективности: Если запрос запрашивает 60% строк таблицы (например, WHERE пол = 'M'), использование индекса приведет к чудовищной деградации, так как миллион случайных обращений к диску (Random I/O) медленнее, чем чтение всей таблицы одним сплошным потоком. В таких случаях СУБД осознанно выбирает Seq Scan!

4. Правила проектирования составных индексов и селективность

Составной индекс (Composite Index) строится по двум или более колонкам: CREATE INDEX idx_name ON table (col1, col2, col3);.

Селективность столбца (Column Selectivity)

Селективность определяется долей уникальных значений в столбце по отношению к общему числу строк:

1105862\text{Селективность} = \frac{\text{Количество уникальных значений (Cardinality)}}{\text{Общее число строк таблицы}}1105862

Правило левого префикса и порядок колонок

Порядок полей в составном индексе определяет, сможет ли запрос использовать его эффективно:

Архитектурная формула:
1. На первое место ставятся столбцы с условием точного равенства (=) с наибольшей селективностью.
2. Затем ставятся столбцы дополнительных условий равенства.
3. Строго после них ставятся столбцы с диапазонами (>, <, BETWEEN, LIKE 'abc%').
4. В конце добавляются столбцы для устранения сортировки (ORDER BY).

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

Внешний ключ связывает дочернюю таблицу с родительской. Однако многие разработчики забывают, что СУБД НЕ создает индекс на FK автоматически.

Две катастрофы отсутствия индекса на FK:

  1. Медленные JOIN: При формировании отчетов со связкой таблиц СУБД вынуждена многократно сканировать дочернюю таблицу методом Seq Scan, что приводит к зависанию запросов.
  2. Каскадные блокировки (Table Locks): Когда транзакция удаляет или обновляет строку в родительской таблице (например, DELETE FROM clients WHERE id = 100), СУБД обязана проверить, нет ли дочерних заказов этого клиента. Без индекса на orders.client_id СУБД накладывает разделяемую блокировку на всю таблицу orders и запускает Seq Scan по миллионам строк. В этот момент все кассиры и клиенты не могут оформить ни одного нового заказа!

6. Вопросы для самопроверки

1. Почему все листовые узлы B-Tree дерева находятся на одной глубине?

B-Tree является самобалансирующимся деревом. При добавлении данных переполненный узел расщепляется ровно пополам, выталкивая медианный ключ на уровень выше. Рост дерева происходит строго снизу вверх от корня, что обеспечивает постоянную глубину всех листьев и гарантию поиска за (\log N)$.

2. Чем отличается уникальный индекс от неуникального на физическом уровне?

Функционально уникальный индекс запрещает дублирование значений, выполняя обязательную проверку уникальности (Unique Check) при каждой операции вставки. Неуникальный индекс допускает хранение множества одинаковых ключей, связывая их с разными идентификаторами строк (TID).

3. Создан индекс на (A, B, C). Будет ли он использоваться для запроса WHERE B = 5 AND C = 10?

Нет, не будет (либо будет использоваться неэффективно через полное сканирование индекса). Согласно правилу левого префикса, поиск по B-Tree требует обязательного наличия ведущей колонки индекса (A). Без колонки A дерево не может локализовать ветку поиска.

4. В чем заключается компромисс между скоростью чтения и накладными расходами при записи?

Каждый созданный индекс ускоряет операции чтения (SELECT), но напрямую замедляет операции модификации данных (INSERT, UPDATE, DELETE), так как любая вставка строки в таблицу требует синхронного добавления ключей во все существующие индексы таблицы, что порождает дисковый ввод-вывод и риск расщепления страниц.