Если CREATE INDEX CONCURRENTLY или REINDEX INDEX CONCURRENTLY завершается с ошибкой, Postgres оставляет после себя невалидный индекс. Многие инженеры считают такие индексы безобидными заготовками, которые просто ждут удаления. В конце концов, планировщик ведь не использует их в запросах, верно?
Нет. Невалидные индексы совсем не безобидны. Они продолжают потреблять ресурсы, генерировать ввод‑вывод, мешать оптимизациям и даже создавать конкуренцию за блокировки — при этом не давая никакого выигрыша в производительности запросов.
В этой статье мы на реальных примерах покажем скрытые издержки невалидных индексов, о которых должен знать каждый администратор Postgres.
Откуда берутся невалидные индексы
Обычно невалидные индексы появляются после неудачно завершившихся параллельных операций:
-- Эта команда может завершиться с ошибкой и оставить невалидный индекс. -- Например, если таблица большая, а statement_timeout установлен слишком низко. create index concurrently idx_orders_created_at on orders(created_at); -- Проверяем наличие невалидных индексов select indexrelid::regclass as index_name, indisvalid as is_valid, indisready as is_ready from pg_index where not indisvalid;
Индекс с indisvalid = false помечен как невалидный. Документация Postgres рекомендует удалять такие индексы, но многие команды оставляют их на месте, считая, что они ни на что не влияют. Давайте разберемся, почему это опасное заблуждение.
Тестовый стенд
Во всех примерах ниже используется одна и та же простая конфигурация:
drop table if exists test_invalid_idx cascade; create table test_invalid_idx ( id bigserial primary key, indexed_col int, non_indexed_col text ) with (fillfactor = 50); insert into test_invalid_idx (indexed_col, non_indexed_col) select i, 'initial' from generate_series(1, 100) i; -- Создаём индекс, а затем помечаем его как невалидный, -- имитируя сбой CREATE INDEX CONCURRENTLY. -- При реальном сбое остаются indisready = true и indisvalid = false. create index idx_test_indexed_col on test_invalid_idx(indexed_col); update pg_index set indisvalid = false, indisready = true where indexrelid = 'idx_test_indexed_col'::regclass;
Теперь разберём каждую скрытую издержку по отдельности.
1. Обслуживается при каждой операции записи
Невалидные индексы обновляются при каждом INSERT и UPDATE, точно так же, как валидные. С помощью pageinspect — contrib‑модуля для низкоуровневого анализа страниц базы данных — посчитаем количество элементов на листовой странице B‑дерева:
create extension if not exists pageinspect; -- До вставки: 100 элементов на листовой странице невалидного индекса select count(*) as leaf_items from bt_page_items('idx_test_indexed_col', 1);
leaf_items ------------ 100
-- INSERT добавляет новую запись в невалидный индекс insert into test_invalid_idx (indexed_col, non_indexed_col) values (999, 'new'); select count(*) as leaf_items from bt_page_items('idx_test_indexed_col', 1);
leaf_items ------------ 101
-- UPDATE тоже добавляет новую запись (B‑деревья не обновляют записи на месте) update test_invalid_idx set indexed_col = 888 where indexed_col = 999; select count(*) as leaf_items from bt_page_items('idx_test_indexed_col', 1);
leaf_items ------------ 102
Каждая операция записи несёт издержки ввода‑вывода на обслуживание индекса, который не даёт никакого выигрыша в производительности запросов.
2. Обрабатывается VACUUM и генерирует WAL
VACUUM сканирует невалидные индексы, расходуя ресурсы autovacuum:
delete from test_invalid_idx where id > 50; vacuum verbose test_invalid_idx;
INFO: vacuuming "public.test_invalid_idx" index "idx_test_indexed_col": pages: 5 in total, 1 newly deleted ^^^^^^^^^^^^^^^^^^^^^^ Невалидный индекс был обработан!
DML‑операции также генерируют записи WAL для невалидных индексов. Проверим это с помощью pg_walinspect, доступного в Postgres 15 и новее:
create extension if not exists pg_walinspect; -- Сохраняем LSN до и после INSERT select pg_current_wal_lsn() as start_lsn \gset insert into test_invalid_idx (indexed_col, non_indexed_col) values (999, 'wal_test'); select pg_current_wal_lsn() as end_lsn \gset -- Ищем записи WAL для нашего невалидного индекса select resource_manager, record_type, block_ref from pg_get_wal_records_info(:'start_lsn', :'end_lsn') where block_ref like '%' || ( select relfilenode::text from pg_class where relname = 'idx_test_indexed_col' ) || '%';
resource_manager | record_type | block_ref ------------------+-------------+---------------------------------- Btree | INSERT_LEAF | blkref #0: rel 1663/5/16957 ... ^^^^^ Невалидный индекс!
Это увеличивает объём WAL, объём данных, передаваемых на реплики, и размер резервных копий.
3. Мешает HOT‑обновлениям
Это, пожалуй, самое серьёзное последствие. Оптимизация Postgres Heap‑Only Tuple (HOT) позволяет при обновлении строк не изменять индексы, если значения индексируемых столбцов не меняются. Невалидные индексы мешают HOT‑обновлениям для своих столбцов точно так же, как валидные, но при этом не дают никакого выигрыша при выполнении запросов. Вы платите полную цену индексирования, не получая от него никакой пользы.
select pg_stat_reset(); -- Обновляем неиндексируемый столбец update test_invalid_idx set non_indexed_col = 'updated' where id <= 10; select round(100.0 * n_tup_hot_upd / n_tup_upd, 1) as hot_percent from pg_stat_user_tables where relname = 'test_invalid_idx'; -- Результат: 96,8% select pg_stat_reset(); -- Обновляем столбец, покрытый невалидным индексом update test_invalid_idx set indexed_col = indexed_col + 1 where id <= 10; select round(100.0 * n_tup_hot_upd / n_tup_upd, 1) as hot_percent from pg_stat_user_tables where relname = 'test_invalid_idx'; -- Результат: 0%
Обновляемый столбец |
Доля HOT |
Неиндексируемый |
96,8% |
Покрытый невалидным индексом |
0% |
Нулевая доля HOT означает более быстрое разрастание таблицы, дополнительную работу для VACUUM и падение производительности — и всё это из‑за индекса, который никак не ускоряет запросы.
4. Засоряет статистику
В системах мониторинга невалидные индексы показывают ноль сканирований, поэтому могут выглядеть как обычные неиспользуемые индексы, если не проверять indisvalid. DBA, запускающий скрипты очистки, может даже не заметить, что индекс уже сломан.
5. Создаёт накладные расходы для планировщика и конкуренцию за блокировки
Во время планирования запроса Postgres получает AccessShareLock на все индексы участвующих таблиц, включая невалидные — если только вы не используете подготовленные запросы:
begin; explain select * from test_invalid_idx where indexed_col = 500; select c.relname, l.mode from pg_locks l join pg_class c on l.relation = c.oid where l.pid = pg_backend_pid();
object_name | lock_mode ----------------------+----------------- idx_test_indexed_col | AccessShareLock <-- Невалидный индекс тоже заблокирован! test_invalid_idx | AccessShareLock
Эта блокировка конфликтует с AccessExclusiveLock, которая требуется для DROP INDEX, REINDEX и ALTER INDEX. В нагруженной системе из‑за этого удалить невалидный индекс во время обычной работы может оказаться неожиданно сложно. Вы несёте те же издержки на блокировки, что и для полезного индекса, но не получаете ничего взамен.
6. Мешает повторному запуску миграций
Невалидные индексы могут мешать повторному запуску миграций схемы. Рассмотрим типичный сценарий:
-- Попытка миграции №1: завершается с ошибкой из-за statement_timeout или по другой причине create index concurrently idx_orders_created_at on orders(created_at); -- ERROR: canceling statement due to statement timeout
После неудачного CREATE INDEX CONCURRENTLY остаётся невалидный индекс с именем idx_orders_created_at. При повторном запуске миграции:
-- Попытка миграции №2: не проходит create index concurrently idx_orders_created_at on orders(created_at); -- ERROR: relation "idx_orders_created_at" already exists
Повторный запуск завершается с ошибкой, потому что имя индекса уже занято невалидным индексом. Особенно неприятно это проявляется в CI/CD‑пайплайнах, где миграции выполняются автоматически: пайплайн будет падать снова и снова, пока кто‑нибудь вручную не удалит невалидный индекс.
Решение: сначала удалить невалидный индекс, а затем повторить операцию:
drop index concurrently if exists idx_orders_created_at; create index concurrently idx_orders_created_at on orders(created_at);
Подведем итоги
Последствие: издержки как у валидного индекса, пользы — ноль |
Подтверждение |
Обслуживается при каждой операции записи |
leaf_items: 100 → 101 → 102 |
Обрабатывается VACUUM |
idx_test_indexed_col: pages: 5, 1 newly deleted |
Генерирует записи WAL |
Btree INSERT_LEAF для невалидного индекса |
Мешает HOT‑обновлениям |
Доля HOT падает с 96,8% до 0%, если столбец покрыт индексом |
Блокируется планировщиком |
AccessShareLock мешает DDL‑операциям |
Мешает повторному запуску миграций |
Ошибка relation already exists |
Как найти невалидные индексы
Выполните следующий запрос, чтобы найти все невалидные индексы в базе данных:
select n.nspname as schema, c.relname as index_name, t.relname as table_name, pg_size_pretty(pg_relation_size(c.oid)) as size, i.indisvalid as is_valid, i.indisready as is_ready from pg_index i join pg_class c on c.oid = i.indexrelid join pg_class t on t.oid = i.indrelid join pg_namespace n on n.oid = c.relnamespace where not i.indisvalid order by pg_relation_size(c.oid) desc;
Или используйте PostgresAI, чтобы находить их автоматически. Это одна из наших базовых проверок состояния — H001. Она помогает командам удалять невалидные индексы вручную или полностью автоматизировать этот процесс с помощью ИИ‑ассистентов вроде Claude Code и Cursor через CLI или MCP PostgresAI:

Удалить или пересоздать?
Прежде чем удалять невалидный индекс, выясните, нужен ли он и следует ли его пересоздать. Вот как это определить.
1. Проверьте наличие валидных дубликатов
Если на тех же столбцах уже существует валидный индекс, невалидный можно просто удалить:
select n.nspname as schema, ci.relname as invalid_index, t.relname as table_name, pg_get_indexdef(i.indexrelid) as definition, -- Проверяем наличие валидного дубликата (select string_agg(c2.relname, ', ') from pg_index i2 join pg_class c2 on c2.oid = i2.indexrelid where i2.indrelid = i.indrelid and i2.indisvalid and i2.indkey = i.indkey ) as valid_duplicates from pg_index i join pg_class ci on ci.oid = i.indexrelid join pg_class t on t.oid = i.indrelid join pg_namespace n on n.oid = ci.relnamespace where not i.indisvalid;
2. Проверьте, связан ли индекс с ограничением
Индексы, на которых основаны ограничения UNIQUE или PRIMARY KEY, необходимо пересоздать:
select c.relname as index_name, con.conname as constraint_name, case con.contype when 'p' then 'PRIMARY KEY' when 'u' then 'UNIQUE' end as constraint_type from pg_index i join pg_class c on c.oid = i.indexrelid left join pg_constraint con on con.conindid = i.indexrelid where not i.indisvalid and con.conname is not null;
3. Проверьте текущие планы запросов
Посмотрите, выполняются ли последовательные сканирования там, где мог бы помочь этот индекс:
-- Получаем определение индекса select pg_get_indexdef('your_invalid_index'::regclass); -- Проверяем план типового запроса по этому столбцу explain select * from your_table where indexed_column = 'value'; -- Seq Scan по большой таблице: скорее всего, индекс стоит пересоздать
Схема принятия решения

Примечание: всегда используйте DROP INDEX CONCURRENTLY и REINDEX INDEX CONCURRENTLY, чтобы не блокировать другие сессии.
Что делать
Невалидные индексы вовсе не бездействуют — они активно ухудшают производительность. Поэтому, обнаружив такой индекс, не откладывайте очистку:
Сразу удалите его с помощью
DROP INDEX CONCURRENTLY, если он не связан с нужным ограничением.Настройте мониторинг: добавьте
select count(*) from pg_index where not indisvalidв систему оповещений.Разберитесь в причине: проверьте журналы Postgres и выясните, почему исходный
CREATE INDEX CONCURRENTLYзавершился с ошибкой.
Проверьте, насколько уверенно вы работаете с индексами, блокировками и обслуживанием PostgreSQL, на вступительном тесте курса «Администрирование PostgreSQL. Экспертный уровень».

Невалидные индексы — лишь один из способов незаметно перегрузить PostgreSQL. На открытых уроках можно глубже разобрать, как на производительность влияют частые обновления, очереди, рост данных и масштабирование, чтобы точнее находить узкие места и выбирать устойчивые решения.
4 августа в 20:00. «PostgreSQL как память ИИ‑агентов: MVCC, очереди и партиции под нагрузкой». Записаться
19 августа в 20:00. «PostgreSQL на стероидах: большие данные, высокие нагрузки и масштабирование без боли». Записаться