PostgreSQL¶
Раздел 02 · Механизмы · Сравнения
Тип материала: архитектурный разбор. Схемы и примеры иллюстративные, если не указано иное.
Область версий: PostgreSQL 17. Проверка источников: 2026-11-02.
1. Архитектура и модель данных¶
FACT. Реляционный сервер с heap tables, процессами backend, shared buffers и WAL. Владелец записи — primary; read replica не становится независимым источником команд. SQL-транзакция охватывает таблицы одной базы, но отправка Kafka-сообщения за её пределами требует отдельного протокола. Это физическая реализация, совместимая с Event Sourcing и CQRS, а не замена этим паттернам.
flowchart LR
Client --> Transaction[SQL transaction]
Transaction --> WAL[WAL flush]
Transaction --> Heap[Heap and indexes]
WAL --> Standby[Physical standby]
WAL --> CDC[Logical decoding]
2. Физическое хранение и путь записи¶
INSERT создаёт tuple в heap и записи WAL. UPDATE обычно создаёт новую версию; старая остаётся для snapshots. Системные xmin/xmax обозначают транзакции создания/удаления или блокировки: xmax не всегда означает уже удалённую строку, а видимость нельзя определять простым сравнением этих двух чисел. HOT update возможен при подходящих индексах и свободном месте на странице; он сокращает обновления индексов, но не отменяет версионность.
VACUUM отмечает доступное место, очищает ненужные версии и поддерживает visibility map; обычный VACUUM обычно не возвращает весь свободный объём ОС. VACUUM FULL переписывает таблицу с сильной блокировкой. Freeze защищает от transaction ID wraparound. Длинная транзакция, старый snapshot или replication slot удерживают горизонт очистки и могут вызвать bloat/рост WAL.
Источники механизма: документация 2, документация 3.
3. Индексы и путь чтения¶
B-tree обслуживает equality/range и сортировку; уникальный индекс обеспечивает уникальность с учётом конкурентных транзакций. BRIN хранит сводки диапазонов страниц: полезен при корреляции времени с физическим порядком, требует recheck и не заменяет точечный B-tree. GIN индексирует составные значения, например элементы JSONB/массивов, с ценой записи и обслуживания pending list. GiST — расширяемый framework, в том числе для spatial/range и exclusion constraints.
SELECT идёт через planner к scan heap/index; index-only scan всё равно проверяет heap, если visibility map не подтверждает all-visible. В EXPLAIN ANALYZE сравнивают estimates и actual rows, loops, buffers, spill и filter selectivity. ANALYZE действительно выполняет запрос, включая изменяющий данные: сначала изолированная среда; rollback не отменяет все внешние эффекты функций.
Источники механизма: документация 6, документация 8.
4. Транзакции и consistency guarantees¶
READ COMMITTED использует snapshot каждого statement; REPEATABLE READ — snapshot транзакции, предотвращает phantom reads в PostgreSQL, но допускает write skew. SERIALIZABLE использует SSI, может завершить транзакцию serialization failure; повторяют всю бизнес-транзакцию с ограниченным backoff. READ UNCOMMITTED работает как READ COMMITTED.
COMMIT при обычном synchronous_commit ждёт локальный WAL flush; синхронная реплика зависит от synchronous_standby_names и режима ожидания. fsync=off и asynchronous commit меняют последствия сбоя. ACID одной базы не даёт exactly-once внешней доставки.
Источники механизма: документация 7.
5. Репликация, partitioning и sharding¶
Streaming replication передаёт WAL и может быть async/sync. Logical replication передаёт выбранные изменения строк; требует replica identity для нужных UPDATE/DELETE, не переносит автоматически DDL и состояние sequences. В PG17 logical slot failover требует специальной настройки и контроля синхронизации, не возникает от наличия standby.
Declarative RANGE/LIST/HASH partitioning — несколько локальных таблиц и pruning, не распределённое sharding. UNIQUE/PRIMARY KEY на partitioned table должен включать все partition keys. Встроенного прозрачного multi-node write sharding нет; расширения и приложение добавляют отдельные гарантии и расходы.
Источники механизма: документация 5, документация 9.
6. Конкурентная запись и конфликты¶
SELECT FOR UPDATE сериализует изменение баланса; блокировать несколько счетов следует в одном порядке. Проверка баланса отдельным SELECT без блокировки и последующий UPDATE дают гонку. Альтернатива — условный UPDATE ... WHERE balance >= amount и проверка числа строк, если весь инвариант действительно локален.
Deadlock возникает при цикле ожидания: сервер прерывает одну транзакцию; клиент повторяет её целиком. Держать row locks во время HTTP-вызова опасно. Уникальный idempotency key должен проверять также неизменность payload: повтор с другой суммой нельзя молча признать успешным.
Источники механизма: документация 10.
7. Отказы и восстановление¶
После crash WAL redo восстанавливает изменения страниц; незакоммиченные версии не становятся видимыми. PITR требует согласованного base backup и непрерывной WAL-цепочки до целевого момента. Реплика не спасает от логически ошибочного DELETE, а slot может заполнить диск primary.
Для failover сначала fencing старого primary, затем promotion, маршрутизация и сверка подтверждённых операций. Потерянный ответ COMMIT — ambiguous outcome: искать idempotency key после восстановления, а не выполнять перевод с новым ключом.
Источники механизма: документация 4, документация 11.
8. Масштабирование и ограничения¶
Scale-up, оптимизация запросов, pooling, read replicas и partition pruning увеличивают доступную ёмкость разными путями. Один горячий счёт остаётся точкой сериализации; больше соединений не делает его параллельным. Sharding по клиенту осложняет переводы между клиентами. Ограничения измеряют на размерах working set, WAL bandwidth, storage latency и lock contention, а не по универсальному TPS.
9. Performance trade-offs¶
Индексы ускоряют чтение ценой WAL, page splits и write amplification. Fillfactor может помочь HOT, но расходует место. Partitioning ускоряет pruning/drop старых диапазонов, но слишком много partitions увеличивает planning/DDL costs. JSONB удобен для переменных атрибутов, однако не заменяет типы и constraints финансового ядра.
10. Мониторинг и эксплуатация¶
Наблюдать pg_stat_activity, pg_locks, pg_stat_statements (расширение), pg_stat_user_tables, pg_stat_wal, pg_replication_slots и pg_stat_replication. Проверять wait events, age(datfrozenxid), dead tuples, autovacuum progress, slot retained WAL, checkpoint pressure, replica replay lag и свободное место. Названия метрик и права зависят от версии.
Runbook: найти удерживающую транзакцию → оценить влияние её отмены → устранить причину → VACUUM/ANALYZE по необходимости; не лечить bloat отключением autovacuum. Restore drill должен проверять бизнес-инварианты, а не только успешный старт сервера.
11. Сценарии FinTech и iGaming¶
INFERENCE. Ledger, settlement state, лимиты и idempotency registry хорошо соответствуют локальным ACID constraints. Financial Ledger показывает две проводки, атомарный balance cache и audit trail. Outbox вставляется в той же транзакции, relay читает committed изменения и доставляет at-least-once; downstream хранит event_id. CDC — наблюдение изменений, а не самостоятельный business event log.
12. Когда не выбирать¶
Не выбирать единственный PostgreSQL primary как универсальное хранилище бесконечного event firehose и full-history OLAP без расчёта retention/IO. Для глобальной записи с независимым прогрессом при сетевом разрыве single-primary модель не подходит. Распределённый ledger требует явной границы счетов, протокола перевода и reconciliation.
13. Вопросы на собеседовании¶
Почему xmin нельзя использовать как вечный business version? XID имеет wraparound/freeze semantics и не является бизнес-последовательностью.
Почему REPEATABLE READ не гарантирует лимит суммы по нескольким строкам? Два snapshot могут обновить разные строки и совместно нарушить предикат; нужны общий lock/constraint или SERIALIZABLE с retry.
Почему outbox может дублировать событие? Relay может отправить его и упасть до фиксации собственного offset; уникальный event_id и идемпотентный consumer закрывают повтор.
14. Практические SQL/config примеры¶
Полный учебный SQL: ledger.sql. Состояние проверки SQL и отсутствие PostgreSQL runtime зафиксированы в отчёте (архив рабочего проекта).
EXPLAIN (ANALYZE, BUFFERS)
SELECT account_id, balance_minor FROM ledger.account
WHERE tenant_id = 1 AND account_id = 1;
SELECT slot_name, restart_lsn, confirmed_flush_lsn
FROM pg_replication_slots;
Запуск EXPLAIN предполагает созданную схему из примера. Отдельное упражнение: удержать snapshot, выполнить много UPDATE в другом соединении и сопоставить dead tuples с VACUUM; затем воспроизвести deadlock двух переводов. Эти конкурентные эксперименты в текущей среде не выполнены.
15. Дополнительные источники¶
- Первичная документация 1
- Первичная документация 2
- Первичная документация 3
- Первичная документация 4
- Первичная документация 5
- Первичная документация 6
- Первичная документация 7
- Первичная документация 8
- Первичная документация 9
- Первичная документация 10
- Первичная документация 11
Политика версий и ограничений: реестр.
Типичные ошибки проектирования¶
Путать local commit с гарантией failover без потери; удерживать snapshots часами; считать partitioning sharding; выполнять ledger и outbox в разных транзакциях.
Практические упражнения¶
Составьте таблицу исходов: commit применён/ответ потерян/relay повторил событие. Для каждого укажите, какой constraint или retry protocol сохраняет баланс. Полный сценарий — Ledger.