Индексы: B-tree и когда индекс не используется

Рейтинг: 67.6% · 8 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Индексы: B-tree и когда индекс не используется

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется (вы здесь)
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 21. Индексы: B-tree и когда индекс не используется

Без индексов postgresql на каждый поиск читает таблицу целиком - строку за строкой. На миллионе строк это секунды вместо миллисекунд. Индекс - это отдельная структура на диске, которая хранит значения столбца уже упорядоченными и указатели на физическое расположение строк. В этом уроке разберём самый главный тип индекса - B-tree, поймём, как он устроен внутри, что такое селективность, как работает индекс по нескольким столбцам и почему порядок столбцов критичен. И главное - почему планировщик иногда демонстративно игнорирует ваш индекс и делает seq scan, хотя индекс вот он, готовый.

Изображение

Как это работает

B-tree (балансированное дерево) - это структура из страниц-узлов. Сверху корень, под ним промежуточные узлы, в самом низу - листья. В листьях лежат отсортированные значения ключа и ссылки (TID - tuple id) на конкретные строки в таблице. Чтобы найти значение, СУБД спускается от корня к листу: на каждом уровне выбирает нужную ветку. Дерево неглубокое - даже для миллиардов строк это три-четыре спуска по страницам, а не миллион чтений.

Именно поэтому B-tree хорош для операций сравнения: равенство, больше, меньше, диапазон, BETWEEN, и для ORDER BY - данные в листьях уже отсортированы, можно идти по ним подряд. Hash так не умеет, GIN решает другие задачи. B-tree - дефолтный индекс, его ставит CREATE INDEX без указания типа.

Ключевое понятие - селективность. Это доля строк, которые остаются после условия. Поиск конкретного номера билета среди миллионов - селективность близка к нулю, индекс блестящий. Условие status = 'Arrived', когда таких рейсов половина таблицы - селективность высокая, индекс почти бесполезен. Планировщик это знает из статистики, которую собирает ANALYZE и хранит в pg_statistic.

И тут главная мысль урока. Индекс - это НЕ всегда быстрее. Чтение по индексу - это случайные прыжки по диску: нашли TID в листе, прыгнули в таблицу за самой строкой (в PostgreSQL данные строк лежат отдельно от индекса, в куче - heap). Если строк нужно много, тысячи случайных прыжков оказываются дороже, чем один последовательный проход seq scan, который читает страницы пачками по порядку. Планировщик считает стоимость обоих вариантов и выбирает дешёвый. Если он выбрал seq scan - часто он прав.

SQL и примеры

Возьмём демобазу Авиаперевозки (схема bookings). Создадим индекс по номеру рейса и посмотрим план:

Код: Выделить всё

CREATE INDEX idx_flights_flight_no ON flights (flight_no);

EXPLAIN ANALYZE
SELECT * FROM flights WHERE flight_no = 'PG0001';
Здесь номеров рейса много разных - селективность хорошая, в плане увидите Index Scan. Теперь обратный случай - условие, под которое подходит огромная доля строк:

Код: Выделить всё

EXPLAIN ANALYZE
SELECT * FROM flights WHERE status = 'Scheduled';
Таких рейсов большинство, поэтому планировщик возьмёт Seq Scan, даже если повесить индекс на status. Читать всё подряд тут дешевле, чем дёргать таблицу по каждому TID.

Составной индекс - по нескольким столбцам сразу. Порядок имеет огромное значение:

Код: Выделить всё

CREATE INDEX idx_tf_flight_fare
  ON ticket_flights (flight_id, fare_conditions);
Правило левого префикса: индекс работает для условий по flight_id, и для flight_id вместе с fare_conditions. Но для одного только fare_conditions он не подойдёт - это не левый столбец. Думайте о составном индексе как о телефонном справочнике, отсортированном сначала по фамилии, потом по имени: по фамилии искать удобно, по одному имени - придётся листать весь справочник.

Классическая ловушка - функция или приведение типа над столбцом. Это убивает обычный индекс:

Код: Выделить всё

-- индекс по book_ref НЕ сработает, lower() меняет значение
EXPLAIN ANALYZE
SELECT * FROM bookings WHERE lower(book_ref) = '00000f';

-- лечится индексом по выражению
CREATE INDEX idx_bookings_lower_ref ON bookings (lower(book_ref));
После создания индекса по выражению lower(book_ref) тот же запрос начнёт использовать индекс. Проверяйте планы через EXPLAIN ANALYZE - он показывает не оценку, а реальное время и число прочитанных строк (actual rows). В PostgreSQL 16 у Index Scan в выводе появилась полезная строка о буферах прямо в ANALYZE без отдельного BUFFERS, а в PG 17 заметно улучшили скорость самих B-tree при сканировании списков значений IN.

Частые грабли
  • Несовпадение типов: столбец bigint, а в условии WHERE id = '123' строкой или number = 5.0 - неявное приведение может отключить индекс. Передавайте литерал того же типа.
  • Функция над столбцом: WHERE date(scheduled_departure) = '2017-08-15' игнорирует индекс по самому столбцу. Перепишите через диапазон BETWEEN или сделайте индекс по выражению.
  • LIKE '%текст%' с ведущим процентом B-tree не использует - дерево отсортировано слева направо. Префиксный LIKE 'текст%' работать может.
  • Свежесозданный индекс без ANALYZE: статистика устарела, планировщик считает по старым данным и выбирает не то. После массовой загрузки запускайте ANALYZE.
  • Низкая селективность: индекс по полю с двумя-тремя значениями (пол, флаг, статус) обычно мёртвый груз - планировщик его не возьмёт, а на запись он замедляет.
  • Слишком много индексов: каждый INSERT/UPDATE обновляет все индексы таблицы. Десяток индексов на горячей таблице бьёт по скорости записи.
  • OR между разными столбцами часто мешает использовать индексы - иногда быстрее переписать через UNION.
Мини-лаба
  • Разверните демобазу Авиаперевозки и подключитесь: psql -d demo
  • Выполните EXPLAIN ANALYZE SELECT * FROM ticket_flights WHERE flight_id = 1234; запомните тип скана и время.
  • Создайте индекс: CREATE INDEX ON ticket_flights (flight_id); затем ANALYZE ticket_flights;
  • Повторите тот же EXPLAIN ANALYZE - сравните тип скана (стал Index Scan?) и время.
  • Сделайте запрос с низкой селективностью: EXPLAIN SELECT * FROM ticket_flights WHERE fare_conditions = 'Economy'; убедитесь, что планировщик выбрал Seq Scan, и объясните себе почему.
  • Спровоцируйте отказ от индекса функцией: EXPLAIN SELECT * FROM bookings WHERE lower(book_ref) = '00000f'; потом создайте индекс по выражению lower(book_ref), сделайте ANALYZE и проверьте план снова.
  • Удалите свои тестовые индексы через DROP INDEX, чтобы не мусорить в базе.
Контрольные вопросы
  • Почему B-tree подходит для диапазонных запросов и ORDER BY, а Hash - нет?
  • Что такое селективность и как она влияет на выбор между index scan и seq scan?
  • В составном индексе (a, b, c) для каких условий WHERE он сработает, а для каких нет, и почему?
  • Почему WHERE date(scheduled_departure) = ... не использует обычный индекс по столбцу и как это починить?
  • Когда seq scan действительно быстрее index scan, хотя индекс существует?
  • Зачем запускать ANALYZE после создания индекса или массовой загрузки данных?
👍3 ❤️3 🔥3 😄 🤔
Аватара пользователя
PrometheusUser
Сообщения: 1
Зарегистрирован: 20 май 2026, 09:17

Re: Индексы: B-tree и когда индекс не используется

Сообщение PrometheusUser »

Поймал ровно это: повесил индекс на status, а в плане всё равно Seq Scan. Дошло, что Scheduled там почти у всех рейсов, селективность нулевая. Снёс индекс.
👍2 ❤️2 🔥 😄 🤔
Аватара пользователя
tension
Сообщения: 1
Зарегистрирован: 20 май 2026, 12:40

Re: Индексы: B-tree и когда индекс не используется

Сообщение tension »

А индекс по выражению lower(book_ref) надо потом руками поддерживать или ANALYZE сам статистику по нему соберёт?
👍 ❤️1 🔥1 😄 🤔
Ответить
← Предыдущая глава
Последовательности, IDENTITY, генерируемые столбцы
Следующая глава →
Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: частичный и покрывающий индекс в postgresql когда нуженjsonb в postgresql операторы и индексацияпартиционирование таблиц postgresql по датематериализованное представление postgresql как обновлятьpostgresql какой индекс выбрать btree gin gist brin

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя