Индексы: типы, B-tree, составные и покрывающие
Кратко о теме
Заголовок раздела «Кратко о теме»Индекс — это отдельная от таблицы структура данных, которая хранит значения одного или нескольких выражений в упорядоченном (или хешированном) виде вместе с физическими адресами строк и позволяет найти нужные строки, не читая таблицу целиком. Главная идея: без индекса единственный способ найти строку — последовательное чтение всей таблицы (Seq Scan) со сложностью O(N); индекс превращает поиск в спуск по дереву за O(log N) или в один хеш-переход за O(1). Индекс — это чистая оптимизация чтения: он ничего не добавляет к логике запросов, любой запрос вернёт тот же результат и без него, но плата за ускорение чтения — место на диске и замедление INSERT/UPDATE/DELETE, потому что каждый индекс надо поддерживать в согласованном состоянии.
Модель, которую надо держать в голове для PostgreSQL: таблица хранится в куче (heap) — неупорядоченном наборе страниц по 8 КБ, строка адресуется парой (номер страницы, номер слота) — это TID. Все индексы в PostgreSQL — вторичные: они хранят ключ и TID, а сами данные строки лежат в куче. Отсюда важное следствие — обычный Index Scan делает два обращения: спуск по индексу, затем случайный поход в heap за строкой. Именно поэтому индекс выгоден только тогда, когда он отсеивает большую часть таблицы: если выборка возвращает существенную долю строк, планировщик предпочтёт Seq Scan, потому что последовательное чтение страниц дешевле множества случайных. В MySQL/InnoDB и MS SQL модель другая: там есть кластерный индекс — сама таблица физически хранится в B-tree по первичному ключу, а вторичные индексы ссылаются не на физический адрес, а на значение PK.
Рабочая лошадка — B-tree, точнее B+-tree: сбалансированное сильно ветвящееся дерево, у которого все данные лежат в листьях, листья связаны в двусвязный список, а размер узла равен странице (8 КБ). Ветвление в сотни ключей даёт высоту 3–4 уровня даже на сотнях миллионов строк, то есть 3–4 обращения к страницам вместо миллионов. B-tree поддерживает =, <, >, BETWEEN, IN, IS NULL, префиксный LIKE 'abc%', ORDER BY и MIN/MAX — то есть всё, что опирается на порядок. Остальные типы индексов в PostgreSQL закрывают то, что B-tree не умеет: hash — только строгое равенство, но короче и быстрее на длинных ключах; GiST и SP-GiST — геометрия, диапазоны, пересечения, kNN; GIN — «много значений в одной строке»: массивы, JSONB, полнотекстовый поиск, триграммы; BRIN — очень компактный индекс для гигантских таблиц, где данные физически коррелируют с ключом (типично — append-only таблицы с временем).
Второй ключевой сюжет — составные (многоколоночные) индексы и правило левого префикса. Индекс по (a, b, c) упорядочен лексикографически, поэтому им можно пользоваться для условий на a, на a, b, на a, b, c, но не для условия только на b или только на c. Порядок колонок в индексе критичен, а порядок условий в WHERE — нет: планировщик нормализует конъюнкцию. Практическое правило построения: сначала колонки с равенством, затем одна колонка с диапазоном, затем колонки для ORDER BY, а то, что нужно только в SELECT, выносится в INCLUDE — так получается покрывающий индекс, по которому возможен Index Only Scan без похода в кучу.
Третий сюжет — как понять, что происходит. Единственный честный инструмент — EXPLAIN (ANALYZE, BUFFERS): он показывает реальный план, реальное время и реальное число строк. Заголовки узлов Seq Scan с большим Rows Removed by Filter, расхождение rows= в оценке и в факте, Bitmap Heap Scan с Recheck Cond и lossy блоками — это язык, на котором база объясняет, чего ей не хватает.
Вопросы и ответы
Заголовок раздела «Вопросы и ответы»Какие типы индексов вы знаете и в чем их разница?
Заголовок раздела «Какие типы индексов вы знаете и в чем их разница?»Коротко. По структуре: B-tree (упорядоченный, универсальный — равенство, диапазоны, сортировка), hash (только равенство, по хешу значения), GiST/SP-GiST (сбалансированные деревья для «нескалярных» типов — геометрия, диапазоны, kNN), GIN (обратный индекс для составных значений — массивы, JSONB, full-text), BRIN (сводки min/max по диапазонам страниц, для огромных коррелированных таблиц). По свойствам они ортогонально делятся на уникальные/неуникальные, одноколоночные/составные, полные/частичные, по колонке/по выражению, покрывающие (INCLUDE), кластерные/некластерные.
Глубже. Разница между типами — в том, какие операторы они умеют ускорять; это в PostgreSQL формализовано через классы операторов (pg_opclass) и методы доступа (pg_am). B-tree знает < <= = >= > и производные, поэтому только он умеет отдавать данные в отсортированном виде и обслуживать ORDER BY/MIN/MAX. Hash знает только =. GIN знает «содержит»: @>, ?, @@. GiST — «пересекается», &&, <->. BRIN хранит для каждой группы страниц (по умолчанию 128) сводку min/max и при поиске отбрасывает целые диапазоны страниц; он крошечный (мегабайты на терабайтную таблицу), но эффективен только если физический порядок строк коррелирует со значением колонки.
CREATE INDEX idx_btree ON orders (created_at);CREATE INDEX idx_hash ON sessions USING hash (token);CREATE INDEX idx_gin ON docs USING gin (payload jsonb_path_ops);CREATE INDEX idx_gist ON events USING gist (period); -- tsrangeCREATE INDEX idx_brin ON metrics USING brin (ts) WITH (pages_per_range = 64);Чем отличается hash-индекс от B-tree индекса ?
Заголовок раздела «Чем отличается hash-индекс от B-tree индекса ?»Коротко. B-tree хранит ключи в отсортированном виде и поэтому поддерживает равенство, диапазоны, сортировку, префиксный LIKE и составные ключи; hash хранит только 32-битный хеш значения и поддерживает исключительно оператор =. Зато hash не зависит от длины значения и на очень длинных строках может быть компактнее и быстрее.
Глубже. Практические ограничения hash-индекса в PostgreSQL: он одноколоночный, не поддерживает UNIQUE, не может обслуживать ограничения PRIMARY KEY/UNIQUE, не используется для ORDER BY, не поддерживает INCLUDE. До PostgreSQL 10 hash-индексы не писались в WAL — то есть не переживали крах и не реплицировались, из-за чего их годами не рекомендовали; с 10-й версии они полноценные и WAL-логируемые. Сложность поиска у hash — O(1) против O(log N) у B-tree, но на практике эта разница почти не видна, потому что log N при ветвлении в сотни — это 3–4 обращения к страницам, и оба варианта упираются в один поход в кучу. Реальный выигрыш hash даёт на длинных ключах (URL, длинные токены), где B-tree раздувается, а hash хранит фиксированные 4 байта.
Как индексы влияют на производительность запросов?
Заголовок раздела «Как индексы влияют на производительность запросов?»Коротко. Индексы ускоряют чтение — точечный поиск, диапазоны, сортировку, соединения, агрегаты MIN/MAX, проверку уникальности — превращая O(N) полный перебор в O(log N) спуск. И замедляют запись: каждый INSERT/DELETE и каждый UPDATE проиндексированной колонки требует правки всех соответствующих индексов, плюс индексы занимают место и конкурируют за буферный кеш.
Глубже. Важно, что выигрыш нелинейно зависит от селективности. Если запрос возвращает 0,1 % таблицы, Index Scan выигрывает на порядки. Если 30 % — индекс, скорее всего, проиграет Seq Scan: 30 % случайных обращений к куче дороже одного последовательного прохода, и планировщик это посчитает через random_page_cost (по умолчанию 4.0) против seq_page_cost (1.0). На SSD random_page_cost разумно снижать до 1.1–2.0, иначе планировщик систематически недооценивает индексы. Промежуточный режим — Bitmap Index Scan + Bitmap Heap Scan: база собирает битовую карту нужных страниц, сортирует их и читает кучу в физическом порядке, что превращает случайный доступ в почти последовательный.
PostgreSQL. Types of indexes. Isolation levels
Заголовок раздела «PostgreSQL. Types of indexes. Isolation levels»Коротко. Типы индексов в PostgreSQL: B-tree, Hash, GiST, SP-GiST, GIN, BRIN (плюс расширения: bloom, rum, pgvector с hnsw/ivfflat). Уровни изоляции по стандарту SQL — Read Uncommitted, Read Committed, Repeatable Read, Serializable; PostgreSQL реализует три: Read Uncommitted работает как Read Committed, по умолчанию Read Committed.
Глубже. В PostgreSQL изоляция построена на MVCC со снимками (snapshot). В Read Committed каждый оператор берёт свой свежий снимок — отсюда возможны non-repeatable read и phantom read внутри транзакции. Repeatable Read берёт снимок один раз на всю транзакцию, поэтому там нет ни неповторяющегося чтения, ни фантомов (в PostgreSQL RR строже стандарта — это фактически snapshot isolation), но возможны аномалии сериализации и ошибка could not serialize access due to concurrent update. Serializable добавляет SSI (Serializable Snapshot Isolation) — отслеживание опасных структур зависимостей чтения/записи и откат транзакции с 40001. Связь с индексами прямая: SSI берёт предикатные блокировки в том числе на страницы индексов, поэтому наличие индекса влияет на гранулярность блокировок и число ложных откатов. Подробнее уровни изоляции разбираются в подтеме transactions.
Типы индексов в постгресе и их особенности
Заголовок раздела «Типы индексов в постгресе и их особенности»Коротко. См. выше. Кратко по особенностям: B-tree — универсальный и единственный, кто умеет сортировку и уникальность; Hash — только =; GiST — расширяемое дерево для геометрии, диапазонов, kNN-поиска, поддерживает exclusion constraints; SP-GiST — несбалансированные разбиения (quad-tree, radix-tree), хорош для точек и текстовых префиксов; GIN — для «много ключей в строке», быстрый поиск, медленная запись (смягчается fastupdate и pending-list); BRIN — крошечный, для append-only таблиц с высокой корреляцией.
Глубже. Практические детали, которые ждут на собеседовании. GIN дорог на запись: обновление одной строки с большим массивом трогает много записей индекса, поэтому включён fastupdate, копящий изменения в pending-list, который потом схлопывается вакуумом. Для JSONB есть два opclass: jsonb_ops (по умолчанию, индексирует и ключи, и значения, поддерживает ?, ?&, ?|, @>) и jsonb_path_ops (только @>, но заметно компактнее и быстрее). BRIN бесполезен, если данные вставляются вперемешку: проверить корреляцию можно через pg_stats.correlation. GiST поддерживает INCLUDE с PostgreSQL 12, B-tree — с 11.
Зачем нужны индексы в БД? Примеры индексов
Заголовок раздела «Зачем нужны индексы в БД? Примеры индексов»Коротко. Индексы нужны, чтобы находить строки без полного перебора таблицы, поддерживать ограничения уникальности, отдавать данные в готовом порядке для ORDER BY/GROUP BY и ускорять соединения. Примеры: обычный B-tree по users(email), уникальный индекс за PRIMARY KEY, составной orders(user_id, created_at DESC), частичный WHERE deleted_at IS NULL, по выражению lower(email), GIN по tsvector для полнотекстового поиска.
Глубже.
CREATE UNIQUE INDEX users_email_uq ON users (lower(email));CREATE INDEX orders_user_created ON orders (user_id, created_at DESC);CREATE INDEX orders_active ON orders (created_at) WHERE status = 'active';CREATE INDEX docs_fts ON docs USING gin (to_tsvector('russian', body));CREATE INDEX users_name_trgm ON users USING gin (full_name gin_trgm_ops);Обратите внимание на индекс по выражению lower(email): он сработает только если запрос написан ровно как WHERE lower(email) = $1. Планировщик сопоставляет выражения синтаксически, а не «понимает» их семантику.
Почему поиск в B-tree работает быстрее полного перебора данных?
Заголовок раздела «Почему поиск в B-tree работает быстрее полного перебора данных?»Коротко. Потому что каждый спуск на уровень вниз отбрасывает почти все оставшиеся ключи: при ветвлении в сотни ключей за 3–4 обращения к страницам мы приходим ровно к нужному листу, тогда как полный перебор читает все страницы таблицы. Формально O(log_b N) против O(N), и, что важнее на практике, 3–4 обращения к диску против десятков тысяч.
Глубже. Оценка в числах: страница 8 КБ, ключ типа bigint плюс служебные данные — порядка 20–25 байт на запись, значит во внутреннем узле помещается примерно 300–400 разделителей. Дерево высотой 4 адресует 300⁴ ≈ 8·10⁹ записей. То есть для таблицы в миллиард строк нужно 4 обращения к страницам, причём верхние 1–2 уровня почти всегда в буферном кеше — фактически остаётся 1–2 реальных чтения с диска плюс поход в кучу. Полный перебор миллиарда строк — это чтение всех страниц таблицы, десятки гигабайт. Внутри страницы поиск идёт бинарно по массиву указателей, но это чистая работа с уже прочитанной памятью и в стоимости почти не участвует. Оговорка: «быстрее» верно для выборок малой доли; на «отдай мне 80 % таблицы» B-tree проиграет Seq Scan.
Какие бывают индексы и для чего они нужны, какие есть проблемы?
Заголовок раздела «Какие бывают индексы и для чего они нужны, какие есть проблемы?»Коротко. Типы перечислены выше; нужны для ускорения поиска, сортировки, соединений и уникальности. Проблемы: замедление записи, дополнительное место, раздувание (bloat) при частых обновлениях, устаревшая статистика и как следствие плохие планы, неиспользуемые индексы, которые только вредят, и невозможность использовать индекс при некоторых формах предикатов (функция над колонкой, LIKE '%x%', несовпадение типов или коллации).
Глубже. Отдельная категория проблем — операционная. CREATE INDEX берёт блокировку SHARE и останавливает запись в таблицу, поэтому в проде всегда CREATE INDEX CONCURRENTLY — она не блокирует запись, но делает два прохода по таблице, не может выполняться в транзакции и при неудаче оставляет INVALID-индекс, который надо удалить вручную. Второе — bloat: HOT-обновления (когда изменённая колонка не проиндексирована и в странице есть место) не трогают индексы, а вот обычные обновления оставляют мёртвые версии в каждом индексе, которые вычищает VACUUM. В PostgreSQL 14 появилось bottom-up index deletion, которое сильно уменьшает раздувание индексов при повторяющихся обновлениях неиндексированных колонок.
Зачем нужны индексы в базе и их примеры?
Заголовок раздела «Зачем нужны индексы в базе и их примеры?»Коротко. См. выше «Зачем нужны индексы в БД? Примеры индексов». Отличие формулировки нулевое: индекс — структура для быстрого поиска и поддержки ограничений; типовой набор для OLTP-таблицы — уникальный по бизнес-ключу, B-tree по внешним ключам, составной под самый частый фильтр+сортировку.
Почему BTREE лучше AVL?
Заголовок раздела «Почему BTREE лучше AVL?»Коротко. AVL — бинарное дерево: ветвление 2, высота log₂N (≈30 для миллиарда записей), и каждый узел — отдельное обращение к памяти или диску. B-tree ветвится в сотни, узел равен странице диска, высота 3–4, поэтому число обращений к диску на порядок меньше. B-tree проектировался под блочные носители, AVL — под оперативную память.
Глубже. Ключевая метрика для СУБД — не количество сравнений, а количество прочитанных блоков. Диск (и даже NVMe) читает минимум страницу; прочитать 8 КБ ради одного 8-байтового ключа — расточительство, поэтому B-tree упаковывает в эту страницу сотни ключей и «окупает» чтение. Второе — AVL требует ротаций почти при каждой вставке, а ротация в дисковой структуре означает перезапись нескольких страниц и WAL-записей; B-tree меняет обычно одну страницу, а расщепление узла происходит редко и амортизировано. Третье — в B+-tree листья связаны в список, поэтому диапазонный запрос и ORDER BY — это один спуск и последовательный проход по листьям; в AVL диапазон — это обход дерева с прыжками по памяти. В in-memory базах, где страница не важна, действительно применяют бинарные деревья и skip-list — там аргумент про блоки не работает.
Что такое индекс? Как он ускоряет поиск? Типы индексов. Отличие B-tree от хэш-индекса.
Заголовок раздела «Что такое индекс? Как он ускоряет поиск? Типы индексов. Отличие B-tree от хэш-индекса.»Коротко. Индекс — вспомогательная структура «значение → адрес строки», позволяющая найти строки без полного сканирования. Ускоряет за счёт упорядоченности (спуск по дереву за O(log N)) или хеширования (O(1)). Типы в PostgreSQL: B-tree, hash, GiST, SP-GiST, GIN, BRIN. B-tree универсален и поддерживает порядок; hash — только =, зато компактен на длинных ключах и не поддерживает уникальность, сортировку и составные ключи.
Глубже. Есть ещё одно отличие, о котором забывают: B-tree в PostgreSQL умеет INCLUDE и, соответственно, Index Only Scan, а hash — нет; hash-индекс всегда требует похода в кучу. И B-tree умеет работать как «частично подходящий» индекс: если условия только на часть колонок составного ключа, он всё равно даст диапазонный скан по префиксу, а остальное отфильтрует. Hash либо подходит целиком, либо не подходит вовсе.
Важна ли последовательность условий для составного индекса?
Заголовок раздела «Важна ли последовательность условий для составного индекса?»Коротко. Последовательность условий в WHERE не важна — a = 1 AND b = 2 и b = 2 AND a = 1 дают идентичный план. Важна последовательность колонок в самом индексе: индекс по (a, b) работает для фильтра по a и по a, b, но не для фильтра только по b.
Глубже. Планировщик PostgreSQL приводит конъюнкцию предикатов к нормализованному списку RestrictInfo и подбирает к каждому индексу подходящие условия независимо от порядка их написания; более того, он сам переупорядочит их в Index Cond. А вот индекс физически лексикографически упорядочен по кортежу колонок, поэтому только левый префикс даёт непрерывный диапазон в листьях. Отдельный нюанс — «диапазон обрывает префикс»: в индексе (a, b) при WHERE a > 10 AND b = 5 условие по b уже не сужает диапазон спуска, оно проверяется как фильтр по каждой записи. Поэтому правило — сначала равенства, потом диапазон.
Что такое индексы? Какие индексы бывают, для чего используются, когда их нежелательно использовать?
Заголовок раздела «Что такое индексы? Какие индексы бывают, для чего используются, когда их нежелательно использовать?»Коротко. См. определение выше. Нежелательно использовать: на колонках с низкой кардинальностью (пол, boolean, enum из нескольких значений), на маленьких таблицах (пара страниц читается быстрее, чем спуск по индексу), на таблицах с интенсивной записью и редким чтением, «на всякий случай» на каждой колонке, а также на колонках, которые постоянно обновляются (это ломает HOT-обновления и раздувает индексы).
Глубже. Ещё три ситуации, когда индекс не поможет. Первая: предикат не sargable — WHERE date_trunc('day', created_at) = $1, WHERE age + 1 > 20, WHERE cast(id as text) = '5' — функция над колонкой убивает индекс; лечится индексом по выражению или переписыванием предиката в диапазон. Вторая: несовпадение типов и особенно коллации — индекс, созданный при одной коллации, не годится для LIKE 'abc%', если коллация не C (нужен text_pattern_ops). Третья: OR по разным колонкам — иногда лучше UNION ALL или BitmapOr по двум индексам. И общее: индекс на low-cardinality колонку иногда всё-таки полезен — как частичный (WHERE status = 'pending', когда таких строк 0,1 %) или как вторая колонка составного индекса.
Как определить, использует ли запрос индекс?
Заголовок раздела «Как определить, использует ли запрос индекс?»Коротко. Посмотреть план: EXPLAIN (ANALYZE, BUFFERS) <запрос>. Если в плане есть Index Scan, Index Only Scan или Bitmap Index Scan с именем индекса — используется; если Seq Scan с Filter — нет. Плюс pg_stat_user_indexes.idx_scan показывает, сколько раз индекс вообще применялся с момента сброса статистики.
Глубже. Тонкости чтения плана. Index Cond — это условия, которые ушли внутрь индекса и сузили спуск; Filter рядом с Index Scan — условия, проверяемые уже по вынутым строкам, и большой Rows Removed by Filter означает, что индекс подобран неудачно. EXPLAIN без ANALYZE не выполняет запрос и показывает только оценки; ANALYZE выполняет (в транзакции с откатом, если это DML). BUFFERS показывает реальные чтения страниц. Полезно уметь принудительно проверить гипотезу: SET enable_seqscan = off; — если план с индексом стал дешевле по факту, значит планировщику мешает статистика или настройки стоимости; в проде так делать не надо, это диагностика.
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)SELECT id FROM orders WHERE user_id = 42 ORDER BY created_at DESC LIMIT 20;
SELECT relname, indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))FROM pg_stat_user_indexesWHERE idx_scan = 0 ORDER BY pg_relation_size(indexrelid) DESC;Вопрос про индексы. Какие индексы в PostgreSQL знаешь?
Заголовок раздела «Вопрос про индексы. Какие индексы в PostgreSQL знаешь?»Коротко. Встроенные методы доступа: B-tree, Hash, GiST, SP-GiST, GIN, BRIN. Дополнительно из расширений — bloom, rum, pgvector (hnsw, ivfflat). Список доступных методов виден в pg_am.
Глубже. Хорошо к перечислению добавить, под что каждый: B-tree — скалярные типы и всё, что про порядок; Hash — равенство по длинным ключам; GIN — jsonb, массивы, tsvector, pg_trgm; GiST — geometry/geography (PostGIS), диапазонные типы, EXCLUDE-ограничения, kNN; SP-GiST — точки, IP-адреса (inet), префиксы текста; BRIN — временные ряды и логи. Отдельно стоит помнить, что «частичный», «уникальный», «покрывающий», «по выражению» — это не типы индексов, а свойства, применимые в основном к B-tree.
SELECT amname FROM pg_am WHERE amtype = 'i';как ускорить запросы в sql (индексы и view)
Заголовок раздела «как ускорить запросы в sql (индексы и view)»Коротко. Индексы — да, ускоряют: подобранный под предикат и сортировку индекс убирает Seq Scan и внешнюю сортировку. Обычный VIEW сам по себе не ускоряет ничего — это подставляемый в запрос текст, план строится заново каждый раз. Ускоряет MATERIALIZED VIEW: результат физически сохранён, по нему можно строить индексы, но данные надо обновлять через REFRESH MATERIALIZED VIEW [CONCURRENTLY].
Глубже. Полный порядок действий при «тормозит запрос»: снять EXPLAIN (ANALYZE, BUFFERS), найти узел с наибольшим фактическим временем и наибольшим расхождением rows оценка/факт; проверить, свежая ли статистика (ANALYZE, pg_stat_user_tables.last_autoanalyze); переписать непригодные для индекса предикаты; добавить точечный индекс (составной под фильтр + сортировку, при необходимости покрывающий); проверить work_mem, если видно Sort Method: external merge Disk; и только потом думать про денормализацию, материализованные представления и кеш. REFRESH MATERIALIZED VIEW CONCURRENTLY требует уникального индекса на представлении и не блокирует чтения, но работает медленнее обычного refresh.
Что такое индексы и для чего они нужны?
Заголовок раздела «Что такое индексы и для чего они нужны?»Коротко. Индекс — отдельная упорядоченная структура данных над колонками таблицы, хранящая ключ и ссылку на строку. Нужен, чтобы находить строки за O(log N) вместо полного перебора O(N), поддерживать уникальность (PRIMARY KEY, UNIQUE, EXCLUDE), отдавать готовый порядок для ORDER BY, ускорять JOIN и агрегаты MIN/MAX.
Глубже. Хорошая аналогия для собеседования — алфавитный указатель в книге: он занимает лишние страницы и его надо переписывать при каждом переиздании, но найти нужное слово по нему на порядки быстрее, чем листать книгу. Ключевое, что стоит проговорить: индекс — это trade-off «чтение против записи и места», и решение о его создании принимается на основе конкретных запросов, а не «на всякий случай». Второе, что стоит проговорить: наличие индекса не гарантирует его использование — решает планировщик на основе статистики и оценки стоимости.
Как индексы влияют на производительность операций JOIN ? Если две таблицы соединяются по определенной колонке, поможет ли индекс на этой колонке ускорить поиск связанных строк?
Заголовок раздела «Как индексы влияют на производительность операций JOIN ? Если две таблицы соединяются по определенной колонке, поможет ли индекс на этой колонке ускорить поиск связанных строк?»Коротко. Да, поможет — прежде всего для Nested Loop: индекс на колонке соединения внутренней таблицы превращает поиск соответствий из полного скана в O(log N) на каждую строку внешней таблицы. Для Merge Join индекс даёт уже отсортированный вход и убирает сортировку. Для Hash Join индекс на ключе соединения обычно не нужен — там строится хеш-таблица по одной из сторон.
Глубже. Правило, которое стоит помнить: индексируется в первую очередь сторона, по которой ищут, — то есть внутренняя таблица Nested Loop, обычно «многие» в связи «один ко многим». PostgreSQL не создаёт индексы под внешние ключи автоматически (в отличие от MySQL/InnoDB), а они нужны и для JOIN, и для того, чтобы DELETE/UPDATE родительской строки не устраивал Seq Scan по дочерней таблице ради проверки ссылочной целостности. Полезны и индексы на колонках фильтрации, потому что они сокращают размер входа для соединения. И наоборот: если после фильтрации соединяются большие объёмы, планировщик сам выберет Hash Join и индекс не тронет — это нормально и не признак ошибки.
-- под FK почти всегда нужен индекс вручнуюCREATE INDEX order_items_order_id_idx ON order_items (order_id);Какие индексы знаешь в pg?
Заголовок раздела «Какие индексы знаешь в pg?»Коротко. См. выше: B-tree, Hash, GiST, SP-GiST, GIN, BRIN, плюс расширения (bloom, rum, pgvector). B-tree — дефолт и покрывает 90 % задач.
Что такое составной индекс и для чего нужен?
Заголовок раздела «Что такое составной индекс и для чего нужен?»Коротко. Составной (многоколоночный, composite) индекс строится по нескольким колонкам и упорядочен по ним лексикографически. Нужен, чтобы одним спуском обслужить запрос с условиями сразу по нескольким колонкам, а также чтобы одновременно закрыть фильтр и сортировку — например, (user_id, created_at DESC) для «последние 20 заказов пользователя».
Глубже. Составной индекс почти всегда лучше двух отдельных, если запросы стабильно фильтруют по обеим колонкам: два отдельных индекса потребуют BitmapAnd — два скана и объединение битовых карт, что дороже одного диапазонного скана. Обратное тоже верно: если запросы по колонкам независимы, два одноколоночных индекса гибче. Ограничения PostgreSQL: до 32 колонок в индексе, размер записи B-tree ограничен примерно 2704 байтами (треть страницы). И помните про правило левого префикса — индекс (a, b, c) работает для (a), (a, b), (a, b, c), но для условия только по b PostgreSQL максимум сделает полный скан индекса, если тот заметно уже таблицы.
What type of index does Postgres use - B-tree or hash?
Заголовок раздела «What type of index does Postgres use - B-tree or hash?»Коротко. Both are supported, but B-tree is the default: CREATE INDEX without USING builds a B-tree, and PRIMARY KEY/UNIQUE constraints are always backed by B-tree. Hash indexes exist (USING hash) and have been crash-safe and WAL-logged since PostgreSQL 10, but they are a niche choice — equality only.
Глубже. Практический ответ на английском собеседовании: «Postgres defaults to B-tree; hash is available but rarely worth it because B-tree already does equality well and additionally supports ranges, ordering, uniqueness and multi-column keys. Hash pays off only for long keys where the B-tree entry would be large.» Стоит упомянуть, что в PostgreSQL нет кластерных индексов вообще — команда CLUSTER разово физически переупорядочивает таблицу, но порядок не поддерживается автоматически.
Standard database questions: indexes, what types of indexes do you know, EXPLAIN ANALYZE, isolation levels;
Заголовок раздела «Standard database questions: indexes, what types of indexes do you know, EXPLAIN ANALYZE, isolation levels;»Коротко. Это не один вопрос, а обозначение блока. Ожидаемый ответ по частям: типы индексов — B-tree/Hash/GiST/SP-GiST/GIN/BRIN, плюс свойства (уникальный, составной, частичный, покрывающий, по выражению); EXPLAIN ANALYZE — реальный план с фактическим временем и числом строк, читаем снизу вверх, ищем Seq Scan с большим Rows Removed by Filter и расхождение оценки с фактом; уровни изоляции — Read Committed (по умолчанию), Repeatable Read (snapshot isolation), Serializable (SSI).
Глубже. Как построить рассказ на 3–5 минут: (1) что такое индекс и модель heap + вторичные индексы; (2) B-tree и почему высота 3–4; (3) остальные типы одной фразой каждый — под какие операторы; (4) как проверяю — EXPLAIN (ANALYZE, BUFFERS), ключевые узлы; (5) цена — запись, место, bloat, CREATE INDEX CONCURRENTLY. Про изоляцию отдельно — см. подтему transactions.
есть ли разница для составного индекса от запросов a = A and b = B и b = B and a = A ?
Заголовок раздела «есть ли разница для составного индекса от запросов a = A and b = B и b = B and a = A ?»Коротко. Нет. Порядок конъюнктов в WHERE не влияет ни на план, ни на возможность использовать индекс: планировщик нормализует условия и сам сопоставляет их с колонками индекса. Разница есть только в порядке колонок при создании индекса.
Глубже. Проверяется тривиально: два EXPLAIN дадут идентичный Index Cond, причём выведен он будет в порядке колонок индекса, а не в порядке написания в запросе. Это отличается от, например, порядка JOIN в старых версиях MySQL или от join_collapse_limit в PostgreSQL, где при большом числе таблиц (по умолчанию > 8) планировщик перестаёт перебирать все порядки соединений и порядок написания уже начинает влиять. Но для конъюнкции предикатов в WHERE — не влияет никогда.
индекс на (b, a, c) или (b, c, a) будет работать быстрее и почему?
Заголовок раздела «индекс на (b, a, c) или (b, c, a) будет работать быстрее и почему?»Коротко. Зависит от запросов — сам по себе ни один не «быстрее». Оба одинаково работают для условий на b и на (b, ...) по первой колонке. Если запрос фильтрует b = ? AND a = ?, выигрывает (b, a, c); если b = ? AND c = ? — (b, c, a). Если по всем трём стоит равенство, разница минимальна, и выбирать стоит по второму критерию: колонку с диапазоном или сортировкой ставить после равенств.
Глубже. Разбор по случаям для индекса (b, a, c):
WHERE b = 1 AND a = 2 AND c = 3— сужение по всем трём, идеальный вариант.WHERE b = 1 AND c = 3— по индексу пройдёт диапазон всех записей сb = 1,cпроверится фильтром внутри индекса (в PostgreSQL это всё равно попадёт вIndex Cond, но не сузит границы спуска);(b, c, a)здесь заметно лучше.WHERE b = 1 AND a > 10—(b, a, c)даёт непрерывный диапазон,(b, c, a)— нет.WHERE b = 1 ORDER BY a—(b, a, c)отдаёт готовый порядок без сортировки,(b, c, a)потребует Sort.
Второй фактор — размер: колонки в индексе стоит располагать так, чтобы чаще использовать один индекс вместо двух; третий — селективность: при прочих равных первой ставят более селективную колонку, но это правило слабее правила «под запросы».
Какие индексы использовал?
Заголовок раздела «Какие индексы использовал?»Коротко. Вопрос про личный опыт. Отвечать надо конкретно: назвать 3–4 типа, для каждого — задачу и результат. Например: B-tree составной (tenant_id, created_at DESC) под ленту заказов, уникальный по выражению lower(email) под регистронезависимую уникальность, GIN + pg_trgm под поиск по подстроке в имени, BRIN по ts на таблице метрик в сотни миллионов строк, частичный индекс WHERE status = 'pending' под воркер очереди.
Глубже. Что хочет услышать интервьюер: (1) что вы принимали решение по данным, а не по наитию — «увидел Seq Scan в pg_stat_statements по топ-запросу, посмотрел EXPLAIN ANALYZE, добавил индекс, время упало с 2,3 с до 12 мс»; (2) что вы знаете цену — «проверил рост объёма и деградацию вставки»; (3) что вы умеете выкатывать безопасно — CREATE INDEX CONCURRENTLY, миграции вне транзакции; (4) что вы умеете удалять лишнее — «нашёл 6 индексов с idx_scan = 0 за квартал, снёс, вставка ускорилась». Типичная ошибка — общие слова «использовал B-tree» без единой цифры и без истории решения.
Question about indexes
Заголовок раздела «Question about indexes»Коротко. Обрывок исходника, конкретный вопрос не восстанавливается — это метка «был вопрос про индексы». Универсальный каркас ответа: определение → зачем → типы → как проверяю использование → чем плачу.
What does B in B-Tree index stand for
Заголовок раздела «What does B in B-Tree index stand for»Коротко. Официально это не раскрыто. Рудольф Байер, один из авторов структуры (Bayer & McCreight, 1972), говорил, что расшифровка намеренно оставлена открытой; чаще всего подразумевают «balanced», иногда «broad», «bushy», «block» или фамилию Bayer/компанию Boeing. Что точно неверно — «binary»: B-tree не бинарное дерево.
Глубже. На собеседовании достаточно ответить «не binary, вероятнее всего balanced или по фамилии автора — авторы официально не расшифровывали». Полезно сразу добавить, что в СУБД используется не классическое B-tree, а B+-tree: все данные в листьях, внутренние узлы содержат только ключи-разделители, листья связаны в двусвязный список для диапазонных сканов в обе стороны. PostgreSQL реализует вариант Lehman & Yao с high-key, позволяющий читать дерево без блокировки всего пути.
Standard database questions (indexes, EXPLAIN ANALYZE, etc.).
Заголовок раздела «Standard database questions (indexes, EXPLAIN ANALYZE, etc.).»Коротко. Повтор блока выше. Ключевое про EXPLAIN ANALYZE: он реально выполняет запрос и показывает actual time, actual rows, loops; читать план стоит от самых глубоких узлов, сравнивая оценку rows= с фактом — расхождение в разы указывает на устаревшую или недостаточную статистику.
Глубже. Полезные опции: BUFFERS (страниц прочитано из кеша/с диска — лучший индикатор реальной работы, чем время), VERBOSE (список колонок), SETTINGS (изменённые GUC), WAL (для DML), FORMAT JSON для автоматического разбора. Для DML безопасно оборачивать в BEGIN; EXPLAIN ANALYZE ...; ROLLBACK;. Помните про loops: указанное actual time — это время одной итерации, полное время узла = actual time × loops.
Что такое индексы в бд?
Заголовок раздела «Что такое индексы в бд?»Коротко. См. выше: вспомогательная структура данных, отображающая значения ключа в адреса строк и позволяющая находить данные без полного сканирования таблицы. Формально — не часть логической модели: удаление индекса не меняет результатов запросов, только их скорость.
Плюсы и минусы индексов?
Заголовок раздела «Плюсы и минусы индексов?»Коротко. Плюсы: быстрый поиск по значению и диапазону, готовый порядок для ORDER BY/GROUP BY, ускорение JOIN, дешёвые MIN/MAX, поддержка уникальности и EXCLUDE, возможность Index Only Scan без чтения таблицы. Минусы: замедление INSERT/UPDATE/DELETE, дополнительное место (нередко индексы суммарно больше самой таблицы), раздувание и необходимость VACUUM/REINDEX, конкуренция за буферный кеш, замедление массовой загрузки, риск плохого плана при кривой статистике.
Глубже. Численно: на OLTP-таблице каждый лишний индекс — это примерно +5–15 % к стоимости вставки и заметная нагрузка на WAL, потому что запись в индекс тоже журналируется. При bulk-load стандартная практика — удалить индексы, загрузить, создать заново: это на порядок быстрее, чем поддерживать их построчно. Ещё один неочевидный минус — индекс на часто обновляемой колонке блокирует HOT-обновления: если изменённая колонка не входит ни в один индекс и в странице есть свободное место, PostgreSQL делает HOT-update и вообще не трогает индексы; стоит проиндексировать эту колонку — и каждый апдейт начинает править все индексы строки.
Что такое составные индексы?
Заголовок раздела «Что такое составные индексы?»Коротко. См. выше «Что такое составной индекс и для чего нужен?» — индекс по нескольким колонкам, упорядоченный лексикографически, работающий по правилу левого префикса. Отличие постановки только в множественном числе: обычно дальше спрашивают про порядок колонок и про то, какие запросы такой индекс закроет.
Рассказать подробнее про b-tree;
Заголовок раздела «Рассказать подробнее про b-tree;»Коротко. B+-tree: сбалансированное дерево, где узел равен странице (8 КБ в PostgreSQL), внутренние узлы хранят только ключи-разделители и указатели на потомков, все данные (ключ + TID) лежат в листьях, листья связаны в двусвязный список. Все листья на одной глубине; высота растёт только при расщеплении корня, поэтому дерево сбалансировано по построению.
Глубже. Что происходит при вставке: спускаемся к нужному листу, вставляем запись; если лист переполнен — расщепляем пополам, средний ключ поднимаем в родителя; если переполнился родитель — рекурсивно вверх; переполнение корня увеличивает высоту на единицу. Для монотонно растущих ключей (bigserial, now()) PostgreSQL применяет оптимизацию rightmost-page split — режет страницу не 50/50, а почти целиком оставляя место справа, чтобы индекс не занимал вдвое больше нужного. С PostgreSQL 12 внутренние ключи усечены до значимого префикса (suffix truncation), что повышает ветвление; с PostgreSQL 13 в листьях работает дедупликация — повторяющиеся значения хранятся одной записью со списком TID, что резко сжимает индексы по низкоселективным колонкам; с PostgreSQL 14 добавлено bottom-up index deletion, вычищающее мусор от повторных обновлений до расщепления страницы. Конкурентность обеспечивается протоколом Lehman & Yao: у каждой страницы есть high key и указатель вправо, так что читатель, попавший на расщеплённую страницу, просто идёт вправо, а не берёт блокировку всего пути.
Есть таблица: phone, fio, sex, birthday. Ключ это телефон, он же логин. Какие индексы на какие поля применил бы?
Заголовок раздела «Есть таблица: phone, fio, sex, birthday. Ключ это телефон, он же логин. Какие индексы на какие поля применил бы?»Коротко. phone — первичный ключ, уникальный B-tree создаётся автоматически, ничего добавлять не надо. fio — индекс нужен, если по нему ищут: B-tree по lower(fio) для точного/префиксного поиска либо GIN + pg_trgm для поиска по подстроке. sex — отдельный индекс бесполезен (две-три градации). birthday — B-tree, если есть диапазонные запросы или сортировка по дате рождения; для поиска «у кого сегодня день рождения» — индекс по выражению.
Глубже. Разбор мотивации, которую хочет услышать интервьюер:
-- PK уже даёт уникальный индекс на phoneCREATE TABLE users ( phone text PRIMARY KEY, fio text NOT NULL, sex char(1), birthday date);
-- поиск по подстроке ФИОCREATE EXTENSION IF NOT EXISTS pg_trgm;CREATE INDEX users_fio_trgm ON users USING gin (fio gin_trgm_ops);
-- «дни рождения сегодня» — индекс по выражениюCREATE INDEX users_bday_md ON users ( (extract(month from birthday)), (extract(day from birthday)));Про sex полезно сказать не просто «не индексирую», а объяснить почему: селективность ~50 %, планировщик всё равно выберет Seq Scan. Исключение — сильный перекос распределения плюс частичный индекс, либо sex как вторая колонка составного индекса, если он реально сужает выборку внутри группы. Ещё стоит упомянуть, что phone как логин желательно хранить в нормализованном виде (E.164), иначе уникальность будет фиктивной.
Какой тип индекса может быть первичным ключом?
Заголовок раздела «Какой тип индекса может быть первичным ключом?»Коротко. В PostgreSQL ограничение PRIMARY KEY (как и UNIQUE) всегда реализуется уникальным B-tree индексом — другие методы доступа для этого не поддерживаются, потому что уникальность реализована только в B-tree. Hash-, GIN-, GiST-, BRIN-индексы первичным ключом быть не могут.
Глубже. Формально ограничение и индекс — разные объекты: PRIMARY KEY создаёт индекс неявно, и его нельзя удалить командой DROP INDEX — только ALTER TABLE ... DROP CONSTRAINT. Есть трюк для выкатки без блокировок: построить CREATE UNIQUE INDEX CONCURRENTLY, а затем ALTER TABLE ... ADD CONSTRAINT ... PRIMARY KEY USING INDEX idx_name. В GiST уникальности нет, но есть более общий механизм EXCLUDE USING gist (...) — «никакие две строки не должны пересекаться», что для диапазонов сильнее уникальности. В MySQL/InnoDB ответ иной: первичный ключ — это кластерный B-tree, по которому физически хранится вся таблица.
Что можешь рассказать про индексы?
Заголовок раздела «Что можешь рассказать про индексы?»Коротко. Открытый вопрос — ответ строится по каркасу: что это (структура «ключ → адрес строки»), зачем (поиск за O(log N), уникальность, порядок, соединения), какие бывают (B-tree и остальные под конкретные операторы), как устроен B-tree (ветвление в сотни, высота 3–4, листья в списке), какие свойства (составной, частичный, покрывающий, по выражению, уникальный), как проверить пользу (EXPLAIN (ANALYZE, BUFFERS), pg_stat_user_indexes), чем платим (запись, место, bloat).
Глубже. Хорошая структура рассказа занимает 3–4 минуты и заканчивается практикой: «в проде создаю только CONCURRENTLY, регулярно смотрю неиспользуемые индексы и дубли, при массовой загрузке дропаю и пересоздаю». Плохая — перечисление типов индексов без единого слова о том, когда индекс не сработает.
Что такое индексыи для чего они нужны?
Заголовок раздела «Что такое индексыи для чего они нужны?»Коротко. Опечатка в вопросе, смысл прежний: см. «Что такое индексы и для чего они нужны?». Индекс — вспомогательная структура для быстрого поиска строк по значению; нужен для ускорения выборок, сортировок, соединений и поддержки ограничений уникальности.
Что такое индексы в БД и какие типы бывают?
Заголовок раздела «Что такое индексы в БД и какие типы бывают?»Коротко. Определение см. выше. Типы полезно назвать в двух измерениях: по методу доступа — B-tree, Hash, GiST, SP-GiST, GIN, BRIN; по свойствам — уникальный/неуникальный, одно-/многоколоночный, частичный, по выражению, покрывающий (INCLUDE), кластерный/некластерный (в PostgreSQL кластерных нет, есть разовая команда CLUSTER).
Глубже. Разделение на «метод доступа» и «свойство» — как раз то, что отличает уверенный ответ от заученного списка. Кандидаты часто перечисляют в одном ряду «B-tree, hash, уникальный, кластерный, покрывающий», и это сразу видно. Правильно: «метод доступа определяет структуру и набор поддерживаемых операторов; уникальность, частичность, покрывающие колонки — это ортогональные свойства, применимые в основном к B-tree».
Что такое B-tree индекс?
Заголовок раздела «Что такое B-tree индекс?»Коротко. Индекс на базе сбалансированного сильно ветвящегося дерева (в СУБД — B+-tree): ключи отсортированы, все данные в листьях, листья связаны в список, глубина одинакова для всех листьев. Поддерживает =, <, >, BETWEEN, IN, IS NULL, префиксный LIKE, сортировку и уникальность. Метод доступа по умолчанию в PostgreSQL.
Чему равно высота дерева в B-tree-индексе?
Заголовок раздела «Чему равно высота дерева в B-tree-индексе?»Коротко. O(log_b N), где b — ветвление (число ключей в узле). На практике при странице 8 КБ и ветвлении в сотни высота почти всегда 3–5 уровней: 3 уровня хватает на миллионы строк, 4 — на миллиарды.
Глубже. Прикидка: 8 КБ страница, запись индекса для bigint ≈ 16–24 байта с накладными → ~350–500 ключей в узле. При ветвлении 300: высота 2 → 9·10⁴ записей, 3 → 2,7·10⁷, 4 → 8·10⁹. Реальную высоту в PostgreSQL можно посмотреть расширением pageinspect:
CREATE EXTENSION IF NOT EXISTS pageinspect;SELECT level, index_size, root_block_no FROM bt_metap('orders_pkey');Здесь level — уровень корня, то есть высота минус лист. Практический вывод для собеседования: даже на очень больших таблицах разница между «миллион строк» и «миллиард строк» — это плюс один-два похода к странице, поэтому индексный поиск масштабируется отлично, а вот количество случайных походов в кучу — нет.
Можно ли искать по концу строки в B-tree?
Заголовок раздела «Можно ли искать по концу строки в B-tree?»Коротко. Нет, обычный B-tree по колонке col не помогает при col LIKE '%abc' — индекс упорядочен по началу строки, поэтому суффикс не даёт диапазона. Решения: индекс по выражению reverse(col) и поиск reverse(col) LIKE reverse('%abc'), то есть LIKE 'cba%'; либо GIN/GiST с pg_trgm, который умеет и суффиксы, и подстроки.
Глубже.
-- вариант 1: реверс + префиксный поиск (нужен text_pattern_ops при не-C коллации)CREATE INDEX t_col_rev ON t (reverse(col) text_pattern_ops);SELECT * FROM t WHERE reverse(col) LIKE reverse('%@gmail.com');
-- вариант 2: триграммы, работают для '%abc', '%abc%' и ILIKECREATE EXTENSION IF NOT EXISTS pg_trgm;CREATE INDEX t_col_trgm ON t USING gin (col gin_trgm_ops);SELECT * FROM t WHERE col LIKE '%abc';Нюанс: pg_trgm эффективен от 3 символов — на более коротких паттернах триграмм не получается и индекс не поможет. И text_pattern_ops нужен потому, что при не-C коллации порядок сравнения строк не совпадает с побайтовым, и обычный B-tree не годится для LIKE-префиксов.
Какие минусы хэширования?
Заголовок раздела «Какие минусы хэширования?»Коротко. Хеш теряет порядок: нет диапазонов, нет сортировки, нет префиксного поиска, нет MIN/MAX. Плюс коллизии (нужен доступ к куче для перепроверки исходного значения), плохая работа при перекошенном распределении, дорогое расширение (rehash/split), в PostgreSQL — отсутствие уникальности, составных ключей и INCLUDE, до версии 10 ещё и отсутствие WAL-логирования.
Глубже. Отдельно стоит различать hash-индекс и hash-таблицу внутри Hash Join: последняя строится в памяти на время запроса и при нехватке work_mem разливается на диск батчами — это видно в плане как Batches: N и Disk Usage, и это частая причина внезапного замедления. Ещё один минус хеширования применительно к шардированию: хеш-распределение ключей ломает локальность — соседние по значению записи оказываются на разных узлах, и диапазонные запросы превращаются в scatter-gather по всем шардам.
Как можно определить, используется ли индекс в запросе?
Заголовок раздела «Как можно определить, используется ли индекс в запросе?»Коротко. См. выше «Как определить, использует ли запрос индекс?»: EXPLAIN (ANALYZE, BUFFERS) и наличие узлов Index Scan / Index Only Scan / Bitmap Index Scan с именем индекса. Для оценки на длинном горизонте — pg_stat_user_indexes.idx_scan и pg_stat_statements для поиска топ-запросов.
Глубже. Отличие от предыдущей формулировки: тут уместно говорить и про «используется ли индекс вообще в системе». Практический скрипт аудита — найти индексы с нулевым idx_scan, дубли (индекс, чей набор колонок — префикс другого) и невалидные индексы после неудачного CREATE INDEX CONCURRENTLY:
SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;Какие индексы в Postgres знаете, с какими есть опыт работы?
Заголовок раздела «Какие индексы в Postgres знаете, с какими есть опыт работы?»Коротко. Знаю B-tree, Hash, GiST, SP-GiST, GIN, BRIN. Реальный опыт стоит называть честно и с задачей: B-tree — везде, GIN — под JSONB и полнотекстовый поиск/pg_trgm, BRIN — на таблицах метрик/логов с временной корреляцией, GiST — под диапазонные типы и EXCLUDE-ограничения (пересечение бронирований), hash — почти не применял, потому что B-tree покрывает те же кейсы.
Глубже. Вопрос с двойным дном: интервьюер проверяет, отличаете ли вы «читал в документации» от «применял». Правильная тактика — по каждому типу, где опыта нет, так и сказать и добавить, при каких условиях бы применили. Пример сильного ответа про GiST: «делали календарь брони, поставили EXCLUDE USING gist (room_id WITH =, period WITH &&) — база сама не даёт пересечься двум броням, не нужны блокировки в приложении».
Что из себя представляет B-tree?
Заголовок раздела «Что из себя представляет B-tree?»Коротко. См. выше «Рассказать подробнее про b-tree». Кратко: многоуровневое сбалансированное дерево, узел = страница диска, ветвление в сотни ключей, все листья на одной глубине, данные только в листьях (B+-вариант), листья связаны двусвязным списком для диапазонных сканов.
Нужно составить составной индекс по трем полям. Как будете его составлять, какие метрики использовать для этого?
Заголовок раздела «Нужно составить составной индекс по трем полям. Как будете его составлять, какие метрики использовать для этого?»Коротко. Порядок диктуется не абстрактной селективностью, а формой запросов: сначала колонки со строгим равенством, затем одна колонка с диапазоном, затем колонки для ORDER BY; то, что нужно только в SELECT, — в INCLUDE. Среди равенств первой ставлю более селективную. Метрики: n_distinct и most_common_freqs из pg_stats, доля возвращаемых строк, correlation, а проверка — EXPLAIN (ANALYZE, BUFFERS) до и после.
Глубже. Рабочий алгоритм:
- Собрать топ-запросы по этой таблице (
pg_stat_statements, поляtotal_exec_time,calls). - Выписать для каждого: предикаты равенства, предикаты диапазона,
ORDER BY, список колонок вSELECT. - Построить индекс
(eq1, eq2, range) INCLUDE (selected); проверить, что он закрывает максимум запросов, а не один. - Оценить селективность:
SELECT attname, n_distinct, null_frac, correlation, most_common_valsFROM pg_stats WHERE tablename = 'orders';- Проверить факт:
EXPLAIN (ANALYZE, BUFFERS)— должен появитьсяIndex Only ScanилиIndex Scanс малымRows Removed by Filter, аHeap Fetches— близко к нулю. - Проверить цену: размер индекса
pg_relation_size, деградацию вставки.
Отдельно: n_distinct бывает отрицательным — это доля уникальных значений от числа строк (-1 = все уникальны). И если колонки коррелируют между собой (город и страна), планировщик перемножает селективности и сильно ошибается — лечится расширенной статистикой CREATE STATISTICS ... (dependencies, ndistinct) ON ....
Что такое селективность?
Заголовок раздела «Что такое селективность?»Коротко. Селективность предиката — доля строк таблицы, которую он оставляет: selectivity = returned_rows / total_rows. Кардинальность колонки — число различных значений. Высокая селективность (малая доля, например 0,1 %) означает, что индекс выгоден; низкая (десятки процентов) — что планировщик предпочтёт Seq Scan.
Глубже. Осторожно с терминологией: в литературе «высокая селективность» иногда означает «много различных значений» (то есть малая доля выборки), а иногда обратное — лучше на собеседовании сразу проговорить, что имеете в виду «доля возвращаемых строк мала». Планировщик PostgreSQL считает селективность по статистике из pg_statistic: гистограмма (histogram_bounds, по умолчанию 100 бакетов, настраивается default_statistics_target), список самых частых значений (most_common_vals/most_common_freqs), n_distinct, null_frac. Именно поэтому после массовой загрузки обязателен ANALYZE: без свежей статистики оценки будут случайными и планы — плохими.
Что такое покрывающий индекс?
Заголовок раздела «Что такое покрывающий индекс?»Коротко. Покрывающий (covering) индекс содержит все колонки, нужные запросу, поэтому база может ответить прямо из индекса, не заглядывая в таблицу — это Index Only Scan. В PostgreSQL с версии 11 есть INCLUDE: колонки в нём хранятся только в листьях, не участвуют в сортировке и в правиле левого префикса, зато не раздувают внутренние узлы.
Глубже. В PostgreSQL Index Only Scan не бесплатен: индекс не хранит информацию о видимости версий строк, поэтому база сверяется с картой видимости (visibility map). Если страница не помечена как all-visible, придётся всё-таки сходить в кучу — в плане это видно как Heap Fetches: N. Отсюда практика: Index Only Scan хорошо работает на таблицах, которые регулярно вакуумируются; после массового обновления Heap Fetches взлетает и выигрыш исчезает до следующего VACUUM.
CREATE INDEX orders_cover ON orders (user_id, created_at) INCLUDE (status, total);-- EXPLAIN покажет: Index Only Scan using orders_cover ... Heap Fetches: 0В чем минусы большого количества индексов?
Заголовок раздела «В чем минусы большого количества индексов?»Коротко. Каждый индекс — это дополнительная запись при каждом INSERT/DELETE и при UPDATE соответствующих колонок, дополнительный объём WAL, дополнительное место на диске и в кеше, дополнительная работа для VACUUM и ANALYZE, дольше идут REINDEX и восстановление из дампа, а планировщику дороже перебирать варианты.
Глубже. Менее очевидные эффекты. Первый — вытеснение из shared_buffers: индексы, которые никто не читает, всё равно попадают в кеш при обновлении и вытесняют горячие данные. Второй — блокировка HOT-обновлений: чем больше колонок покрыто индексами, тем чаще UPDATE вынужден править все индексы строки, а не только heap-страницу. Третий — время VACUUM: он проходит по каждому индексу, поэтому 15 индексов на большой таблице превращают автовакуум в многочасовую задачу и вызывают отставание. Четвёртый — дубли: индекс (a) полностью покрывается индексом (a, b), и первый обычно можно удалить. Практика: регулярно смотреть pg_stat_user_indexes и удалять индексы с нулевым idx_scan (аккуратно — уникальные индексы могут не сканироваться, но обеспечивать ограничение).
Какая сложность чтения по индексу добавления?
Заголовок раздела «Какая сложность чтения по индексу добавления?»Коротко. Формулировка искажена; по смыслу — сложность чтения и добавления. Для B-tree и то и другое O(log N): поиск — спуск по дереву, вставка — тот же спуск плюс запись в лист и амортизированно редкое расщепление. Для hash-индекса чтение и вставка — O(1) в среднем. Диапазонное чтение — O(log N + k), где k — число возвращённых записей.
Глубже. Асимптотика в СУБД вторична по отношению к числу обращений к страницам. Реальное чтение по индексу — это высота дерева обращений плюс одно случайное обращение в кучу за строкой; при выборке k строк — k случайных обращений, если только не Index Only Scan или Bitmap Heap Scan. Реальная вставка — это правка листовой страницы каждого индекса плюс WAL-запись; при расщеплении — правка двух-трёх страниц и путь вверх. Именно из-за «плюс один поход в кучу за каждую строку» индекс с плохой селективностью может быть медленнее полного скана, несмотря на O(log N).
Какими индексами пользуетесь в PostgreSQL?
Заголовок раздела «Какими индексами пользуетесь в PostgreSQL?»Коротко. См. выше «Какие индексы в Postgres знаете, с какими есть опыт работы?». В подавляющем большинстве задач — B-tree (обычный, уникальный, составной, частичный, по выражению, с INCLUDE); GIN для JSONB/поиска, BRIN для временных рядов, GiST для диапазонов и геометрии.
В каких случаях нужно применять B-tree, а в каких хэш?
Заголовок раздела «В каких случаях нужно применять B-tree, а в каких хэш?»Коротко. B-tree — по умолчанию всегда: если нужны диапазоны, сортировка, уникальность, составной ключ, префиксный LIKE, MIN/MAX, покрывающий индекс. Hash — узкий случай: только строгое равенство, одна колонка, длинные значения (URL, длинные токены, большие текстовые ключи), когда важен размер индекса и не нужна уникальность.
Глубже. Честный практический вывод: в PostgreSQL hash применяют редко. Даже для длинных ключей часто удобнее B-tree по выражению md5(url) или hashtext(url) — он даёт уникальность и работает как обычный индекс. Если всё же выбираете hash, помните: с PostgreSQL 10 он WAL-логируется и реплицируется, до 10 — нет. И бенчмаркьте: разница на реальных нагрузках обычно в пределах шума, потому что доминирует поход в кучу.
Что такое покрывающие индексы?
Заголовок раздела «Что такое покрывающие индексы?»Коротко. См. выше «Что такое покрывающий индекс?» — индекс, содержащий все колонки запроса, что позволяет обойтись Index Only Scan. Во множественном числе обычно подразумевают технику в целом: добавлять в индекс через INCLUDE колонки, которые нужны только для чтения, чтобы убрать походы в кучу.
Глубже. Разница между (a, b, c) и (a, b) INCLUDE (c): обе конструкции покрывают запрос SELECT c WHERE a = ? AND b = ?, но во втором варианте c не участвует в сортировке индекса и не хранится во внутренних узлах — индекс компактнее и его можно сделать UNIQUE по (a, b). Первый вариант дополнительно позволяет ORDER BY c и фильтр по c внутри индекса. В MySQL/InnoDB INCLUDE нет, но вторичный индекс автоматически «включает» колонки первичного ключа, поэтому покрывающим он становится чаще.
Есть таблица: Люди. Поля: Фамилия, Имя, Отчество. Создаем индекс по 2м полям (фамилия, имя), какие запросы смогут использовать этот индекс?
Заголовок раздела «Есть таблица: Люди. Поля: Фамилия, Имя, Отчество. Создаем индекс по 2м полям (фамилия, имя), какие запросы смогут использовать этот индекс?»Коротко. Индекс (фамилия, имя) полноценно используют запросы с условием на фамилию: WHERE фамилия = 'Иванов', WHERE фамилия = 'Иванов' AND имя = 'Пётр', WHERE фамилия LIKE 'Ива%' (при подходящем opclass), WHERE фамилия IN (...), а также ORDER BY фамилия, имя и WHERE фамилия = 'Иванов' ORDER BY имя. Запрос только по имени (WHERE имя = 'Пётр') левый префикс не задействует.
Глубже. Разбор по случаям:
WHERE фамилия = 'Иванов' AND имя = 'Пётр'— точный диапазон по обеим колонкам, лучший случай.WHERE фамилия = 'Иванов'— диапазон по первой колонке, всё ещё отлично.WHERE фамилия = 'Иванов' AND отчество = 'Сергеевич'— по индексу сузим до фамилии, отчество проверится фильтром по строкам из кучи.WHERE имя = 'Пётр'— левого префикса нет. Теоретически возможен Index Only Scan по всему индексу (полный проход индекса дешевле полного прохода таблицы, если индекс сильно уже), но это не «использование индекса» в смысле поиска; на практике будет Seq Scan.WHERE фамилия > 'И' AND имя = 'Пётр'— диапазон по первой колонке обрывает точность:имяне сузит спуск.ORDER BY фамилия, имя LIMIT 100— индекс отдаст готовый порядок без сортировки;ORDER BY имя— нет.
Если нужны все три поля и поиск по любому — практичнее один GIN-индекс по конкатенации с pg_trgm, либо полнотекстовый индекс.
БД: что такое индекс, зачем, какие бывают, как понять что запрос плохо работает, explain analyze, seqscan и т. п.;
Заголовок раздела «БД: что такое индекс, зачем, какие бывают, как понять что запрос плохо работает, explain analyze, seqscan и т. п.;»Коротко. Комплексный блок. Индекс — структура «ключ → адрес строки» для поиска без полного перебора; бывают B-tree/Hash/GiST/SP-GiST/GIN/BRIN. Плохо работающий запрос ищем через pg_stat_statements (топ по total_exec_time и mean_exec_time), диагностируем через EXPLAIN (ANALYZE, BUFFERS); тревожные признаки — Seq Scan с большим Rows Removed by Filter, расхождение оценки и факта в разы, Sort Method: external merge Disk, Nested Loop с большим числом loops, Bitmap Heap Scan с lossy блоками.
Глубже. Важно уметь сказать, что Seq Scan сам по себе не ошибка: на маленькой таблице или при выборке большой доли строк это правильный выбор. Ошибка — Seq Scan там, где выборка возвращает единицы строк из миллионов. Порядок действий: убедиться, что статистика свежая (ANALYZE), проверить, sargable ли предикат, посмотреть, есть ли подходящий индекс и почему он не выбран (можно временно SET enable_seqscan = off и сравнить фактическое время), проверить random_page_cost (на SSD 4.0 — почти всегда завышено), только потом создавать индекс.
Что такое индекс? Сколько и какие индексы будут созданы по этой схеме?
Заголовок раздела «Что такое индекс? Сколько и какие индексы будут созданы по этой схеме?»Коротко. Определение см. выше. Схема в задании не приведена, поэтому отвечаю правилом: в PostgreSQL индексы создаются автоматически под PRIMARY KEY и UNIQUE (по одному уникальному B-tree на каждое ограничение, включая составные) и под EXCLUDE (GiST). Под FOREIGN KEY индекс НЕ создаётся — его нужно добавлять руками. CHECK, NOT NULL, DEFAULT индексов не порождают.
Глубже. Типичная схема-ловушка на собеседовании:
CREATE TABLE order_items ( id bigserial PRIMARY KEY, -- 1 индекс (уникальный B-tree) order_id bigint NOT NULL REFERENCES orders, -- 0 индексов! sku text NOT NULL, UNIQUE (order_id, sku) -- 2-й индекс, составной уникальный);Ответ: два индекса — order_items_pkey и order_items_order_id_sku_key. Внешний ключ индекса не даёт, но уникальный (order_id, sku) по левому префиксу закрывает поиск по order_id, поэтому отдельный индекс под FK здесь не нужен — это как раз то рассуждение, которое ждут. В MySQL/InnoDB ответ отличался бы: InnoDB создаёт индекс под внешний ключ автоматически, если подходящего нет.
Какой узел плана однозначано показывает, что не хватает индекса?
Заголовок раздела «Какой узел плана однозначано показывает, что не хватает индекса?»Коротко. Seq Scan с Filter и большим Rows Removed by Filter при малом actual rows — то есть база прочитала всю таблицу и выбросила почти всё. Второй яркий признак — Nested Loop, у которого внутренний узел — Seq Scan (или Materialize над Seq Scan) с большим loops.
Глубже. Слово «однозначно» здесь условно: единственного узла, который безошибочно кричит «дай индекс», не существует. Seq Scan может быть корректным выбором. Правильная формулировка на собеседовании: «Смотрю на Seq Scan, у которого Rows Removed by Filter на порядки больше, чем actual rows — это прямое указание на отсутствие подходящего индекса». Дополнительно сигналят: Sort перед Limit (индекс по колонке сортировки убрал бы сортировку), Heap Fetches большое при Index Only Scan (нужен VACUUM), Bitmap Heap Scan с Recheck Cond и lossy=N (не хватает work_mem), Hash Join с Batches > 1.
Что такое btrее индекс? Чем отличается balanced trее от binary tree
Заголовок раздела «Что такое btrее индекс? Чем отличается balanced trее от binary tree»Коротко. B-tree — сбалансированное сильно ветвящееся дерево поиска (см. выше). Binary tree — дерево, где у узла максимум два потомка; оно не обязано быть сбалансированным и в худшем случае вырождается в связный список с O(N). Balanced tree — дерево, у которого высота гарантированно O(log N) за счёт правил перестройки; B-tree сбалансировано по построению (все листья на одном уровне), а среди бинарных балансировку обеспечивают AVL и красно-чёрные деревья.
Глубже. Ключевые различия для СУБД: (1) ветвление — 2 против сотен, отсюда высота 30 против 3–4 для миллиарда записей; (2) размер узла — B-tree узел равен странице диска, бинарный узел — несколько десятков байт, то есть страница используется на 1 %; (3) балансировка — B-tree растёт вверх от корня при расщеплении и не требует ротаций; (4) диапазонные сканы — в B+-tree листья связаны в список, у бинарного дерева нужен обход. Отсюда правило: бинарные деревья — для памяти, B-tree — для блочных носителей.
В чем минус индексов? (Занимают место и надо апдейтить на insert/update/delete - лишнее время).
Заголовок раздела «В чем минус индексов? (Занимают место и надо апдейтить на insert/update/delete - лишнее время).»Коротко. Именно так: место на диске (индексы часто в сумме больше таблицы) и накладные расходы на каждую модификацию данных — при INSERT и DELETE правятся все индексы, при UPDATE — те, чьи колонки изменились (плюс все, если HOT-обновление невозможно). Добавьте к этому WAL-трафик, работу VACUUM, bloat и риск плохого плана из-за устаревшей статистики.
Глубже. Уточнение про UPDATE в PostgreSQL, которое повышает уровень ответа: из-за MVCC любое обновление создаёт новую версию строки. Если ни одна проиндексированная колонка не изменилась и в странице есть место, срабатывает HOT-update и индексы не трогаются вообще — новая версия связывается со старой цепочкой внутри страницы. Если же изменилась проиндексированная колонка или места в странице нет, новая версия попадает в другую страницу и придётся вставить запись во все индексы таблицы, даже в те, чьи колонки не менялись. Отсюда практический совет: не индексировать «горячие» часто обновляемые колонки и следить за fillfactor.
Есть ли смысл в индексе для поля enum из 4-х значений? (Нe особо, ибо выборка по индексу должна отдавать процентов 10-15, иначе выигрыша особо не будет, учитывая чтение индекса по сравнению с чтением всей таблицы).
Заголовок раздела «Есть ли смысл в индексе для поля enum из 4-х значений? (Нe особо, ибо выборка по индексу должна отдавать процентов 10-15, иначе выигрыша особо не будет, учитывая чтение индекса по сравнению с чтением всей таблицы).»Коротко. В общем случае нет: при четырёх равновероятных значениях выборка вернёт ~25 % таблицы, и планировщик выберет Seq Scan — индекс просто не будет использоваться. Смысл появляется в двух случаях: сильный перекос распределения (одно значение — 0,1 % строк) и тогда лучше частичный индекс; либо enum как дополнительная колонка составного индекса.
Глубже. Порог «10–15 %» — разумная эвристика, но не константа: точка перелома зависит от random_page_cost / seq_page_cost, от ширины строки, от того, влезает ли таблица в кеш, и от того, возможен ли Index Only Scan (при нём порог сдвигается вверх, потому что нет походов в кучу). Классический полезный вариант — очередь задач:
-- строк со статусом 'pending' единицы из миллионовCREATE INDEX tasks_pending ON tasks (created_at) WHERE status = 'pending';Такой частичный индекс крошечный, обслуживает самый горячий запрос воркера и почти ничего не стоит на записи. Кстати, с PostgreSQL 13 дедупликация в B-tree сильно уменьшила размер индексов по низкоселективным колонкам, но это не делает их полезнее для планировщика — проблема была не в размере, а в доле выборки.
Что такое селективность индексов? Какие там границы?
Заголовок раздела «Что такое селективность индексов? Какие там границы?»Коротко. Селективность — доля строк, которую отбирает предикат по этому индексу. Практическая граница: если запрос возвращает меньше примерно 1–5 % таблицы, Index Scan уверенно выигрывает; от ~5 до ~20 % обычно побеждает Bitmap Heap Scan; выше ~20–30 % планировщик выбирает Seq Scan. Точных границ в стандарте нет — их вычисляет модель стоимости.
Глубже. Границы плавают в зависимости от: random_page_cost (на SSD снижают до 1.1, и тогда Index Scan выигрывает на большей доле), ширины строки (узкая строка — больше строк на страницу — Seq Scan дешевле), корреляции физического порядка со значением колонки (при correlation ≈ 1 индексный скан читает кучу почти последовательно и выигрывает даже на 50 %), возможности Index Only Scan (тогда доля почти не важна), и наличия LIMIT (с LIMIT 10 индекс выигрывает почти всегда). Проверять надо не по правилу, а по EXPLAIN (ANALYZE, BUFFERS).
Есть запрос SELECT * FROM employее WHERE lastname = :lastname . Запрос тормозит. Почему? Повесили индекс, но с ним запрос то тормозит, то работает нормально. Почему?
Заголовок раздела «Есть запрос SELECT * FROM employее WHERE lastname = :lastname . Запрос тормозит. Почему? Повесили индекс, но с ним запрос то тормозит, то работает нормально. Почему?»Коротко. Изначально тормозил из-за Seq Scan — индекса на lastname не было. После создания индекса поведение стало нестабильным из-за перекоса распределения фамилий: для редкой фамилии индекс возвращает несколько строк и запрос летит, для частой (условный «Иванов», доли процента от всей таблицы, но десятки тысяч строк) — это тысячи случайных походов в кучу, и Seq Scan был бы дешевле. Плюс SELECT * мешает Index Only Scan.
Глубже. Механика в PostgreSQL. Для непараметризованного запроса планировщик каждый раз видит константу и по most_common_vals/most_common_freqs из pg_stats знает, что «Иванов» встречается часто, а «Загогулькин» — редко, и строит разные планы. А вот для подготовленного запроса (PREPARE/серверный prepared statement в драйвере) PostgreSQL после пяти выполнений с кастомными планами может перейти на generic plan — план, построенный без знания конкретного значения, по средней селективности. Тогда для одних значений он окажется удачным, для других — катастрофическим; это и есть плавающая производительность. Что делать:
SET plan_cache_mode = force_custom_plan;(PostgreSQL 12+) для проблемного запроса или на уровне сессии;- увеличить
default_statistics_targetдля колонки, чтобы MCV-список точнее описывал перекос:ALTER TABLE employee ALTER COLUMN lastname SET STATISTICS 1000; ANALYZE employee; - убрать
SELECT *и сделать покрывающий индекс, чтобы даже для частых фамилий не ходить в кучу; - проверить типы и коллацию:
lastnameтипаvarcharи параметр другого типа могут дать неявное приведение, из-за которого индекс не применится; - отдельно исключить банальности: раздутый индекс (нужен
REINDEX CONCURRENTLY), устаревшая статистика, холодный кеш при первом запуске.
Есть запрос SELECT * FROM employее WHERE lastname LIKE '%somename%' . Будет ли использоваться индекс? Как решить проблему поиска, чтобы он был быстрым?
Заголовок раздела «Есть запрос SELECT * FROM employее WHERE lastname LIKE '%somename%' . Будет ли использоваться индекс? Как решить проблему поиска, чтобы он был быстрым?»Коротко. Обычный B-tree не будет использован: паттерн начинается с %, префикса для спуска по дереву нет. Решения: GIN (или GiST) индекс с pg_trgm — он умеет LIKE '%x%' и ILIKE; либо полнотекстовый поиск через tsvector + GIN, если нужен поиск по словам, а не по подстроке; либо внешний поисковый движок при больших объёмах и сложных требованиях.
Глубже.
CREATE EXTENSION IF NOT EXISTS pg_trgm;CREATE INDEX employee_lastname_trgm ON employee USING gin (lastname gin_trgm_ops);-- теперь план: Bitmap Index Scan on employee_lastname_trgm + Recheck CondSELECT * FROM employee WHERE lastname ILIKE '%somename%';Нюансы: pg_trgm работает от трёх символов в паттерне — на '%ab%' триграмм не набирается и индекс не поможет; GIN даёт быстрый поиск, но медленную запись, GiST (gist_trgm_ops) — наоборот, компактнее и дешевле на запись, но медленнее на поиск, зато поддерживает % (similarity) и kNN-сортировку по похожести. Для поиска по словам правильнее полнотекст: CREATE INDEX ON employee USING gin (to_tsvector('russian', lastname)) и запрос через @@ plainto_tsquery(...) — но он ищет словоформы, а не произвольные подстроки. Если требуется только префикс (LIKE 'somename%'), достаточно обычного B-tree с text_pattern_ops при не-C коллации.
В монго индексы есть?
Заголовок раздела «В монго индексы есть?»Коротко. Да. MongoDB строит индексы на B-tree, по _id уникальный индекс создаётся автоматически. Поддерживаются single-field, compound (с правилом левого префикса, как в SQL), multikey (по массивам), text, geospatial (2d, 2dsphere), hashed (для шардирования), wildcard, а также свойства: unique, partial, sparse, TTL, case-insensitive (через collation).
Глубже. Отличия от PostgreSQL, о которых полезно упомянуть. Multikey-индекс по массиву создаёт запись индекса на каждый элемент массива — отсюда и мощь, и раздувание; составной индекс может содержать не более одного массивного поля. TTL-индекс (expireAfterSeconds) — фоновое удаление документов по времени, аналога в PostgreSQL нет. Диагностика — db.coll.find(...).explain("executionStats"), где COLLSCAN соответствует Seq Scan, IXSCAN — Index Scan, FETCH — походу в документ, а отсутствие FETCH означает covered query. Индексы в MongoDB строятся {background: true} по умолчанию начиная с 4.2 (там оптимизированное построение, не блокирующее коллекцию целиком).
Какого типа индексы?
Заголовок раздела «Какого типа индексы?»Коротко. Обрывок, вопрос без контекста; по смыслу — уточнение к предыдущему («в Mongo/в вашем проекте — какого типа?»). Универсальный ответ: в реляционных СУБД по умолчанию B-tree; в MongoDB тоже B-tree (плюс специализированные — text, geo, hashed). Если речь про конкретный проект — назвать реальные: B-tree составные и частичные, GIN под JSONB и trgm-поиск.
Какие плюсы и минусы у индекса b-tree?
Заголовок раздела «Какие плюсы и минусы у индекса b-tree?»Коротко. Плюсы: универсальность (равенство, диапазоны, IN, IS NULL, префиксный LIKE, ORDER BY, MIN/MAX), поддержка уникальности и составных ключей, INCLUDE/Index Only Scan, предсказуемая высота 3–4, сбалансированность по построению, хорошая конкурентность. Минусы: больше по размеру, чем hash/BRIN (хранит полные значения ключей), ограничение на размер записи (~2704 байта), деградация и bloat при интенсивных обновлениях, бесполезен при LIKE '%x%', при функции над колонкой и при низкой селективности, дороже на запись, чем отсутствие индекса.
Глубже. Ещё пара минусов, которые редко называют. Первый — на монотонно растущем ключе (bigserial, now()) все вставки идут в одну крайнюю правую страницу, что при высокой конкурентности даёт contention на этой странице; в распределённых системах это называют hot-spot и лечат UUIDv7/шардированием ключа, хотя в PostgreSQL проблема обычно не критична. Второй — B-tree индекс на очень широкой колонке: значение хранится целиком, поэтому индекс по длинному тексту раздувается; лечится индексом по хешу или по префиксу (left(col, 32)). Из плюсов недооценён Bitmap-режим: два B-tree индекса могут быть скомбинированы через BitmapAnd/BitmapOr, чего hash не умеет.
Мы сделали этот запрос, но нужно поставить индекс на поле duration. Какой именно?
Заголовок раздела «Мы сделали этот запрос, но нужно поставить индекс на поле duration. Какой именно?»Коротко. Сам запрос в задании не приведён, поэтому отвечаю правилом: для duration (числовое/интервальное поле) в подавляющем большинстве случаев нужен обычный B-tree — он закрывает duration > X, BETWEEN, ORDER BY duration, MIN/MAX. Hash не подойдёт (нужны диапазоны). BRIN имеет смысл только если таблица огромная и физический порядок строк коррелирует с duration, что для длительности почти никогда не так.
Глубже. Уточняющие решения, которые стоит проговорить вслух: если фильтр по duration всегда идёт вместе с равенством по другой колонке — делать составной (service_id, duration), а не отдельный; если интересуют только аномалии — частичный индекс WHERE duration > interval '1 second', он будет на порядки компактнее; если запрос сортирует по duration DESC с LIMIT — указать порядок в индексе (duration DESC NULLS LAST), чтобы убрать Sort; если в SELECT пара колонок — добавить их в INCLUDE ради Index Only Scan. И обязательно проверить EXPLAIN (ANALYZE, BUFFERS) до и после — правильный ответ на такой вопрос всегда включает фразу «а дальше смотрю план».
Как btrее обеспечивает эффективную доскачу запросов, например по датам или по числам?
Заголовок раздела «Как btrее обеспечивает эффективную доскачу запросов, например по датам или по числам?»Коротко. Формулировка искажена («доставку/выборку запросов»). Суть: B-tree хранит ключи в отсортированном виде, поэтому диапазонный запрос — это один спуск к левой границе за O(log N) и затем последовательный проход по связанным листьям до правой границы. Ни одного лишнего сравнения: всё, что вне диапазона, вообще не читается.
Глубже. Для WHERE ts >= '2026-01-01' AND ts < '2026-02-01' план будет Index Scan с Index Cond по обеим границам. Листья B+-tree связаны двусвязным списком, поэтому проход возможен в обе стороны — отсюда работает и ORDER BY ts DESC без сортировки, и Backward Index Scan. Для MIN(ts)/MAX(ts) PostgreSQL превращает агрегат в Result + Limit 1 над индексным сканом — это O(log N) вместо прохода по всей таблице. Важный практический момент: скорость диапазонного скана сильно зависит от корреляции — если строки с близкими датами физически лежат рядом (типично для append-only таблиц), походы в кучу почти последовательны; если данные перемешаны, k строк дадут k случайных чтений, и выигрыш растворится. Смотреть pg_stats.correlation, при необходимости — CLUSTER или партиционирование по времени.
Что такое индекс и какие виды индексов существуют?
Заголовок раздела «Что такое индекс и какие виды индексов существуют?»Коротко. См. выше — определение и перечисление: по методу доступа B-tree, Hash, GiST, SP-GiST, GIN, BRIN; по свойствам — уникальный, составной, частичный, по выражению, покрывающий; по отношению к физическому хранению — кластерный (MySQL/InnoDB, MS SQL) и некластерный (все индексы в PostgreSQL).
Глубже. Разделение «кластерный/некластерный» стоит проговорить отдельно, потому что оно определяет производительность целого класса запросов. В InnoDB таблица физически хранится в B-tree по первичному ключу: поиск по PK возвращает строку сразу из листа индекса, зато вторичный индекс хранит значение PK и требует второго спуска по кластерному индексу (bookmark lookup). Отсюда рекомендации InnoDB: короткий монотонный PK, потому что он дублируется во всех вторичных индексах. В PostgreSQL кластерных индексов нет — все индексы ссылаются на TID в куче, поэтому поиск по PK стоит ровно столько же, сколько по любому другому уникальному индексу.
Что такое реиндексация?
Заголовок раздела «Что такое реиндексация?»Коротко. Реиндексация — пересоздание индекса с нуля из текущих данных таблицы, команда REINDEX INDEX | TABLE | SCHEMA | DATABASE. Нужна, когда индекс раздулся (bloat) после массовых обновлений/удалений, повреждён, стал INVALID после неудачного CREATE INDEX CONCURRENTLY, или когда изменилась версия библиотеки коллации (glibc/ICU) и порядок сортировки строк перестал соответствовать индексу.
Глубже. Обычный REINDEX берёт на таблице блокировку, запрещающую запись (а для REINDEX TABLE — и чтение индекса), поэтому в проде используют REINDEX ... CONCURRENTLY, появившийся в PostgreSQL 12: он строит новый индекс параллельно, затем атомарно подменяет старый. Стоит помнить, что REINDEX CONCURRENTLY требует больше места (одновременно живут две копии), медленнее обычного и при сбое оставляет индекс с суффиксом _ccnew, который надо удалить. Отдельный важный кейс — обновление ОС и смена версии glibc: порядок сравнения строк может измениться, и все индексы по текстовым колонкам становятся логически некорректными — их обязательно реиндексировать (PostgreSQL 15+ предупреждает о несовпадении версии коллации). Раздувание можно измерить расширением pgstattuple (pgstatindex) или запросами из pg_bloat_check.
REINDEX INDEX CONCURRENTLY orders_user_created;SELECT * FROM pgstatindex('orders_user_created'); -- avg_leaf_density, leaf_fragmentationКак можно создавать и удалять индексы?
Заголовок раздела «Как можно создавать и удалять индексы?»Коротко. CREATE INDEX [CONCURRENTLY] [IF NOT EXISTS] name ON table [USING method] (cols) [INCLUDE (...)] [WHERE predicate]; и DROP INDEX [CONCURRENTLY] [IF EXISTS] name;. В проде — всегда с CONCURRENTLY: обычный CREATE INDEX блокирует запись в таблицу на всё время построения.
Глубже. Практические правила выкатки:
CREATE INDEX CONCURRENTLY IF NOT EXISTS orders_user_created ON orders (user_id, created_at DESC) INCLUDE (status) WHERE deleted_at IS NULL;
DROP INDEX CONCURRENTLY IF EXISTS orders_old_idx;CONCURRENTLY нельзя выполнять внутри транзакционного блока — значит, миграция должна быть помечена как «вне транзакции» (в goose это -- +goose NO TRANSACTION, в golang-migrate — отдельный файл без обёртки). Построение делает два прохода по таблице и ждёт завершения всех транзакций, начатых до него, поэтому долгая аналитическая транзакция может заблокировать процесс на часы. При неудаче остаётся невалидный индекс — он не используется планировщиком, но продолжает обновляться при записи; найти такие можно через pg_index WHERE NOT indisvalid, лечится DROP INDEX CONCURRENTLY и повторной попыткой. Индексы, созданные ограничением, удаляются только через ALTER TABLE ... DROP CONSTRAINT. Полезно также знать max_parallel_maintenance_workers и maintenance_work_mem — они заметно ускоряют построение больших индексов.
Что такое маппинги, индексы и алиасы?
Заголовок раздела «Что такое маппинги, индексы и алиасы?»Коротко. Эта триада — терминология Elasticsearch/OpenSearch, а не реляционных СУБД: index — именованная коллекция документов (аналог таблицы), mapping — описание схемы индекса (какие поля, каких типов, как анализируются), alias — логическое имя-указатель на один или несколько индексов, через которое можно бесшовно переключать трафик. В SQL-контексте «индекс» — это структура ускорения поиска, а «алиас» — псевдоним таблицы/колонки в запросе (FROM users u, SELECT count(*) AS cnt); понятия «маппинг» в SQL нет, ближайший аналог — DDL-схема таблицы или ORM-маппинг «класс → таблица».
Глубже. Если вопрос задан на секции про поиск — отвечайте про ES. Практическая ценность алиаса там в zero-downtime переиндексации: создаёте orders_v2, льёте данные, затем атомарно перекидываете alias orders с orders_v1 на orders_v2. Маппинг в ES почти неизменяем: добавить новое поле можно, поменять тип существующего — нет, только reindex. Если же вопрос был на SQL-секции, стоит уточнить у интервьюера, что он имеет в виду под маппингом: ORM-маппинг (например, теги db:"..." в sqlx или схема в GORM) или соответствие типов приложения типам БД.
Для чего нужна команда VACUUM в PostgreSQL? Чистит ли она индексы? Что делает VACUUM FULL?
Заголовок раздела «Для чего нужна команда VACUUM в PostgreSQL? Чистит ли она индексы? Что делает VACUUM FULL?»Коротко. VACUUM убирает мёртвые версии строк, оставшиеся после UPDATE/DELETE из-за MVCC, и помечает освободившееся место в страницах как пригодное для повторного использования; попутно он обновляет free space map и visibility map и «замораживает» старые transaction id, защищая от wraparound. Да, индексы он тоже чистит: перед освобождением TID в куче вакуум обязан удалить ссылающиеся на него записи из всех индексов. VACUUM FULL — это другое: он полностью перезаписывает таблицу в новый файл и заново строит все индексы, отдавая место операционной системе, но берёт ACCESS EXCLUSIVE lock, то есть блокирует таблицу целиком.
Глубже. Обычный VACUUM не возвращает место ОС (за исключением пустого «хвоста» файла) и не устраняет раздувание индекса полностью: страницы B-tree, ставшие пустыми, попадают в free space map индекса и переиспользуются, но плотность записей не восстанавливается. От разбухшего индекса лечит REINDEX, а на проде — REINDEX INDEX CONCURRENTLY (появился в PG 12), который не блокирует запись. Стоит помнить про смежные вещи: ANALYZE (отдельно или VACUUM ANALYZE) обновляет статистику для планировщика — сам VACUUM этого не делает; autovacuum запускается по порогам autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor и на больших таблицах со scale_factor 0.2 приходит слишком поздно, поэтому на горячих таблицах его настраивают индивидуально через ALTER TABLE ... SET (autovacuum_vacuum_scale_factor = 0.02). Из свежего: в PG 13 у B-tree появилась дедупликация одинаковых ключей, в PG 14 — bottom-up index deletion, который вычищает «мусорные» версии в индексе до того, как страница расколется, а в PG 17 хранилище списка мёртвых TID переписали на radix-дерево, что сняло старое ограничение в 1 GB на maintenance_work_mem и убрало множественные проходы по индексам.
-- посмотреть, когда таблицу вакуумили и сколько в ней мёртвых строкSELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_autoanalyzeFROM pg_stat_user_tablesORDER BY n_dead_tup DESCLIMIT 10;База данных на сотни миллионов записей жителей России. Менеджер захотел повесить индекс на поле “пол”, у которого градация мужчина/женщина. Ускорит ли это запрос к базе данных?
Заголовок раздела «База данных на сотни миллионов записей жителей России. Менеджер захотел повесить индекс на поле “пол”, у которого градация мужчина/женщина. Ускорит ли это запрос к базе данных?»Коротко. Нет. Селективность такого поля ~0.5: по условию sex = 'M' отбирается половина таблицы, и планировщик справедливо предпочтёт Seq Scan, потому что сходить в индекс, а потом сделать десятки миллионов случайных обращений к куче дороже, чем прочитать таблицу последовательно. Индекс будет занимать место, замедлять вставки и, скорее всего, вообще не использоваться.
Глубже. Когда такой индекс всё же имеет смысл: (1) как не первая колонка составного индекса под конкретный запрос, например (region_id, sex, birth_date); (2) как частичный индекс, если интересует редкое значение — CREATE INDEX ... ON people (id) WHERE sex = 'X'; (3) когда пол сам по себе не фильтр, а нужен для index-only scan в агрегате — CREATE INDEX ON people (sex) позволяет посчитать SELECT sex, count(*) FROM people GROUP BY sex без чтения кучи, хотя всё равно прочитается весь индекс; (4) внутри BitmapAnd с другим, более селективным индексом. Хороший ответ менеджеру: «покажите запрос — индексы строятся под запрос, а не под колонку», а потом EXPLAIN (ANALYZE, BUFFERS) до и после на копии данных.
Тa же таблица, добавил индексы, стало ок, потом прошел месяц и опять проблема. Что делать?
Заголовок раздела «Тa же таблица, добавил индексы, стало ок, потом прошел месяц и опять проблема. Что делать?»Коротко. Сначала диагностика, а не новые индексы: снять EXPLAIN (ANALYZE, BUFFERS) и сравнить план с прежним. Типичные причины деградации за месяц — устаревшая статистика после роста данных, раздувание таблицы и индексов из-за отстающего autovacuum, выход рабочего набора за пределы shared_buffers/page cache, изменение распределения данных (плановая оценка перестала попадать) и просто рост объёма.
Глубже. Практический чек-лист: ANALYZE таблицы и при неравномерном распределении ALTER TABLE ... ALTER COLUMN x SET STATISTICS 1000; проверить pg_stat_user_tables.n_dead_tup и last_autovacuum, при отставании — ужесточить autovacuum для таблицы; оценить раздувание индексов (pg_relation_size в динамике, расширение pgstattuple) и сделать REINDEX CONCURRENTLY; посмотреть pg_stat_user_indexes.idx_scan — не появились ли неиспользуемые индексы, которые только тормозят запись; проверить, не изменился ли сам запрос (новое условие, новая сортировка, OFFSET на большой глубине). Если данные продолжают расти линейно, точечными индексами вопрос не закрыть — дальше идут партиционирование по времени/региону (PARTITION BY RANGE), архивирование холодных данных, покрывающие индексы с INCLUDE, предагрегация в материализованное представление. И отдельно: рост числа версий строк на «горячих» обновляемых записях лечится fillfactor < 100, чтобы работал HOT-update и индексы не трогались вовсе.
Важен ли в составном индексе SQL порядок столбцов и почему?
Заголовок раздела «Важен ли в составном индексе SQL порядок столбцов и почему?»Коротко. Да, критично. Составной индекс отсортирован лексикографически: сначала по первой колонке, внутри равных значений — по второй, и так далее. Поэтому индекс эффективно работает только на левом префиксе ключа: (a, b, c) годится для условий по a, по a, b и по a, b, c, но поиск только по b или c в общем случае превращается в полный проход по индексу или Seq Scan.
Глубже. Правила выбора порядка: колонки, которые всегда встречаются в условии равенства, идут первыми; колонка с диапазоном (>, <, BETWEEN) — последней из «фильтрующих», потому что после диапазонного условия следующие колонки перестают ограничивать поиск и работают только как фильтр; если есть ORDER BY, то хвост индекса должен совпадать с порядком сортировки, чтобы избавиться от узла Sort. Важно не путать порядок колонок в индексе с порядком условий в WHERE: a = 1 AND b = 2 и b = 2 AND a = 1 для планировщика идентичны. Оговорка про современные версии: в PostgreSQL 18 у B-tree появился skip scan, который позволяет использовать индекс (a, b) при поиске только по b, если у a мало различных значений — но полагаться на это как на замену правильному порядку колонок не стоит.
Какие индексы есть в Postgres?
Заголовок раздела «Какие индексы есть в Postgres?»Коротко. Шесть встроенных методов доступа: B-tree (по умолчанию, для сравнений и сортировки), Hash (только равенство), GiST (обобщённое дерево: геометрия, диапазоны, pg_trgm, полнотекст), SP-GiST (несбалансированные разбиения: quad-tree, radix-tree, для точек, IP-адресов, текстовых префиксов), GIN (обратный индекс для составных значений: jsonb, массивы, tsvector, триграммы), BRIN (блочно-диапазонный, крошечный, для больших таблиц с физической корреляцией — обычно по времени вставки).
Глубже. Отдельно от «типа» существуют свойства индекса, и их часто путают с типом: уникальный (UNIQUE), частичный (WHERE ...), по выражению (ON t (lower(email))), покрывающий (INCLUDE (col) — с PG 11), многоколоночный, с указанным класcом операторов (text_pattern_ops, gin_trgm_ops, jsonb_path_ops). Из расширений упоминают bloom (фильтр Блума для произвольной комбинации колонок) и rum (улучшенный GIN для полнотекста с ранжированием). Уместно добавить, что первичный ключ и UNIQUE-ограничение в Postgres реализуются именно уникальным B-tree индексом, а хеш-индекс уникальность поддерживать не умеет.
Почему в реляц базах обычно btree используется, а не hash?
Заголовок раздела «Почему в реляц базах обычно btree используется, а не hash?»Коротко. Потому что B-tree универсален: он покрывает не только равенство, но и <, >, BETWEEN, ORDER BY, MIN/MAX, поиск по префиксу строки и merge join, поддерживает уникальность, многоколоночность и index-only scan. Hash умеет ровно одну операцию — =, а выигрыш даже на ней в реальной СУБД невелик, потому что B-tree высотой 3–4 страницы почти всегда лежит в кеше.
Глубже. Есть и исторические причины: до PostgreSQL 10 hash-индексы не писались в WAL, то есть не переживали крах и не реплицировались — их прямо не рекомендовали в проде, и репутация закрепилась. Дополнительно B-tree хорошо ложится на страничную модель хранения: узел = страница 8 KB, высокий fanout, амортизированные записи при вставках, предсказуемое поведение при росте. Hash-индексу при росте нужен bucket split, поведение по latency более рваное, а на длинных ключах его преимущество (сравнение 4-байтного хеша вместо длинной строки) частично воспроизводится обычным индексом по выражению md5(x) или hashtext(x).
В каких выражениях используется индекс?
Заголовок раздела «В каких выражениях используется индекс?»Коротко. B-tree применяется там, где условие сводится к операторам его класса: =, <, <=, >, >=, BETWEEN, IN (...) (как набор равенств), IS NULL / IS NOT NULL, LIKE 'префикс%' (при C-локали или индексе с text_pattern_ops), а также для ORDER BY, GROUP BY, DISTINCT, MIN/MAX и как вход для merge/nested loop join.
Глубже. Индекс не сработает, если колонка «завёрнута» в функцию или выражение (WHERE lower(email) = $1 при индексе по email, WHERE created_at::date = $1, WHERE id + 1 = $1) — лечится индексом по выражению или переписыванием условия в диапазон; если типы не совпадают так, что приведение идёт по колонке, а не по параметру; при LIKE '%подстрока%' и регулярках — тут нужен GIN/GiST с pg_trgm; при <> и NOT IN, потому что они не селективны; при OR иногда помогает BitmapOr по двум индексам, но чаще выгоднее переписать через UNION ALL. Отдельная ловушка — сортировка: индекс (a ASC) может обслуживать и ORDER BY a DESC (проход в обратную сторону), но для ORDER BY a ASC, b DESC нужен индекс ровно с такой комбинацией направлений, иначе появится Sort.
Какие недостатки есть у hash индекса?
Заголовок раздела «Какие недостатки есть у hash индекса?»Коротко. Он умеет только равенство: ни диапазонов, ни сортировки, ни поиска по префиксу. В PostgreSQL он к тому же не бывает многоколоночным, не поддерживает уникальные ограничения и первичные ключи, не даёт index-only scan (в индексе хранится только хеш, самого значения нет), и его нельзя использовать для сортировки результата.
Глубже. Плюс общая для хеширования проблема коллизий: при неудачном распределении бакеты переполняются, появляются overflow-страницы, и поиск деградирует; расширение индекса требует bucket split, который дороже, чем расщепление страницы B-tree. До PG 10 hash-индексы не журналировались в WAL (не переживали крах, не реплицировались) — сейчас это исправлено, и на очень длинных ключах с чистым равенством hash может выигрывать по размеру и скорости, но случаев, где он объективно нужен, мало. Практический ответ: «беру B-tree по умолчанию, hash — только если профилирование показало выигрыш на конкретном сценарии равенства по длинному ключу».
Что такое индексы в БД? Почему при explain иногда видим что индекс применился, а иногда нет? А как правильно составлять индекс, на что ориентироваться? Что такое селективность, как определить(измерить), что значение обладает высокой селективностью?
Заголовок раздела «Что такое индексы в БД? Почему при explain иногда видим что индекс применился, а иногда нет? А как правильно составлять индекс, на что ориентироваться? Что такое селективность, как определить(измерить), что значение обладает высокой селективностью?»Коротко. Индекс — вспомогательная упорядоченная структура «ключ → ссылка на строку», ускоряющая поиск ценой замедления записи и расхода места. Планировщик берёт индекс, только если по его оценке это дешевле Seq Scan; оценка строится на статистике, поэтому при большой доле отбираемых строк, устаревшей статистике или условии, несовместимом с индексом, план будет последовательным. Индекс составляют под конкретный запрос: сначала колонки с равенством в порядке убывания селективности, затем диапазон, затем колонки для сортировки, при необходимости — INCLUDE для покрытия. Селективность — доля строк, которую отбирает условие; чем она меньше, тем «выше селективность» и тем полезнее индекс.
Глубже. Численно селективность условия равенства оценивают как 1 / n_distinct при равномерном распределении, а фактически смотрят в pg_stats: n_distinct, most_common_vals/most_common_freqs (для частых значений селективность берётся из них напрямую), histogram_bounds для диапазонов, correlation — насколько физический порядок совпадает с логическим (влияет на стоимость Index Scan). Ориентир грубый: если условие отбирает больше ~5–10% таблицы, Index Scan обычно проигрывает Seq Scan; между ними планировщик может выбрать Bitmap Scan. Измерить на практике проще всего запросом:
-- селективность колонки status: сколько строк в среднем на одно значениеSELECT count(DISTINCT status) AS distinct_vals, count(*) AS rows_total, count(DISTINCT status)::numeric / count(*) AS selectivityFROM orders;
-- и что думает планировщикSELECT attname, n_distinct, most_common_vals, correlationFROM pg_stats WHERE tablename = 'orders';Важно понимать разницу «высокая селективность колонки» и «высокая селективность конкретного значения»: в колонке status со значениями new/paid/cancelled в целом селективность низкая, но status = 'cancelled' при доле 0.1% — отличный кандидат на частичный индекс.
Как проверить в базе данных, идет ли запрос с использованием индекса или без использования индекса?
Заголовок раздела «Как проверить в базе данных, идет ли запрос с использованием индекса или без использования индекса?»Коротко. Через EXPLAIN, а лучше EXPLAIN (ANALYZE, BUFFERS) — в плане видно Index Scan / Index Only Scan / Bitmap Index Scan с именем индекса против Seq Scan. ANALYZE реально выполняет запрос и показывает фактические строки и время, BUFFERS — сколько страниц прочитано из кеша и с диска.
Глубже. Дополнительные инструменты: счётчики использования индексов в pg_stat_user_indexes (idx_scan, idx_tup_read, idx_tup_fetch) — так находят мёртвые индексы, которые ни разу не сканировались; pg_stat_statements для поиска самих проблемных запросов; auto_explain с log_min_duration, чтобы ловить планы медленных запросов на проде. Что смотреть в плане: расхождение rows= (оценка) и actual rows= больше чем на порядок — сигнал плохой статистики; Rows Removed by Filter — индекс нашёл лишнее и оно отфильтровалось потом; Heap Fetches в Index Only Scan — если их много, visibility map не обновлена, нужен вакуум; Recheck Cond и lossy=N в Bitmap Heap Scan — карте не хватило work_mem и она огрубилась до страниц. Для проверки гипотезы «индекс бы помог» на сессии можно временно сделать SET enable_seqscan = off; и сравнить стоимости — но это диагностика, а не решение.
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)SELECT id, email FROM users WHERE lower(email) = 'a@b.c';Сталкивался ли с материализованными представлениями в индексе?
Заголовок раздела «Сталкивался ли с материализованными представлениями в индексе?»Коротко. Формулировка смазанная — речь про материализованные представления и индексы на них. Materialized view хранит результат запроса физически, ведёт себя как таблица, и на неё можно и нужно вешать обычные индексы; более того, REFRESH MATERIALIZED VIEW CONCURRENTLY вообще невозможен без хотя бы одного уникального индекса на матвью.
Глубже. Практика: матвью применяют для тяжёлых агрегатов и витрин, где допустима задержка данных. Обычный REFRESH MATERIALIZED VIEW берёт ACCESS EXCLUSIVE и на время пересчёта делает представление недоступным; CONCURRENTLY работает без блокировки чтений, но требует уникального индекса и обходится дороже (он вычисляет дельту). Автоматического инкрементального обновления в ванильном PostgreSQL нет — обновление вешают на cron/pg_cron или на триггеры. Если интервьюер имел в виду «индексированные представления» из MS SQL Server — это их аналог, но там индексированное view обновляется синхронно, а в Postgres — только по явному refresh. Обычное (не материализованное) VIEW собственных индексов не имеет: оно разворачивается в запрос, и работают индексы базовых таблиц.
Что такое индексация?
Заголовок раздела «Что такое индексация?»Коротко. Индексация — процесс создания и поддержания индексных структур над данными, чтобы поиск по значению шёл за O(log n) (или за константу для хеша) вместо полного перебора O(n). В БД это CREATE INDEX и последующее автоматическое обновление индекса при каждой модификации данных.
Глубже. Стоит развести два смысла термина. В реляционных СУБД индексация — вспомогательная оптимизация поверх таблицы. В поисковых движках (Elasticsearch, Lucene) «индексация» — это основной процесс приёма документа: анализ текста (токенизация, нормализация, стемминг) и построение inverted index, где ключ — токен, а значение — список документов. В Postgres прямой аналог inverted index — GIN, на нём же строится полнотекстовый поиск по tsvector. Технически важная деталь про сам процесс: CREATE INDEX блокирует запись в таблицу на всё время построения, поэтому на проде используют CREATE INDEX CONCURRENTLY — он делает два прохода, не берёт блокировку записи, но не может выполняться в транзакции и при неудаче оставляет индекс в состоянии INVALID, который нужно дропнуть и пересоздать.
Почему не стоит, вешать индексы на все столбцы и их комбинации?
Заголовок раздела «Почему не стоит, вешать индексы на все столбцы и их комбинации?»Коротко. Потому что индексы платные на запись: каждый INSERT/DELETE и каждый UPDATE проиндексированной колонки обновляет все соответствующие индексы, пишет дополнительный WAL и потребляет место — суммарный объём индексов легко превышает объём таблицы. Плюс число комбинаций растёт факториально: для 10 колонок это тысячи возможных составных индексов, поддерживать которые невозможно.
Глубже. Менее очевидные издержки: лишние индексы удлиняют вакуум (он обязан пройти по каждому индексу), увеличивают время планирования (планировщик перебирает больше путей), вытесняют полезные данные из shared_buffers и мешают HOT-обновлениям — обновление строки, не затрагивающее ни одной проиндексированной колонки, может остаться внутри страницы и вовсе не трогать индексы, но чем больше колонок покрыто, тем реже это происходит. Правильный подход: собрать реальные запросы (pg_stat_statements), построить индексы под топовые из них, регулярно вычищать индексы с idx_scan = 0 и помнить, что один грамотно составленный составной индекс (a, b, c) закрывает запросы по a, (a,b) и (a,b,c), то есть заменяет несколько одиночных.
Как работает индекс в бд?
Заголовок раздела «Как работает индекс в бд?»Коротко. При поиске СУБД спускается по индексной структуре от корня к листу, сравнивая ключ, находит в листе пары «ключ → TID» и по TID читает нужные страницы таблицы; при записи она параллельно с изменением строки в куче вставляет/удаляет соответствующие записи во всех индексах, при переполнении страницы расщепляя её.
Глубже. Разложим на шаги для B-tree в Postgres: планировщик формирует из WHERE условия сканирования индекса (index conditions); executor вызывает метод доступа, тот читает корневую страницу (почти наверняка из shared_buffers), бинарным поиском внутри страницы выбирает потомка, спускается на 2–4 уровня до листа; в листе последовательно читает записи, пока выполняется условие, отдавая TID-ы наверх. Дальше либо Index Scan (сразу heap fetch и проверка видимости версии строки), либо Index Only Scan (если visibility map говорит, что страница all-visible, кучу не трогаем), либо Bitmap (накапливаем TID-ы, сортируем по номерам страниц, читаем кучу по порядку). Важный нюанс MVCC: в индексе нет информации о видимости версии, поэтому Index Scan всегда обязан заглянуть в кучу, чтобы проверить xmin/xmax — это и есть причина, по которой в Postgres индексный доступ дороже, чем в СУБД с кластеризованным первичным ключом вроде InnoDB.
Что такое составной индекс?
Заголовок раздела «Что такое составной индекс?»Коротко. Индекс, построенный по нескольким колонкам сразу: CREATE INDEX idx ON t (a, b, c). Ключом является кортеж значений, записи упорядочены лексикографически — сначала по a, при равных a по b, затем по c.
Глубже. В PostgreSQL многоколоночными могут быть B-tree, GiST, GIN и BRIN; hash — нет. Ограничение по умолчанию — до 32 колонок в индексе. Составной индекс ценен тремя вещами: он закрывает фильтр по нескольким условиям одним сканированием (вместо BitmapAnd двух индексов), он позволяет получить готовый порядок для ORDER BY a, b, и он может стать покрывающим — тогда возможен Index Only Scan. Если колонка нужна только для чтения, а не для фильтрации и сортировки, её лучше добавить не в ключ, а в INCLUDE (...): она попадёт только в листья, не увеличит внутренние узлы и не помешает уникальности.
Важна ли последовательность в составном индексе?
Заголовок раздела «Важна ли последовательность в составном индексе?»Коротко. Да — см. выше про порядок столбцов: индекс работает по левому префиксу ключа, поэтому (a, b) и (b, a) — разные индексы с разными сценариями применения.
Глубже. Единственное, что здесь стоит добавить к предыдущему ответу: порядок условий в тексте запроса не важен вообще, WHERE a = 1 AND b = 2 эквивалентно WHERE b = 2 AND a = 1. Важен порядок колонок в определении индекса и то, какое условие по ним стоит: цепочка равенств продолжает «сужать» поиск, а первое же диапазонное условие её обрывает — колонки правее него становятся просто фильтром внутри просканированного участка индекса.
Что такое селективность индекса?
Заголовок раздела «Что такое селективность индекса?»Коротко. Селективность — мера того, насколько узкую выборку даёт условие по индексу: доля строк таблицы, которая остаётся после фильтра. Условие, отбирающее 0.01% строк, высокоселективно (индекс полезен), условие на 50% строк — низкоселективно (планировщик уйдёт в Seq Scan).
Глубже. Терминологическая путаница, на которой ловят: в теории БД «selectivity» — это дробь от 0 до 1 (меньше = лучше), а «cardinality» индекса — число различных значений (больше = лучше). Люди часто говорят «высокая селективность», имея в виду и то и другое; на собеседовании безопаснее сразу проговорить определение через долю строк. Уникальный индекс — предельный случай: селективность 1/N. Ещё нюанс: селективность бывает у колонки в среднем и у конкретного значения — при перекошенном распределении (status = 'cancelled' — 0.1%, status = 'paid' — 90%) Postgres благодаря most_common_vals выбирает разные планы для разных значений одного и того же запроса, что заодно объясняет проблему закешированного generic-плана для prepared statement.
Как посчитать селективность индекса?
Заголовок раздела «Как посчитать селективность индекса?»Коротко. Для колонки: count(distinct col) / count(*) — чем ближе к 1, тем лучше кандидат в индекс. Для конкретного условия: count(*) FILTER (WHERE условие) / count(*). Оценку планировщика смотрят в pg_stats (n_distinct, most_common_freqs) или прямо в EXPLAIN по полю rows.
Глубже. Пример полного замера:
-- 1) средняя селективность колонкиSELECT count(DISTINCT city_id)::numeric / nullif(count(*), 0) AS selectivityFROM people;
-- 2) селективность конкретного значенияSELECT count(*) FILTER (WHERE city_id = 77)::numeric / count(*) FROM people;
-- 3) что об этом думает планировщик (n_distinct < 0 означает долю от числа строк)SELECT attname, n_distinct, null_frac, most_common_vals, most_common_freqsFROM pg_statsWHERE schemaname = 'public' AND tablename = 'people' AND attname = 'city_id';Полезно знать, что отрицательное n_distinct в pg_stats — это не ошибка, а доля уникальных значений от общего числа строк (например, -0.5 = половина строк уникальна); такое значение можно задать вручную через ALTER TABLE ... ALTER COLUMN ... SET (n_distinct = ...), если сэмплирование ANALYZE систематически врёт. Для составных индексов одиночной статистики мало: Postgres по умолчанию считает колонки независимыми и перемножает селективности, из-за чего при коррелирующих колонках (город и регион) недооценивает результат в разы — лечится CREATE STATISTICS ... (dependencies, ndistinct) ON a, b FROM t.
Почему uuid медленнее чем serial? (uuid - 16 байт + медленнее обновление индекса, потому что у serial новое значение индекса всегда падает в конец индекса)
Заголовок раздела «Почему uuid медленнее чем serial? (uuid - 16 байт + медленнее обновление индекса, потому что у serial новое значение индекса всегда падает в конец индекса)»Коротко. Две причины, и они названы в вопросе верно. Первая — размер: 16 байт против 8 у bigserial (и 4 у serial), значит меньше записей на страницу индекса, больший индекс, больше уровней дерева, хуже кеш-попадания. Вторая, более важная — случайность: UUIDv4 распределён равномерно, поэтому каждая вставка попадает в случайную страницу B-tree, рабочий набор «горячих» страниц равен всему индексу, растут random I/O, расщепления страниц и объём WAL (full page writes при первом изменении страницы после чекпоинта). Последовательный ключ всегда пишется в самую правую страницу, которая всегда в кеше, и заполняет её плотно.
Глубже. Дополнительные эффекты: при случайных вставках страницы расщепляются пополам и в среднем заполнены на ~50–70%, тогда как при монотонном ключе B-tree в Postgres применяет оптимизацию rightmost split и заполняет страницы почти полностью — разница в размере индекса легко двукратная. Также случайный ключ убивает корреляцию порядка вставки с физическим порядком строк, что делает бесполезным BRIN и ухудшает range-запросы. Обратная сторона serial: точка контентности на правом краю индекса при высоком параллелизме вставок и предсказуемость идентификаторов (утечка бизнес-информации, перебор в API). Компромисс — time-ordered UUID (UUIDv7, RFC 9562, 2024): старшие биты — миллисекундная метка времени, поэтому значения растут монотонно и ведут себя в индексе почти как serial, сохраняя генерацию на стороне клиента и глобальную уникальность; в PostgreSQL 18 появилась встроенная функция uuidv7(), до неё генерировали на стороне приложения или расширением. И ещё важно: тип uuid в Postgres — это именно 16 байт, а не текст; хранение UUID в text/varchar(36) — распространённая ошибка, дающая 37 байт и медленное сравнение.
Какие виды индексов? Для чего нужны? Как работают под капотом?
Заголовок раздела «Какие виды индексов? Для чего нужны? Как работают под капотом?»Коротко. См. выше перечисление типов Postgres. Кратко под капотом: B-tree — сбалансированное многоуровневое дерево страниц с упорядоченными листьями, для сравнений и сортировки; Hash — таблица бакетов по 32-битному хешу, только =; GiST — сбалансированное дерево с пользовательскими предикатами «ключ содержит поддерево», для пересечений, диапазонов, KNN; SP-GiST — несбалансированные разбиения пространства (quad-tree, radix), для точек и префиксов; GIN — обратный индекс «элемент → posting list документов», для массивов, jsonb, полнотекста; BRIN — для каждого диапазона блоков хранит min/max, крошечный, работает только при физической корреляции.
Глубже. Ключевые практические различия: GIN даёт быстрый поиск по вхождению, но медленную вставку — её смягчает отложенный список fastupdate и gin_pending_list_limit; GiST дешевле в записи, но даёт «приблизительный» ответ с последующей перепроверкой (lossy), зато умеет KNN-поиск (ORDER BY point <-> ...) и exclusion-ограничения; BRIN размером в килобайты индексирует таблицы на терабайты, но при перемешанных данных бесполезен, а параметр pages_per_range определяет точность. Для полнотекста и LIKE '%...%' используют pg_trgm (gin_trgm_ops/gist_trgm_ops), для jsonb — jsonb_ops (все ключи и значения) или более компактный jsonb_path_ops (только пути с оператором @>).
Как устроен b-tree, чем отличается от бинарного, как происходит поиск, какую имеет структуру, чем помогает балансировка, сколько может быть детей у листьев (хотят пересказ из postgres internal).
Заголовок раздела «Как устроен b-tree, чем отличается от бинарного, как происходит поиск, какую имеет структуру, чем помогает балансировка, сколько может быть детей у листьев (хотят пересказ из postgres internal).»Коротко. B-tree — сбалансированное дерево с высоким ветвлением: узел равен странице (в Postgres 8 KB) и содержит сотни ключей, а не один, как в бинарном дереве. Все листья лежат на одном уровне (это и есть балансировка), поиск идёт от корня вниз бинарным поиском внутри каждой страницы, число обращений к диску равно высоте дерева — обычно 3–4 даже для сотен миллионов строк. У листьев детей нет по определению; в реализации Postgres (B+-дерево в стиле Lehman–Yao) данные-указатели лежат только в листьях, а листья связаны в двусвязный список для диапазонных сканов.
Глубже. Разница с бинарным деревом не в асимптотике (O(log n) и там и там), а в базе логарифма и в единице I/O: у бинарного дерева каждое сравнение — потенциально отдельная страница, у B-tree за одно чтение страницы делается ~9–10 «бинарных шагов» внутри неё. Оценка fanout: страница 8 KB минус заголовок, запись = ключ + 6-байтный TID + заголовок; для bigint это ~16 байт, то есть порядка 400–500 записей на страницу, и дерево высотой 4 адресует ~500⁴ ≈ 6·10¹⁰ строк. Устройство страницы в Postgres: заголовок, массив указателей на элементы (line pointers), сами элементы с конца, плюс high key — верхняя граница ключей страницы, и right link — ссылка на правого соседа; именно right-link по схеме Lehman–Yao позволяет читателю не блокироваться при расщеплении страницы конкурирующим писателем: если он попал на страницу, из которой ключ уже уехал вправо, high key подскажет пройти по right link. Балансировка поддерживается снизу вверх при расщеплении: переполненная страница делится, средний (разделяющий) ключ поднимается в родителя, при переполнении корня дерево растёт в высоту — только так, поэтому все листья всегда на одном уровне. Ограничения: размер записи не более примерно 1/3 страницы (~2704 байта), иначе CREATE INDEX упадёт с ошибкой «index row size exceeds maximum». Из современного: дедупликация (PG 13) хранит повторяющиеся ключи как один ключ со списком TID, а bottom-up index deletion (PG 14) вычищает записи, ставшие мусором из-за неHOT-обновлений, вместо того чтобы расщеплять страницу.
За счет чего b-trее хорошо работает с большими/меньше и диапазонами?
Заголовок раздела «За счет чего b-trее хорошо работает с большими/меньше и диапазонами?»Коротко. За счёт того, что записи в листьях физически упорядочены по ключу, а сами листья связаны в двусвязный список. Для BETWEEN a AND b достаточно один раз спуститься по дереву к границе a, а дальше идти по цепочке листьев подряд, пока ключ не превысит b — это последовательное чтение внутри индекса, без повторных спусков.
Глубже. Отсюда же берутся другие свойства: ORDER BY по ключу индекса выполняется без узла Sort (в том числе в обратную сторону — двусвязность списка листьев), MIN/MAX превращаются в чтение крайней записи (Result + Limit над Index Scan), а > без верхней границы — в проход от точки входа до конца индекса. Важная оговорка: последовательным будет чтение индекса, а обращения к куче всё равно случайные, поэтому широкий диапазон превращается в Bitmap Heap Scan или Seq Scan. Здесь и всплывает correlation из pg_stats: если физический порядок строк совпадает с порядком ключа (типично для created_at с append-only вставками), стоимость Index Scan резко падает, и для таких данных вместо B-tree часто хватает BRIN.
Как устроен hash индекс?
Заголовок раздела «Как устроен hash индекс?»Коротко. К значению ключа применяется хеш-функция типа данных, получается 32-битный код; по нему вычисляется номер бакета, бакет — это одна или несколько страниц, в которых хранятся пары «хеш-код → TID». Поиск равенства: посчитать хеш, найти бакет, перебрать его записи, сравнив хеш-коды, и для выживших сходить в кучу за реальным значением и проверить условие.
Глубже. В PostgreSQL индекс состоит из метастраницы, страниц бакетов, overflow-страниц и bitmap-страниц (учёт свободных overflow-страниц). Число бакетов растёт постепенно: при превышении порога заполненности выполняется split одного бакета, часть записей переезжает в новый — это инкрементальное расширение, а не полная перестройка. Само значение ключа в индексе не хранится, только хеш, поэтому: нельзя сделать index-only scan; при коллизии хешей обязательна перепроверка по куче; и, что важно, размер hash-индекса не зависит от длины ключа — это его единственное реальное преимущество на длинных строках. С PG 10 hash-индексы полноценно журналируются в WAL, реплицируются и переживают крах.
Будет ли работать составной индекс по полям (a, b, c) если мы ищем по полям b и c?
Заголовок раздела «Будет ли работать составной индекс по полям (a, b, c) если мы ищем по полям b и c?»Коротко. Как эффективный поисковый — нет: отсутствует левый префикс a, поэтому нельзя ограничить участок индекса и спуститься к нужным записям. Планировщик либо возьмёт Seq Scan, либо, если индекс сильно меньше таблицы, сделает полное сканирование индекса (Index Only Scan / Bitmap Index Scan с фильтром) — это может оказаться дешевле чтения кучи, но выигрыш несопоставим с обычным индексным поиском.
Глубже. Есть два уточнения. Первое: с PostgreSQL 18 у B-tree появился skip scan — если у ведущей колонки a мало различных значений, executor может перебрать их по очереди и для каждого выполнить поиск по (b, c); это спасает узкий класс случаев, но при высокой кардинальности a не работает. Второе: если запрос по b, c регулярный и важный, правильное решение — отдельный индекс (b, c), а не надежда на существующий. Проверять всегда через EXPLAIN (ANALYZE, BUFFERS) на реальных объёмах: на маленькой тестовой таблице Postgres в любом случае выберет Seq Scan, и эксперимент ничего не покажет.
Индекс построили, но запрос все равно тормозит, что делать?
Заголовок раздела «Индекс построили, но запрос все равно тормозит, что делать?»Коротко. Снять EXPLAIN (ANALYZE, BUFFERS) и понять, где время: индекс не используется вовсе, используется, но возвращает слишком много строк, или узкое место вообще не в доступе к таблице (сортировка, join, агрегат, сеть, OFFSET).
Глубже. Разбор по симптомам. Если в плане Seq Scan — проверить совместимость условия с индексом (функция над колонкой, приведение типов, LIKE '%...'), выполнить ANALYZE, посмотреть селективность, при перекосе — поднять statistics target или сделать частичный индекс. Если Index Scan, но много Rows Removed by Filter — индекс не покрывает часть условий, нужно расширить его составными колонками. Если много Heap Fetches в Index Only Scan — не обновлена visibility map, нужен вакуум. Если основное время в Sort или Hash Aggregate со Disk: — не хватает work_mem или индекс не даёт нужный порядок. Если тормозит Nested Loop с огромным числом итераций — виновата недооценка строк, помогают расширенная статистика CREATE STATISTICS или переписывание запроса. Отдельные классические случаи: пагинация через большой OFFSET (лечится keyset-пагинацией WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT 20), раздутый индекс (REINDEX CONCURRENTLY), generic plan у prepared statement при перекошенных данных (plan_cache_mode = force_custom_plan), и банальная блокировка — тогда время уходит не в чтение, и это видно по pg_locks/pg_stat_activity.
Какие бывают типы индексов?
Заголовок раздела «Какие бывают типы индексов?»Коротко. См. выше подробный разбор типов в Postgres (B-tree, Hash, GiST, SP-GiST, GIN, BRIN). Если вопрос задан вне контекста конкретной СУБД, полезно разложить по другим осям: по структуре (древовидные, хеш, битовые, инвертированные, LSM), по отношению к хранению данных (кластеризованный против некластеризованного), по уникальности (unique/non-unique), по числу колонок (одиночный/составной), по охвату (полный/частичный), по содержимому (по колонке/по выражению/покрывающий).
Глубже. Про кластеризованный индекс стоит сказать отдельно, потому что это частый уточняющий вопрос: в MySQL/InnoDB первичный ключ является таблицей (данные лежат в листьях B+-дерева PK), а вторичные индексы хранят значение PK, поэтому чтение по вторичному индексу требует второго спуска по дереву PK. В PostgreSQL кластеризованных индексов нет вообще: все индексы вторичные и ссылаются на TID, а команда CLUSTER лишь однократно переупорядочивает физически файл таблицы и порядок не поддерживает. Ещё стоит упомянуть bitmap-индексы как тип хранения (есть в Oracle, хороши для колонок низкой кардинальности в OLAP) — в Postgres такого типа индекса нет, но есть bitmap-сканирование, которое строит битовую карту на лету; на этом различии часто ловят.
Расскажи про индексы
Заголовок раздела «Расскажи про индексы»Коротко. Открытый вопрос — отвечать структурой: (1) что это и зачем — ускорение поиска ценой записи и места; (2) как устроено — B-tree, ключ → TID, спуск по дереву, index/index-only/bitmap scan; (3) какие бывают в Postgres — шесть методов доступа плюс свойства (unique, partial, expression, covering, multicolumn); (4) как выбирать — под конкретные запросы, селективность, порядок колонок в составном; (5) цена — запись, WAL, вакуум, раздувание; (6) как проверять — EXPLAIN (ANALYZE, BUFFERS), pg_stat_user_indexes.
Глубже. В таком вопросе интервьюер проверяет не знание фактов, а умение структурировать. Держите наготове один рабочий пример: «была таблица событий на 300 млн строк, запросы вида WHERE tenant_id = $1 AND created_at BETWEEN ... ORDER BY created_at DESC LIMIT 50; построили (tenant_id, created_at DESC) INCLUDE (payload_id), получили Index Only Scan вместо Seq Scan, время с 8 с до 15 мс» — конкретика с числами и планом ценится сильно выше пересказа теории. И обязательно проговорите обратную сторону: где вы индекс убирали, потому что он не использовался и тормозил вставку.
У нас есть таблица с bigint primary key. Мы добавляем записи в таблицу и удаляем их оттуда случайным образом. Вопрос: как найти первый незанятый индекс в этой таблице?
Заголовок раздела «У нас есть таблица с bigint primary key. Мы добавляем записи в таблицу и удаляем их оттуда случайным образом. Вопрос: как найти первый незанятый индекс в этой таблице?»Коротко. Задача поиска первого «дырки» в возрастающей последовательности. На SQL это анти-join: ищем минимальный id, для которого нет id + 1, с отдельной проверкой начала диапазона.
SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM t WHERE id = 1) THEN 1 ELSE (SELECT min(t1.id) + 1 FROM t t1 WHERE NOT EXISTS (SELECT 1 FROM t t2 WHERE t2.id = t1.id + 1)) END AS first_free_id;Глубже. Альтернатива через оконную функцию (удобна, когда нужны все дыры, а не первая):
SELECT id + 1 AS gap_start, next_id - 1 AS gap_endFROM (SELECT id, lead(id) OVER (ORDER BY id) AS next_id FROM t) sWHERE next_id > id + 1ORDER BY gap_startLIMIT 1;Про производительность: оба варианта в теории обслуживаются index-only scan по PK, но в худшем случае (плотный префикс без дыр) придётся просканировать весь индекс до первой дыры — это O(k), где k — позиция дыры, а не O(log n). Если дыры близко к началу, ответ мгновенный; если id заняты подряд до миллиарда — придётся прочитать миллиард индексных записей. Правильный «инженерный» довод, который тут и хотят услышать: переиспользовать id обычно не нужно и вредно — bigint не кончится (9.2·10¹⁸), а переиспользование ломает внешние ссылки, кеши, аудит и идемпотентность. Если задача реальная (пул номеров, например, номера портов или слотов), делают отдельную таблицу свободных значений (free_ids) и берут из неё SELECT ... FOR UPDATE SKIP LOCKED LIMIT 1 — это O(log n) и корректно работает при конкуренции. Ещё частая путаница в этом вопросе: значение serial/identity берётся из sequence, которая не откатывается при rollback и не переиспользует значения, так что дыры в id — норма, а не аномалия.
Нужно учитывать пары любых двух элементов, включая одинаковые по значению, но с разными индексами.
Заголовок раздела «Нужно учитывать пары любых двух элементов, включая одинаковые по значению, но с разными индексами.»Обрывок исходника, вопрос не восстанавливается. Похоже на фрагмент условия алгоритмической задачи (подсчёт пар i < j в массиве, где «индекс» — позиция элемента, а не индекс БД); к индексам в базе данных отношения не имеет.
Когда PostgreSQL не использует индекс?
Заголовок раздела «Когда PostgreSQL не использует индекс?»Коротко. Когда планировщик считает Seq Scan дешевле или когда индекс технически неприменим. Дешевле — на маленьких таблицах, при низкой селективности условия (отбирается большая доля строк), при плохой корреляции и дорогом random I/O. Неприменим — при функции/выражении над колонкой, несовпадении типов, LIKE '%...', несовпадении класса операторов или коллации, отсутствии левого префикса составного индекса, а также если условие не соответствует предикату частичного индекса.
Глубже. Полный чек-лист причин, который стоит держать в голове: устаревшая статистика (не было ANALYZE) или её недостаточная детализация; перекос данных и коррелирующие колонки — недооценка/переоценка строк; неверно настроенные random_page_cost (для SSD его обычно снижают с 4.0 до 1.1) и effective_cache_size; индекс в состоянии INVALID после неудачного CREATE INDEX CONCURRENTLY (виден как indisvalid = false в pg_index); индекс на партиционированной таблице, созданный не на всех партициях; ORDER BY с другой коллацией или направлением; условия через OR, которые не свелись к BitmapOr; <>, NOT IN, NOT LIKE; неявное приведение вида WHERE bigint_col = '123'::text (наоборот, text_col = 123 приведёт колонку и убьёт индекс); наконец, параллельный Seq Scan, который на многоядерной машине может обогнать одиночный Index Scan. Диагностика: SET enable_seqscan = off для проверки гипотезы, сравнение rows и actual rows в плане, pg_index.indisvalid, pg_stat_user_indexes.idx_scan.
Что такое индексация в базах данных, для чего она нужна, минусы индексов и почему нельзя покрыть всю таблицу индексами?
Заголовок раздела «Что такое индексация в базах данных, для чего она нужна, минусы индексов и почему нельзя покрыть всю таблицу индексами?»Коротко. Индексация — построение вспомогательных структур для поиска по значению без полного перебора; нужна для ускорения WHERE, JOIN, ORDER BY, GROUP BY и для обеспечения уникальности. Минусы: замедление INSERT/UPDATE/DELETE, дополнительный объём на диске и в кеше, рост WAL, удлинение вакуума и восстановления, усложнение планирования. Покрыть всю таблицу нельзя потому, что стоимость записи растёт линейно с числом индексов, а число полезных комбинаций колонок — комбинаторно; в пределе получится база, где запись стоит дороже, чем выигрыш на чтении.
Глубже. Полезно оценить цену в цифрах: индекс по bigint на таблице в 100 млн строк — это примерно 2–3 GB (16 байт на запись плюс накладные расходы страниц), и десять таких индексов дадут 20–30 GB, которые конкурируют с самой таблицей за page cache. Каждый лишний индекс — это ещё один проход вакуума и ещё одна запись в WAL при каждой вставке. Практический критерий: индекс оправдан, если он реально используется (idx_scan растёт) и если запрос, ради которого он создан, значим для продукта. Обратная сторона — таблицы с интенсивной записью и редким чтением (логи, очереди событий): там индексов держат минимум, а поиск выносят в отдельное хранилище или в партиции по времени, где старые партиции индексируются, а горячая — нет.
Что такое составные индексы? В чем их отличие от простого индекса на колонку?
Заголовок раздела «Что такое составные индексы? В чем их отличие от простого индекса на колонку?»Коротко. Составной (многоколоночный) индекс строится по кортежу колонок и упорядочен лексикографически, простой — по одной колонке. Отличие практическое: составной закрывает фильтр сразу по нескольким условиям одним спуском по дереву и даёт готовый порядок для ORDER BY a, b, но работает только по левому префиксу; простой универсальнее для одиночного условия, зато при a = 1 AND b = 2 два простых индекса дадут лишь BitmapAnd — пересечение битовых карт, которое почти всегда дороже.
Глубже. Когда что выбирать. Один составной (a, b, c) вместо трёх простых имеет смысл, если запросы устойчиво фильтруют по a, по (a, b) или по всем трём — он же покроет и эти комбинации, то есть заменит индексы (a) и (a, b), которые после его создания можно удалить. Три простых предпочтительны, если колонки используются в запросах независимо и в разных сочетаниях. Дополнительные детали: составной индекс шире, значит меньше записей на страницу и выше дерево; уникальность у составного индекса — на кортеже, а не на каждой колонке; колонки, нужные только для возврата данных, лучше выносить в INCLUDE, чтобы получить Index Only Scan без раздувания ключа; и наконец, у каждой колонки в B-tree можно задать своё направление сортировки и положение NULL ((a ASC, b DESC NULLS LAST)), что важно, когда индекс обслуживает конкретный ORDER BY.
Частые ошибки на собесе
Заголовок раздела «Частые ошибки на собесе»- Путают порядок колонок в индексе с порядком условий в
WHERE: заявляют, чтоa = 1 AND b = 2иb = 2 AND a = 1дадут разные планы. Порядок условий не важен, порядок колонок в индексе — критичен. - Говорят «создал индекс — запрос ускорится», не упоминая селективность и планировщик. Индекс на колонку с четырьмя значениями просто не будет использован.
- Считают, что B-tree — это бинарное дерево, и расшифровывают «B» как «binary». B-tree ветвится в сотни, «B» официально не расшифровано (скорее всего balanced/Bayer).
- Забывают, что в PostgreSQL индекс под внешний ключ не создаётся автоматически, и удивляются Seq Scan по дочерней таблице при удалении родительской строки.
- Считают Index Only Scan бесплатным и не знают про visibility map и
Heap Fetches— после массового обновления «покрывающий» индекс перестаёт покрывать доVACUUM. - Не различают
VIEWиMATERIALIZED VIEW: обычное представление не ускоряет ничего, это подстановка текста запроса. - Создают индексы в проде обычным
CREATE INDEX, блокируя запись в таблицу, и не знают проCONCURRENTLYи про то, что его нельзя выполнять в транзакции. - Перечисляют типы индексов вперемешку со свойствами («B-tree, hash, уникальный, кластерный, покрывающий»), не понимая, что это разные измерения.
- Утверждают, что hash-индекс всегда быстрее B-tree из-за O(1), не замечая, что доминирует поход в кучу, а B-tree при этом даёт диапазоны и сортировку.
- Говорят «индекс — это как оглавление книги» и на этом останавливаются, не умея объяснить ни структуру B-tree, ни почему индекс может не примениться.
- Путают порядок колонок в индексе с порядком условий в WHERE и утверждают, что
WHERE a=1 AND b=2иWHERE b=2 AND a=1дают разные планы. - Считают, что индекс на колонке низкой кардинальности (пол, флаг, статус) всегда ускоряет запрос, и не упоминают частичный индекс как правильное решение для редкого значения.
- Утверждают, что PostgreSQL хранит данные в листьях первичного ключа — это про InnoDB; в Postgres все индексы вторичные, а кластеризованных нет.
- Забывают про цену индекса на запись, WAL и вакуум и предлагают «повесить индексы на все поля».
- Не различают Index Scan, Index Only Scan и Bitmap Heap Scan, а значит не могут объяснить
Heap Fetches,Recheck Condиlossy. - Считают, что
VACUUMвозвращает место операционной системе и полностью снимает раздувание индексов, и не знают проREINDEX CONCURRENTLY. - Уверены, что hash-индекс всегда быстрее B-tree, потому что «O(1) против O(log n)», не учитывая, что B-tree высотой 3–4 целиком в кеше и умеет несравнимо больше.
Что почитать
Заголовок раздела «Что почитать»- PostgreSQL Documentation — «Indexes» (главы 11.1–11.11: типы, составные, уникальные, по выражению, частичные, Index-Only Scans): https://www.postgresql.org/docs/current/indexes.html
- PostgreSQL Documentation — «Index Access Method Interface» и описание B-tree: https://www.postgresql.org/docs/current/btree.html
- Егор Рогов, «PostgreSQL изнутри» (главы про индексы и планировщик) и цикл статей Postgres Professional «Индексы в PostgreSQL»: https://habr.com/ru/companies/postgrespro/articles/326096/
- Markus Winand, «Use The Index, Luke!» — лучший разбор составных индексов, правила левого префикса и sargability: https://use-the-index-luke.com/
- Исходники PostgreSQL:
src/backend/access/nbtree/README— описание реализации Lehman & Yao, дедупликации и bottom-up deletion. - Rudolf Bayer, Edward McCreight, «Organization and Maintenance of Large Ordered Indices» (1972) — оригинальная статья о B-tree.
- PostgreSQL Docs, «Indexes» — https://www.postgresql.org/docs/current/indexes.html (типы, частичные, покрывающие, порядок колонок).
- PostgreSQL Docs, «B-Tree Indexes» и README исходников
src/backend/access/nbtree/README— устройство страниц, high key, right link, схема Lehman–Yao. - PostgreSQL Docs, «Routine Vacuuming» — https://www.postgresql.org/docs/current/routine-vacuuming.html и «VACUUM».
- Егор Рогов, «PostgreSQL изнутри» (postgrespro.ru/education/books/internals) — главы про индексы, MVCC и вакуум; лучший русскоязычный источник по этой теме.
- Markus Winand, «Use The Index, Luke!» — https://use-the-index-luke.com/ — про левый префикс, порядок колонок и keyset-пагинацию.
Список исходных вопросов с привязкой к компаниям: ../questions/indexes.md