Когда мы говорим о B+ Tree, важно понимать, что именно лежит в его листовых узлах. От этого зависит архитектура всей базы данных.
1. Clustered Index (Кластерный)
В этой концепции индекс — это и есть таблица (Index-Organized Table). Листовые узлы дерева хранят физически полную строку данных. Строки на диске упорядочены ровно так, как диктует этот индекс. Очевидно, что такой индекс может быть только один на таблицу.
- Плюсы: Феноменально быстрые Range-запросы (считываем данные прямо из листьев подряд).
- Минусы: Если ты обновляешь (UPDATE) кластеризующий ключ, базе приходится физически перемещать всю тяжелую строку данных в другое место на диске. Это очень дорого.
- Пример: MySQL (движок InnoDB) по умолчанию хранит все таблицы как кластерные индексы на базе Primary Key.
2. Non-Clustered Index (Некластерный)
Листья такого дерева хранят только сам проиндексированный ключ и указатель (Pointer/ID) на место, где физически лежит полная строка.
Когда ты ищешь данные по некластерному индексу, происходит Bookmark Lookup (двойной прыжок). Сначала индекс за O(log N) находит нужный указатель, а затем делает дополнительное случайное чтение с диска, чтобы вытащить саму строку из таблицы. Таких индексов может быть сколько угодно.
Архитектурная разница: В PostgreSQL вообще нет кластерных индексов в классическом понимании. Таблица в PG — это неупорядоченная «куча» (Heap). И первичный ключ, и вторичные индексы там равноправны — они просто хранят ссылки (CTID) на эту кучу.
3. Covering Index (Покрывающий)
Bookmark Lookup — это дорого. Если запрос требует вытащить 10 000 строк по вторичному индексу, база сделает 10 000 дополнительных прыжков на диск. Чтобы этого избежать, придумали механизм .INCLUDE
Это способ прикрепить к листьям индекса дополнительную (не ключевую) «полезную нагрузку».
-- Пример: мы часто ищем заказы клиента, и нам нужна только сумма (total_sum). -- Мы добавляем total_sum прямо в листья индекса. CREATE INDEX idx_orders_covering ON orders (customer_id) -- ключ для поиска (B-Tree) INCLUDE (total_sum); -- просто данные в листьях
Теперь база найдет и сразу же отдаст customer_id прямо из индекса (Index-Only Scan), ни разу не обратившись к основной таблице!total_sum
Ловушка MVCC (Почему Covering деградирует)
В Computer Science есть концепция MVCC (Multi-Version Concurrency Control), которая позволяет транзакциям не блокировать друг друга. База хранит несколько версий одной и той же строки. И здесь кроется главный подвох покрывающих индексов.
В системах вроде PostgreSQL индекс не знает, какая версия строки сейчас активна (видима) для твоей транзакции. Если таблица часто обновляется (высокий DML), Index-Only Scan вырождается:
- Индекс находит нужные данные в своих листьях.
- Но он не уверен, не удалены ли они параллельной транзакцией.
- Ему всё равно приходится делать дорогой прыжок в Visibility Map или саму таблицу, чтобы проверить статус строки.
Вывод: Покрывающий индекс — мощный инструмент для таблиц, которые читаются чаще, чем пишутся. В write-heavy таблицах он будет просто тратить память.