awesome-everything EN
↑ Обратно к восхождению

Базы данных

Акты 1–3 в глубину: схема, индексы и статистика планировщика

Суть Первые три акта компонуются необратимо — неправильный тип это многодневная миграция при 100M строк, правило leading-column — самый нарушаемый принцип в дикой природе, а устаревшая статистика заставляет планировщик выбирать план в 1000× медленнее необходимого.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Команда добавляет колонку TEXT для email вместо CITEXT в День 0. Шесть месяцев и 100M строк спустя баг с регистром требует смены типа по всей таблице. Исходное решение по схеме — принятое за пять секунд — стоит трёх дней и окна обслуживания. Схема — единственный акт, где цена ошибки необратима.

Акт 1 — Схема: единственное необратимое решение

Неправильный тип (TEXT vs CITEXT для email) становится многодневной миграцией при 100M строк. Неправильный выбор ключа (составной натуральный ключ vs суррогатный BIGSERIAL) делает каждый join дорогим навсегда. Реляционная модель вознаграждает строгость с самого начала:

  • NOT NULL по умолчанию — nullable колонки влекут app-level null-проверки там, где должны быть ограничения базы.
  • FK-ограничения, если нет измеренного throughput pressure — ссылочная целостность предотвращает orphaned child rows, которые на масштабе дают неправильные результаты запросов.
  • CHECK-ограничения для бизнес-инвариантов — прямой SQL update без них может создать невалидное состояние.
  • Суррогатные ключи для внешних ссылокid BIGSERIAL изолирует от изменений email; ретрофит суррогатных ключей при 1B строк требует полной перезаписи таблицы.

Ошибки схемы и escape hatches:

  • Составной натуральный ключ (user_id, organization_id, email) как PK означает: каждый join несёт три колонки; каждый FK реплицирует три колонки. Исправление: суррогатный id дешевле, но ретрофит требует миграции с перезаписью таблицы.
  • Отсутствующий FK на child table означает отсутствие ссылочной целостности; данные деградируют со временем. Исправление: добавить FK ретроактивно, но сначала исправить orphaned child rows.
  • Отсутствующее CHECK-ограничение на domain-specific колонке (status должен быть одним из ACTIVE, PENDING, ARCHIVED) — приложение единственный guard. Исправление: добавить ограничение, вычистить невалидные строки.

Нарушай правила только с доказательствами — отключай FK на highest-write child tables, если стоимость FK trigger превышает write budget, но никогда как стилистическое предпочтение.

Акт 2 — Индексы: окупаются только на правильных колонках

B-tree на email разрешает WHERE email = ? за O(log N) page reads. B-tree на (org_id, created_at DESC) разрешает «50 последних событий для org» за два page read. GIN-индекс на prefs jsonb делает WHERE prefs @> '{"theme":"dark"}' поиском за 2 мс вместо 4 с seq-scan.

Забытая стоимость: каждый insert и update пишет во все индексы. Правило leading-column — самый нарушаемый принцип в дикой природе.

Ошибки выбора индексов:

  • B-tree индекс на low-cardinality колонке (gender с 2 значениями, status с 5) почти всегда бесполезен — планировщик выбирает seq-scan, а индекс ест write throughput.
  • Индекс на (a, b) не помогает запросам, фильтрующим только по b. Индекс (user_id, created_at), созданный для «событий пользователя», не помогает «последним событиям любого пользователя». Отдельный (created_at, user_id) удовлетворяет оба запроса, и планировщик выбирает его исходя из запроса.
  • JSONB-индексы (GIN или GIST) мощные, но дорогие на writes — 100K inserts/s могут вдвое снизить write throughput при наивном индексировании JSONB-колонки.
  • Partial indexes решают проблему write throughput для dimension tables: CREATE INDEX ... WHERE status != 'ARCHIVED' покрывает 5% активных строк за долю стоимости.

Цена ошибки здесь постоянная: legacy-приложения наследуют неверный порядок колонок, и любая попытка удалить индекс рискует регрессией в недокументированных code path.

Акт 3 — Статистика: планировщик выбирает планы из гистограмм

Postgres сэмплирует таблицу во время ANALYZE и строит гистограмму на каждую колонку. Планировщик использует гистограммы для оценки количества строк; cost model ранжирует планы. Когда статистика устаревает, оценки неверны.

Ошибка оценки в 1000× даёт план в 1000× медленнее: планировщик выбирает nested loop, ожидая 10 строк; реальная мощность — 10000 строк; цикл сканирует 10M index entries вместо одного hash join по 10K-строчной build side.

Защита:

  • autovacuum_analyze_scale_factor = 0.05 (или меньше, 0.01) на busy tables — при дефолтном 0.1 таблица, растущая на 1M строк в день, имеет устаревшую статистику уже через часы.
  • CREATE STATISTICS (dependencies) ON user_id, created_at FROM events учит планировщика корреляциям колонок (новые пользователи появляются в свежих данных; планировщик по умолчанию считает их независимыми).
  • pg_stat_statements для поиска проблемных планов по total execution time.
  • auto_explain.log_min_duration = '500ms' для triage хвостовой латентности.

Ловушка на масштабе: одна плохая оценка каскадирует. Планировщик кеширует plan IDs (pg_stat_statements.plan_id); человек должен найти и принудительно перепланировать, удалив и пересоздав prepared statement запроса, или выполнив DISCARD PLANS.

Акты 1–3: стоимость и время исправления
Схема: исправить неправильный тип при 1M строк
часы
Схема: исправить неправильный тип при 100M строк
дни + окно обслуживания
Index lookup в shared_buffers (L3 cache)
5–50 мкс
Index lookup с SSD
5–10 мс
Seq-scan таблицы 1 GB на SSD
1–3 с
autovacuum analyze при дефолтном factor 0.1
срабатывает после 100K новых строк
Почему это работает

Лимит work_mem (по умолчанию 4 MB) влияет на сортировки и hash join. Если оценки планировщика неверны и реальные данные требуют больше work_mem, чем доступно, запрос сбрасывается на диск и становится очень медленным. Re-ANALYZE решает проблему оценки; увеличение work_mem — проблему hash-build; ни то ни другое не является долгосрочной заменой корректной статистики.

Викторина

Индекс есть на (user_id, created_at). Запрос фильтрует только по created_at. Что происходит?

Викторина

autovacuum_analyze_scale_factor = 0.1 (дефолт). Таблица растёт на 1M строк в день. Как часто запускается ANALYZE?

Викторина

Добавляешь суррогатный BIGSERIAL id к таблице, которая уже использует email как натуральный ключ. Какова главная выгода на масштабе года 3?

Вспомните перед уходом
  1. 01
    Почему схема описана как 'единственное необратимое решение' и что это означает операционно?
  2. 02
    Объясни правило leading-column и приведи пример, как его нарушение вызывает production incident.
  3. 03
    Что такое каскадный сбой при устаревшей статистике и как autovacuum_analyze_scale_factor и CREATE STATISTICS его исправляют?
Итог

Акты 1–3 — фундамент, на котором строится каждый последующий акт. Схема необратима — неправильные типы и отсутствие суррогатных ключей становятся многодневными миграциями при 100M строк; правило — NOT NULL по умолчанию, FK если нет измеренного throughput pressure, суррогатные ключи для внешних ссылок. Правило leading-column управляет каждым B-tree индексом: индекс на (a, b) не помогает запросам только по b, и неверный выбор навсегда замедляет нужные запросы — исправление это новый индекс, но старый плохой нельзя безопасно удалить без аудита. Статистика деградирует по мере роста таблицы быстрее дефолтного порога autovacuum; защита — меньший autovacuum_analyze_scale_factor на high-growth tables и CREATE STATISTICS для коррелированных колонок. Все три акта объединены одной темой: стоимость правильного решения в День 0 близка к нулю; стоимость исправления неверного решения в Год 3 экспоненциальна.

Связанные уроки
встречается в258
Продолжить восхождение ↑Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграции
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.