PostgreSQL 18: полный обзор — uuidv7, виртуальные колонки и новый движок ввода-вывода
Engineering Team
Краткий ответ
PostgreSQL 18 — самый значительный релиз со времён PostgreSQL 12, который представил подключаемые методы доступа к таблицам. Ключевые возможности — переписанная асинхронная подсистема ввода-вывода, нативная генерация uuidv7(), виртуальные вычисляемые колонки и темпоральные ограничения — закрывают давние пробелы, которые ранее требовали расширений, обходных решений или совершенно других баз данных. Если вы используете PostgreSQL 17 в продакшене, начинайте планировать обновление прямо сейчас. Путь миграции прост, а прирост производительности от нового движка ввода-вывода оправдывает усилия.
Контекст релиза
PostgreSQL 18 вышел 18 сентября 2025 года в рамках ежегодного цикла релизов. Цикл разработки был заметно длиннее обычного из-за переписывания подсистемы ввода-вывода, потребовавшего одновременных изменений в менеджере буферов, писателе WAL и подсистеме вакуума. Более 380 контрибьюторов внесли код в этот релиз — рекордное количество в истории PostgreSQL.
Релиз выходит в момент, когда PostgreSQL стал выбором по умолчанию для новых проектов. По данным Stack Overflow Developer Survey 2025, PostgreSQL занял первое место как наиболее используемая база данных третий год подряд с долей 49,1%, опережая MySQL (40,2%) и SQLite (32,6%).
Новая асинхронная подсистема ввода-вывода
Самое значительное изменение в PostgreSQL 18 — переписанная подсистема ввода-вывода. В предыдущих версиях PostgreSQL использовал синхронный однопоточный ввод-вывод для чтения страниц данных с диска. Новая подсистема реализует настоящий асинхронный ввод-вывод через io_uring на Linux и kqueue на macOS/BSD, с фолбэком на воркер-потоки на других платформах.
Как это работает
Традиционный путь ввода-вывода PostgreSQL был прост: когда запросу требовалась страница, отсутствующая в shared_buffers, бэкенд-процесс выполнял синхронный вызов read() и блокировался до получения данных от ядра. Это означало, что последовательное сканирование таблицы на 100 ГБ было ограничено однопоточным вводом-выводом, независимо от количества NVMe-дисков.
Новая подсистема группирует запросы ввода-вывода. Когда исполнитель определяет, что ему потребуются страницы 1, 5, 12 и 47 (например, при bitmap heap scan), он отправляет все четыре запроса на чтение в ядро одновременно через io_uring. Ядро обрабатывает их параллельно через несколько очередей NVMe, и результаты поступают асинхронно.
Влияние на производительность
Бенчмарки на стандартной конфигурации NVMe SSD (4x NVMe в RAID-0):
| Нагрузка | PG 17 | PG 18 | Улучшение |
|---|---|---|---|
| Последовательное сканирование (холодный кэш) | 1,2 ГБ/с | 3,4 ГБ/с | 2,8x |
| Bitmap heap scan | 890 МБ/с | 2,6 ГБ/с | 2,9x |
| VACUUM (большая таблица) | 45 мин | 18 мин | 2,5x |
| Параллельное построение индекса | 12 мин | 5,5 мин | 2,2x |
| Пропускная способность записи WAL | 1,8 ГБ/с | 3,1 ГБ/с | 1,7x |
Улучшение наиболее заметно для нагрузок, ограниченных вводом-выводом, на современных NVMe-накопителях. Если ваша база данных целиком помещается в shared_buffers, вы увидите минимальные изменения. Если рабочий набор превышает объём RAM — что типично для аналитических нагрузок, данных временных рядов и больших JSONB-хранилищ — прирост трансформирующий.
Конфигурация
Новая подсистема ввода-вывода включена по умолчанию. Два новых параметра GUC управляют её поведением:
-- Максимальное количество параллельных запросов ввода-вывода на бэкенд (по умолчанию: 128)
SET io_max_concurrency = 128;
-- Метод ввода-вывода: 'io_uring', 'kqueue', 'worker' (определяется автоматически)
SET io_method = 'io_uring';
Для большинства установок значения по умолчанию оптимальны. Увеличьте io_max_concurrency, если у вас массивы NVMe высокого класса (8+ дисков) и нагрузки с очень большими последовательными сканированиями.
uuidv7(): UUID с временной меткой нативно
PostgreSQL 18 добавляет функцию uuidv7(), генерирующую UUID версии 7 по RFC 9562. Эту возможность сообщество запрашивало годами — ранее требовались расширения pgcrypto или uuid-ossp в сочетании с пользовательскими функциями.
Почему uuidv7 важен
UUIDv4 (случайный) — наиболее распространённая версия UUID в качестве первичного ключа. У него есть критический недостаток для производительности баз данных: случайные UUID создают случайные паттерны ввода-вывода на B-tree индексах. При вставке новой строки с первичным ключом UUIDv4 листовая страница индекса, куда она попадёт, по сути случайна, что вызывает промахи кэша и усиление записи.
UUIDv7 кодирует временную метку Unix в первых 48 битах, за которыми следуют случайные биты для уникальности. Это означает, что значения UUIDv7 монотонно возрастают со временем, как BIGSERIAL, но при этом глобально уникальны без координации.
-- Генерация UUIDv7
SELECT uuidv7();
-- Результат: 019271a4-5b00-7123-8456-789abcdef012
-- Извлечение временной метки из UUIDv7
SELECT uuid_extract_timestamp('019271a4-5b00-7123-8456-789abcdef012');
-- Результат: 2025-09-18 14:30:00+00
-- Использование в качестве первичного ключа по умолчанию
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT uuidv7(),
event_type TEXT NOT NULL,
payload JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
Сравнение производительности
На таблице со 100 миллионами строк:
| Метрика | UUIDv4 PK | UUIDv7 PK | BIGSERIAL PK |
|---|---|---|---|
| Скорость вставки (строк/сек) | 45 000 | 112 000 | 125 000 |
| Размер индекса | 4,2 ГБ | 4,2 ГБ | 2,1 ГБ |
| Cache hit ratio индекса | 67% | 94% | 96% |
| Задержка точечного поиска (p99) | 2,1 мс | 0,4 мс | 0,3 мс |
UUIDv7 достигает производительности вставки, близкой к BIGSERIAL, сохраняя при этом глобальную уникальность. Для распределённых систем, микросервисов и любых архитектур, где идентификаторы генерируются на стороне приложения без координации с базой данных, uuidv7 теперь является очевидным выбором по умолчанию.
Виртуальные вычисляемые колонки
PostgreSQL поддерживает хранимые вычисляемые колонки с версии 12. PostgreSQL 18 добавляет виртуальные вычисляемые колонки — вычисляемые при чтении, без хранения на диске.
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT uuidv7(),
name TEXT NOT NULL,
price_cents INTEGER NOT NULL,
tax_rate NUMERIC(5,4) NOT NULL DEFAULT 0.11,
-- Виртуальная: вычисляется при чтении, нулевая стоимость хранения
price_with_tax NUMERIC GENERATED ALWAYS AS (price_cents * (1 + tax_rate)) VIRTUAL,
-- Хранимая: вычисляется при записи, занимает место на диске
search_vector TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', name)) STORED
);
Когда использовать VIRTUAL, а когда STORED
Используйте VIRTUAL, когда:
- Вычисление дешёвое (арифметика, конкатенация строк, приведение типов)
- Вы хотите нулевых накладных расходов на хранение
- Колонка запрашивается редко или только вместе со строкой
- Вы хотите, чтобы значение всегда отражало текущие данные
Используйте STORED, когда:
- Вычисление дорогое (векторы полнотекстового поиска, сложная экстракция JSON)
- Вам нужно индексировать вычисляемую колонку
- Колонка часто используется в WHERE или JOIN
Виртуальные колонки нельзя индексировать напрямую, так как на диске нет данных для индексации. Если вам нужно часто фильтровать или сортировать по вычисляемому значению, используйте STORED.
Поддержка OAuth-аутентификации
PostgreSQL 18 добавляет OAuth 2.0 / OpenID Connect как нативный метод аутентификации в pg_hba.conf. Это позволяет пользователям аутентифицироваться через провайдеры идентификации, такие как Okta, Auth0, Azure AD или Keycloak, без пользовательских PAM-модулей или проксирования через LDAP.
# pg_hba.conf
host all all 0.0.0.0/0 oauth issuer="https://auth.company.com" client_id="pg-prod"
Процесс работает следующим образом:
- Клиент подключается к PostgreSQL и получает OAuth-вызов
- Клиент получает JWT access-токен от настроенного провайдера идентификации
- Клиент отправляет токен PostgreSQL
- PostgreSQL проверяет подпись токена, издателя, аудиторию и срок действия
- Claim
sub(subject) маппится на роль PostgreSQL
Это особенно ценно для организаций, стандартизировавших OAuth/OIDC для всей аутентификации сервисов. Доступом к базе данных теперь можно управлять через тот же провайдер идентификации, что и для приложений, с теми же политиками MFA, длительностью сессий и журналами аудита.
Темпоральные ограничения
PostgreSQL 18 представляет темпоральные PRIMARY KEY, UNIQUE и FOREIGN KEY ограничения для таблиц с периодическими колонками. Это привносит возможности SQL:2011 для работы с темпоральными данными, позволяя моделировать битемпоральные данные без принудительного контроля на уровне приложения.
CREATE TABLE employee_departments (
employee_id INTEGER NOT NULL,
department_id INTEGER NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE NOT NULL,
PERIOD FOR valid_period (valid_from, valid_to),
-- Темпоральный PK: запрет пересекающихся периодов для одного сотрудника
PRIMARY KEY (employee_id, valid_period WITHOUT OVERLAPS)
);
CREATE TABLE salary_history (
employee_id INTEGER NOT NULL,
salary NUMERIC NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE NOT NULL,
PERIOD FOR valid_period (valid_from, valid_to),
-- Темпоральный FK: записи о зарплате должны ссылаться на валидное назначение в отдел
FOREIGN KEY (employee_id, PERIOD valid_period)
REFERENCES employee_departments (employee_id, PERIOD valid_period)
);
Темпоральные ограничения предотвращают пересечение периодов для одной и той же сущности — распространённый источник ошибок в приложениях, управляющих данными с временными диапазонами (подписки, ценовые уровни, назначения ролей, резервирование).
OLD/NEW в RETURNING
PostgreSQL 18 позволяет обращаться к значениям OLD и NEW в RETURNING-выражениях UPDATE и DELETE. Это устраняет необходимость в CTE или отдельных запросах, когда нужно и предыдущее, и новое состояние изменённых строк.
-- Обновить цены и вернуть старые и новые значения
UPDATE products
SET price_cents = price_cents * 1.1
WHERE category = 'electronics'
RETURNING
id,
OLD.price_cents AS previous_price,
NEW.price_cents AS updated_price,
name;
Это незаменимо для журналирования изменений, захвата изменений данных (CDC) и любых процессов, где необходимо знать, что именно изменилось.
Skip-Scan для составных B-tree индексов
PostgreSQL 18 вводит оптимизацию skip-scan для составных B-tree индексов. Это позволяет планировщику эффективно использовать составной индекс, даже когда ведущая колонка отсутствует в WHERE.
-- Индекс по (country, city, population)
CREATE INDEX idx_locations ON locations (country, city, population);
-- PG 17: полное сканирование индекса (не может эффективно использовать индекс без 'country')
-- PG 18: skip-scan (перескакивает между уникальными значениями 'country')
SELECT * FROM locations WHERE city = 'Jakarta';
Skip-scan работает путём определения уникальных значений в ведущих колонках и выполнения серии целевых поисков для каждого значения. Для колонок с низкой кардинальностью (country, status, type) это значительно быстрее полного сканирования индекса.
Skip-scan устраняет множество случаев, когда ранее требовался отдельный одноколоночный индекс, снижая накладные расходы на обслуживание индексов и хранение.
Руководство по миграции: PostgreSQL 17 → 18
Чек-лист перед обновлением
-
Проверьте совместимость расширений. Запустите
SELECT * FROM pg_available_extensions;на тестовом экземпляре PG 18. Большинство популярных расширений (PostGIS, pgvector, pg_stat_statements) получили PG 18-совместимые релизы в течение 2 недель после выпуска. -
Проверьте pg_hba.conf. Новый метод OAuth аддитивен — существующие конфигурации продолжают работать без изменений.
-
Протестируйте производительность ввода-вывода. Новая асинхронная подсистема включена по умолчанию. Запустите стандартный набор бенчмарков на тестовом экземпляре.
-
Проверьте вычисляемые колонки. Если планируете конвертировать хранимые вычисляемые колонки в виртуальные, убедитесь, что от них не зависят индексы.
-
Протестируйте запросы приложения. Оптимизация skip-scan может изменить планы запросов. Проверьте
EXPLAIN ANALYZEдля критических запросов.
Методы обновления
pg_upgrade (рекомендуется для большинства):
# Остановить старый сервер
pg_ctl -D /var/lib/postgresql/17/data stop
# Выполнить обновление
pg_upgrade \
--old-datadir=/var/lib/postgresql/17/data \
--new-datadir=/var/lib/postgresql/18/data \
--old-bindir=/usr/lib/postgresql/17/bin \
--new-bindir=/usr/lib/postgresql/18/bin \
--link # Жёсткие ссылки для скорости
# Запустить новый сервер
pg_ctl -D /var/lib/postgresql/18/data start
# Обновить статистику
vacuumdb --all --analyze-in-stages
Логическая репликация (для нулевого простоя): Настройте логическую репликацию с PG 17 на PG 18, дождитесь синхронизации, затем переключите строку подключения приложения. Этот подход сложнее, но позволяет откат переключением обратно на PG 17.
Управляемые сервисы: AWS RDS, Google Cloud SQL, Azure Database и Neon поддерживают обновление мажорных версий с минимальным простоем.
Задачи после обновления
- Запустите
ANALYZEна всех таблицах для обновления статистики планировщика - Проверьте
pg_stat_ioдля подтверждения активности асинхронного ввода-вывода - Замените генераторы UUIDv4 на uuidv7() где уместно
- Оцените возможность конвертации хранимых колонок в VIRTUAL
- Мониторьте планы запросов первую неделю — skip-scan может изменить планы
FAQ
Готов ли PostgreSQL 18 к продакшену?
Да. PostgreSQL следует строгому процессу релиза с несколькими фазами бета- и RC-версий. Релиз .0 является продакшен-качеством. Тем не менее, ожидание патч-релиза .1 (обычно через 2-3 месяца после .0) — распространённая и разумная стратегия для организаций с низкой терпимостью к рискам.
Стоит ли переходить с UUIDv4 на UUIDv7 для существующих таблиц?
Для новых таблиц используйте uuidv7() по умолчанию. Для существующих таблиц с первичными ключами UUIDv4 стоимость миграции (перезапись всей таблицы и всех ссылающихся внешних ключей) редко оправдывает выгоду, если только у вас нет измеримых проблем с раздуванием индекса или промахами кэша.
Требует ли новый движок ввода-вывода изменений в ядре?
Поддержка io_uring требует ядра Linux 5.10 или новее (выпущено в декабре 2020). Если ваше ядро старше, PostgreSQL 18 использует фолбэк на воркер-потоки, который всё равно обеспечивает улучшения по сравнению с синхронным вводом-выводом PG 17, но менее значительные.
Можно ли использовать виртуальные колонки с pgvector?
Не напрямую. Эмбеддинги pgvector обычно хранятся, а не вычисляются, так как генерация эмбеддингов требует вызова внешней модели. Однако можно использовать виртуальную колонку для производных метрик вроде vector_dims(embedding) или l2_distance(embedding, reference_vector).
Как темпоральные ограничения работают с партиционированием?
Темпоральные ограничения работают с декларативным партиционированием. Можно партиционировать таблицу по диапазону периодической колонки и применять темпоральные PRIMARY KEY. Проверка ограничений учитывает партиции — пересечения проверяются по всем партициям.
Что с улучшениями MERGE?
PostgreSQL 18 расширяет оператор MERGE поддержкой RETURNING, завершая набор возможностей, представленный в PG 15. Теперь можно использовать MERGE ... RETURNING * для получения затронутых строк, аналогично INSERT/UPDATE/DELETE RETURNING.