PostgreSQL — отличная СУБД. Но её реализация MVCC (Multi-Version Concurrency Control) имеет известные ограничения по сравнению с другими популярными реляционными базами. Это подтверждается исследованиями CMU и практическим опытом компаний вроде Uber.
Что такое MVCC
MVCC позволяет нескольким транзакциям одновременно читать и писать данные без блокировок. Основная идея: вместо перезаписи строк СУБД создаёт новые версии.
- Читатели видят snapshot базы на момент начала транзакции
- Писатели не блокируют читателей
- Не нужны явные блокировки записей
Концепция описана в диссертации David Reed (MIT, 1978). Первая коммерческая реализация — InterBase в 1980-х. Сегодня MVCC используют практически все транзакционные СУБД.
Три ключевых решения при реализации MVCC
- Как хранить обновления — копировать строку целиком или только изменения (delta)
- Как находить нужную версию — порядок цепочки версий
- Как удалять старые версии — когда и как очищать
PostgreSQL принял решения по первому пункту в 1980-х, и это создало проблемы с остальными двумя, с которыми приходится считаться до сих пор.
Как это делает PostgreSQL
Append-only storage
При UPDATE PostgreSQL копирует всю строку в новое место и применяет изменения к копии. Оригинал остаётся на месте.
-- Изменяем один столбец
UPDATE movies SET year = 1983 WHERE name = 'Shaolin and Wu Tang';
-- PostgreSQL:
-- 1. Находит строку в Table Page #1
-- 2. Копирует ВСЮ строку в Table Page #2
-- 3. Применяет изменение year к новой копии
-- 4. Старая версия остаётся в Table Page #1
Version chain (O2N)
Версии связаны в цепочку. PostgreSQL использует порядок oldest-to-newest (O2N): старая версия указывает на новую.
- O2N (PostgreSQL): индексы указывают на старую версию, нужно пройти цепочку до актуальной
- N2O (MySQL, Oracle): индексы указывают на новую версию, быстрый доступ к актуальным данным
HOT updates
Оптимизация: если новая версия помещается на ту же страницу и не затрагивает индексированные столбцы, PostgreSQL не обновляет индексы (Heap-Only Tuple). Это значительно снижает write amplification для подходящих случаев.
По данным OtterTune (анализ ~100 тысяч реальных PostgreSQL-инстансов), только ~46% обновлений используют HOT. Остальные 54% платят полную цену. Причины:
- На странице нет свободного места для новой версии строки (заполнена до fillfactor)
- UPDATE затрагивает хотя бы один индексированный столбец
- Таблица имеет много индексов — вероятность затронуть индексированный столбец выше
Четыре проблемы
Проблема #1: Копирование версий
PostgreSQL копирует всю строку при каждом UPDATE, даже если изменился один столбец.
| СУБД | Подход | UPDATE 1 столбца из 1000 |
|---|---|---|
| PostgreSQL | Append-only (полная копия) | Копирует все 1000 столбцов |
| MySQL, Oracle | Delta versions | Сохраняет только изменение 1 столбца |
Результат: PostgreSQL требует больше памяти и диска для хранения той же базы.
Проблема #2: Table bloat
Мёртвые строки (dead tuples) занимают место до очистки vacuum. При интенсивной записи они накапливаются быстрее, чем vacuum успевает убирать.
Пример: таблица с 10M живых строк и 40M мёртвых (80% — мусор):
- Живые данные: 10 GB
- Мёртвые данные: 40 GB
- PostgreSQL читает при full scan: 50 GB
Даже после очистки VACUUM не возвращает место ОС. Только VACUUM FULL или pg_repack реально уменьшают размер файлов — но они блокируют таблицу.
Проблема #3: Write amplification индексов
При каждом UPDATE (не HOT) PostgreSQL обновляет все индексы таблицы, даже те, которые не связаны с изменёнными столбцами.
-- Таблица movies с индексами:
-- - movies_pkey (PRIMARY KEY на id)
-- - idx_name (на name)
-- - idx_director (на director)
UPDATE movies SET year = 1983 WHERE id = 1;
-- PostgreSQL обновляет ВСЕ ТРИ индекса,
-- хотя year не индексирован
Почему MySQL и Oracle не страдают: их вторичные индексы хранят не физический адрес, а логический идентификатор (primary key). При UPDATE не нужно обновлять вторичные индексы.
Проблема #4: Управление Vacuum
Производительность PostgreSQL критически зависит от autovacuum. Но настроить его правильно сложно:
-
Неадекватные defaults:
autovacuum_vacuum_scale_factor = 20%означает, что для таблицы в 100M строк vacuum запустится только после изменения 20M строк - Блокировка долгими транзакциями: vacuum не может удалить версии, видимые активным транзакциям
- Порочный круг: bloat → медленные запросы → долгие транзакции → блокировка vacuum → больше bloat
Сравнение с MySQL и Oracle
| Аспект | PostgreSQL | MySQL/Oracle |
|---|---|---|
| Хранение версий | Append-only (полная копия) | Delta versions (только изменения) |
| Порядок цепочки | O2N (oldest-to-newest) | N2O (newest-to-oldest) |
| Вторичные индексы | Физический адрес (обновляются при UPDATE) | Логический ID (не обновляются) |
| Очистка | Autovacuum (отдельный процесс) | Встроенная в транзакции |
| Table bloat | Серьёзная проблема | Минимальная |
Что делать
Мониторинг
-- Dead tuples по таблицам
SELECT relname, n_dead_tup, n_live_tup,
round(n_dead_tup::numeric / nullif(n_live_tup, 0) * 100, 2) as dead_ratio
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
-- Когда последний раз был vacuum
SELECT relname, last_vacuum, last_autovacuum, last_analyze
FROM pg_stat_user_tables
ORDER BY last_autovacuum DESC NULLS LAST;
Настройка autovacuum для больших таблиц
-- Для таблицы с 100M строк: vacuum после 1M изменений (1%)
ALTER TABLE large_table SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_analyze_scale_factor = 0.005
);
-- Или абсолютный порог
ALTER TABLE large_table SET (
autovacuum_vacuum_threshold = 1000000
);
Минимизация индексов
- Удаляйте неиспользуемые индексы — они замедляют запись
- Используйте partial indexes где возможно
- Рассмотрите covering indexes для read-heavy запросов
HOT updates
- Оставляйте свободное место на страницах:
fillfactor = 70-90 - Избегайте индексов на часто обновляемых столбцах
ALTER TABLE frequently_updated SET (fillfactor = 80);
Регулярное обслуживание
VACUUM ANALYZEпосле bulk операцийpg_repackдля сжатия таблиц без блокировки (вместо VACUUM FULL)- Мониторинг долгих транзакций
Что улучшается в новых версиях
Сообщество PostgreSQL активно работает над улучшением MVCC:
- PostgreSQL 13: дедупликация B-tree индексов — уменьшение bloat индексов
- PostgreSQL 14: улучшение HOT-цепочек для обновлений, более эффективный autovacuum
- PostgreSQL 16: оптимизация freeze map для ускорения vacuum на больших таблицах
- В разработке: проект zheap (undo-based storage) — радикальная альтернатива append-only подходу, но пока не включён в основную ветку
- Оптимизация запросов PostgreSQL — EXPLAIN ANALYZE, индексы, стратегии
- Масштабирование PostgreSQL — репликация и шардирование