lesson ── postgres-labs ── ~24 мин ── 5 шагов
Соберём частый паттерн «последние записи в ветке» и уберём дорогую
сортировку правильным составным индексом, заодно проверим правило
префикса и найдём неиспользуемые индексы. Запусти psql во вкладке
client.
интерактивный sandbox
Поднимется контейнер postgreslab/postgres-base с PostgreSQL 17 и psql. В браузере откроется терминал, база lab уже настроена. Каждый шаг проверяется автоматически. Сеть air-gapped, наружу контейнер не ходит.
stack ── PostgreSQL 17 · psql · 1 GB RAM · air-gapped · самоуничтожается через 45 мин простоя
CREATE TABLE repos AS
SELECT g id, (g % 5000) AS forked_from_id,
'2026-01-01'::timestamptz + (g || ' min')::interval AS updated_at,
'repo' || g AS name
FROM generate_series(1, 500000) g;
ANALYZE repos;
Паттерн: много форков от одного репозитория, нужны последние по времени.
✓ Таблица repos готова.
EXPLAIN SELECT * FROM repos
WHERE forked_from_id = 42 ORDER BY updated_at DESC LIMIT 10;
Предскажи: какой метод доступа и есть ли узел Sort? (Ожидается Seq Scan и Sort - большой cost.)
Без индекса надо прочитать все строки ветки и отсортировать.
✓ Seq Scan + Sort - дорого. Сейчас починим.
CREATE INDEX repos_fork_upd ON repos (forked_from_id, updated_at DESC);
ANALYZE repos;
EXPLAIN SELECT * FROM repos
WHERE forked_from_id = 42 ORDER BY updated_at DESC LIMIT 10;
Индекс находит участок forked_from_id = 42, где строки уже отсортированы по updated_at DESC - бери первые 10. Sort исчез.
Равенство раньше, сортировка в конце ключа - тогда ORDER BY бесплатен.
✓ Index Scan, сортировка ушла - cost упал на порядки.
EXPLAIN SELECT * FROM repos WHERE updated_at > '2026-06-01';
Индекс (forked_from_id, updated_at) отсортирован сначала по forked_from_id. Условие только по updated_at не фиксирует префикс - записи разбросаны, индекс не подходит. Предскажи: Seq Scan.
Индекс (a,b) помогает по a и (a,b), но не по одному b.
✓ Seq Scan - по средней колонке составной индекс не работает.
Заведём индекс и ни разу им не воспользуемся:
CREATE INDEX repos_name_unused ON repos (name);
SELECT indexrelname, idx_scan FROM pg_stat_user_indexes WHERE relname = 'repos';
У repos_name_unused idx_scan = 0 - им никто не пользуется, это
кандидат на удаление (налог на запись без пользы).
idx_scan = 0 значит, что индексом ни разу не воспользовались.
✓ idx_scan = 0 - неиспользуемый индекс найден.
Составной индекс (forked_from_id, updated_at DESC) убирает Sort в запросе ORDER BY ... LIMIT: строки уже отсортированы внутри участка. Правило префикса: индекс (a,b) помогает по a и (a,b), но не по одному b. Каждый индекс - налог на запись; неиспользуемые (idx_scan=0) удаляют.
команды
CREATE INDEX ON t (a, b DESC);составной индекс под WHERE a ORDER BY bSELECT indexrelname, idx_scan FROM pg_stat_user_indexes;найти неиспользуемые индексыконцепции