Если убрать бесконечные споры о расширениях, повседневная работа с PostgreSQL сводится к десятку тем. Знаешь их — и 90% инцидентов, тормозов и «почему прод лёг» разбираются без паники. Ниже — эти десять, с практикой и граблями, а не общими словами.
1. MVCC и VACUUM
PostgreSQL не изменяет строку на месте — при UPDATE/DELETE он создаёт новую версию, а старую помечает «мёртвой» (это и есть MVCC — многоверсионность). Мёртвые версии должен убирать autovacuum. Если он настроен плохо, таблицы и индексы раздуваются (bloat), а задержки ползут вверх неделями — незаметно, пока не станет больно.
Что делать:
- следить за bloat и «возрастом» транзакций (
pg_stat_user_tables, поляn_dead_tup,last_autovacuum); - для горячих таблиц снижать
autovacuum_vacuum_scale_factor(дефолтные 20% на большой таблице — это миллионы мёртвых строк до срабатывания); - не путать
VACUUM(возвращает место под переиспользование) иVACUUM FULL(переписывает таблицу, берётACCESS EXCLUSIVE— на проде почти всегда нельзя); - держать в голове угрозу transaction ID wraparound — если autovacuum не успевает, база в крайнем случае уйдёт в аварийную защиту.
2. Индексы под реальные запросы
Индекс полезен только под конкретный паттерн запроса. Ключевые вещи:
- Порядок колонок в составном индексе решает всё: индекс
(a, b)работает дляWHERE a=…иWHERE a=… AND b=…, но не дляWHERE b=…. - Частичный индекс (
CREATE INDEX ... WHERE status='active') — маленький и быстрый, когда запросы всегда бьют в подмножество. - Покрывающий индекс (
INCLUDE (...)) отдаёт данные из самого индекса — index-only scan без похода в таблицу. - ORM легко незаметно устраивает Seq Scan: функция над колонкой (
WHERE lower(email)=…) убивает обычный индекс — нужен индекс по выражению; неявное приведение типов тоже ломает использование индекса.
Проверяйте гипотезы через EXPLAIN (см. пункт 7), а не «на глаз».
3. Блокировки и DDL
ALTER TABLE может взять ACCESS EXCLUSIVE и заблокировать запись (а иногда и чтение) на всю таблицу. На проде это инцидент.
Безопасные приёмы:
CREATE INDEX CONCURRENTLY— строит индекс, не блокируя запись (дольше, может упасть и оставитьINVALIDиндекс — тогда пересоздать);- добавление колонки с дефолтом в современных версиях дешёвое, но добавление
NOT NULL/constraint — с оглядкой: используйтеNOT VALID+ отдельныйVALIDATE CONSTRAINT; - ставьте
lock_timeoutперед DDL, чтобы миграция не встала в очередь за долгой транзакцией и не заблокировала за собой весь трафик; - большие бэкофилы данных — маленькими батчами, а не одним
UPDATEна всю таблицу.
Помните про очередь блокировок: одна ждущая ACCESS EXCLUSIVE блокирует всех, кто встал за ней, даже читателей.
4. Уровни изоляции и аномалии
Три уровня, которые реально используются: Read Committed (дефолт), Repeatable Read, Serializable.
- пропавшие/задвоенные строки, «странные» суммы — это чаще всего не баг в коде, а гонка конкурентных транзакций;
- воспроизводить такое надо двумя параллельными сессиями, а не чтением кода в одиночку;
Serializableспасает от аномалий сериализации, но требует ретраев на ошибку40001(serialization failure) — приложение должно уметь повторять транзакцию;- классика на Read Committed — потерянное обновление при read-modify-write; лечится
SELECT ... FOR UPDATEили атомарнымUPDATE ... SET x = x + 1.
5. Управление соединениями
Каждое соединение в PostgreSQL — это процесс с своей памятью. Слишком много соединений выжигает CPU и RAM быстрее, чем кажется.
- ставьте PgBouncer (или встроенный пул на стороне приложения); для веб-нагрузки — режим
transaction; - размер пула считайте от числа ядер, а не «побольше»: сотни активных соединений на десятке ядер — это деградация, а не пропускная способность;
- главный тихий убийца —
idle in transaction: транзакция открыта и ничего не делает, но держит блокировки и мешает VACUUM. Ограничьтеidle_in_transaction_session_timeout.
6. WAL, checkpoints и репликация
Любая запись сначала идёт в WAL (журнал упреждающей записи). Отсюда три следствия:
- объём WAL напрямую бьёт по I/O; агрессивные
checkpointдают всплески записи и скачки задержки — настраивайтеmax_wal_sizeиcheckpoint_completion_target, чтобы размазать I/O; - отставание реплик (
replication lag) ломает чтение с реплик (приложение видит устаревшие данные) и делает переключение при сбое рискованным — при большом лаге теряются данные; - следите за лагом (
pg_stat_replication,pg_wal_lsn_diff) и за тем, чтобыwal_keep_size/слоты не переполнили диск (застрявший слот реплики способен забить диск WAL-ом до отказа).
7. Основы планировщика запросов
EXPLAIN (ANALYZE, BUFFERS) — это ваш отладчик, а не украшение. Что читать:
- оценки строк vs факт: если planner ждал 10 строк, а пришло 10 000 — у него неверная статистика, план будет плохой;
- типы JOIN: Nested Loop хорош на малых объёмах и катастрофичен на больших не туда оценённых; Hash/Merge Join — для крупных наборов;
BUFFERSпокажет, сколько реально читалось с диска, а сколько из кэша;- лечение кривых оценок —
ANALYZE, повышениеdefault_statistics_targetдля проблемных колонок или расширенная статистика (CREATE STATISTICS) для коррелирующих колонок.
Читать план надо снизу вверх и изнутри наружу, обращая внимание на узлы с наибольшим actual time и расхождением rows.
8. Наблюдаемость, привязанная к реальным сбоям
Мониторьте не «всё подряд», а то, что предсказывает боль:
- p95/p99 задержки запросов (среднее врёт);
- ожидание блокировок и число заблокированных сессий;
- временные файлы (
temp_files,temp_bytes) — признак того, чтоwork_memмал и сортировки/хеши льются на диск; - cache hit ratio, отставание autovacuum и реплик;
- включите лог медленных запросов (
log_min_duration_statement) с разумным порогом иpg_stat_statementsдля агрегированной картины.
Метрика полезна, если по ней понятно, что чинить, а не просто «красиво».
9. Бэкапы и проверка восстановления
Минимум для прода — базовый бэкап + архивирование WAL (PITR: восстановление на момент времени). Инструменты: pg_basebackup, а лучше pgBackRest/Barman.
Главная ошибка — никогда не проверять восстановление. Во время аварии выясняется, что:
- не хватает ролей/расширений, и база не поднимается;
- восстановление занимает часы, а RTO у вас — минуты;
- бэкап давно бьётся, но никто не смотрел.
Правило простое: бэкап, который ни разу не восстанавливали, — это не бэкап, а надежда. Регулярно прогоняйте restore на отдельном хосте и замеряйте время.
10. Безопасность и права
- принцип наименьших привилегий: приложению — минимально нужные права, никакого superuser;
- отдельные владельцы объектов, приложение работает под ролью без права менять схему;
- в production закройте на запись схему
public(REVOKE CREATE ON SCHEMA public FROM PUBLIC) — иначе любой может создавать там объекты; - регулярная ротация учётных данных, ограничение сетевого доступа (
pg_hba.conf, firewall, TLS); - отдельные роли под миграции и под рантайм — чтобы приложение в обычной работе не могло сделать
DROP TABLE.
Итог
Расширения, экзотические типы индексов и модные надстройки — это последние 10%. Первые 90% надёжности и производительности PostgreSQL держатся на скучных вещах из списка выше: вовремя убранные мёртвые строки, индексы под запросы, аккуратный DDL, понимание изоляции, пул соединений, здоровый WAL и репликация, чтение планов, честная наблюдаемость, проверенные бэкапы и минимальные права. Освойте эти десять — и большинство «магических» проблем перестанут быть магическими.