PostgreSQL 18 вышел 25 сентября 2025 года и уже добрался до стабильных минорных релизов (18.1), а в июне 2026-го появилась первая бета PostgreSQL 19 — то есть «восемнадцатая» официально стала рабочей версией для продакшена. Это крупный релиз: новая подсистема ввода-вывода, упорядоченные UUID, более умная работа с B-tree индексами и приятные мелочи для разработчиков. В этой статье разберём на практике то, что реально пригодится в бэкенд-проектах на PHP, Laravel и в Docker-окружении.
Асинхронный ввод-вывод (AIO) и параметр io_method
Главная фишка релиза — асинхронная подсистема ввода-вывода. Раньше PostgreSQL полагался на механизмы readahead операционной системы, но ОС не знает специфику доступа к данным базы и не всегда угадывает, что читать дальше. AIO позволяет PostgreSQL выдавать несколько запросов на чтение одновременно, не дожидаясь завершения каждого по очереди. В отдельных сценариях разработчики показывали прирост до 3x при чтении с диска. Поддерживаются последовательные сканы (sequential scan), bitmap heap scan и vacuum.
Поведением управляет новый параметр io_method. Доступны три значения: worker (по умолчанию, переносит ввод-вывод в отдельные процессы), io_uring (нативный асинхронный I/O ядра Linux) и sync (старое синхронное поведение, как до версии 18).
-- посмотреть текущее значение
SHOW io_method;
-- в postgresql.conf
io_method = worker # значение по умолчанию
# io_method = io_uring # требует сборку с поддержкой liburing (Linux)
# io_method = sync # вернуть прежнее поведение
-- сопутствующие параметры тюнинга
SHOW io_combine_limit;
SHOW io_max_combine_limit;
Менять io_method можно только при перезапуске сервера — это не runtime-параметр. Если вы запускаете Postgres в Docker, для io_uring понадобится образ, собранный с liburing, и достаточно свежее ядро на хосте; в большинстве случаев дефолтный worker даёт хороший результат без дополнительной возни.
uuidv7(): упорядоченные UUID в качестве первичных ключей
Случайные UUID (UUIDv4) удобны, но плохо ложатся в B-tree индекс: каждая новая запись попадает в произвольное место индекса, что увеличивает фрагментацию и снижает кэш-локальность. PostgreSQL 18 добавил встроенную функцию uuidv7(), которая генерирует UUID, упорядоченные по времени создания — новые значения всегда «справа», как у автоинкрементного ключа, но без глобального счётчика.
-- старый способ (случайный UUID)
SELECT gen_random_uuid();
-- новый: timestamp-ordered UUID
SELECT uuidv7();
-- uuidv4() добавлен как явный псевдоним gen_random_uuid()
SELECT uuidv4();
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
total numeric(10,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
Для Laravel это означает, что UUID-первичные ключи перестают быть компромиссом по производительности вставок. Если вы используете трейт с UUID, имеет смысл переключить генерацию на серверный uuidv7() через DEFAULT в миграции, а не генерировать ключ в приложении.
Skip scan: больше запросов используют B-tree индексы
Раньше составной индекс по (a, b) почти не помогал запросам, в которых не было условия по первой колонке a. В PostgreSQL 18 появился «skip scan»: планировщик умеет перебирать различающиеся значения ведущих колонок и нырять в индекс для условий по последующим колонкам. Это ускоряет запросы, в которых пропущено условие = на одном или нескольких префиксных столбцах индекса.
CREATE INDEX idx_events_status_created
ON events (status, created_at);
-- до 18: этот запрос часто шёл через seq scan,
-- т.к. нет условия по ведущей колонке status
EXPLAIN ANALYZE
SELECT * FROM events
WHERE created_at >= now() - interval '1 day';
Дополнительно в 18-й версии планировщик научился использовать индекс для условий с OR в WHERE, а также получил улучшения хеш-джойнов и merge join с инкрементальной сортировкой. Отдельно стоит отметить параллельную сборку GIN-индексов — пересоздание больших полнотекстовых индексов теперь заметно быстрее.
Виртуальные генерируемые колонки по умолчанию
Генерируемые колонки в PostgreSQL 18 по умолчанию стали виртуальными: их значение вычисляется в момент чтения, а не хранится на диске. Это экономит место и ускоряет запись, когда выражение дешёвое.
CREATE TABLE products (
price numeric(10,2) NOT NULL,
vat_rate numeric(4,2) NOT NULL DEFAULT 20,
-- VIRTUAL теперь подразумевается по умолчанию
price_with_vat numeric(12,2)
GENERATED ALWAYS AS (price * (1 + vat_rate / 100)) VIRTUAL
);
-- если значение нужно физически хранить и индексировать:
-- ... GENERATED ALWAYS AS (...) STORED
Помните: индексировать напрямую можно только STORED-колонки. Если вы раньше полагались на хранимые генерируемые колонки, явно указывайте STORED в миграциях, чтобы не получить сюрприз при апгрейде. Приятный бонус: хранимые генерируемые колонки теперь умеют логически реплицироваться.
RETURNING с OLD и NEW
Раньше RETURNING возвращал только итоговое состояние строки. В PostgreSQL 18 в INSERT, UPDATE, DELETE и MERGE можно обращаться к предыдущему (OLD) и текущему (NEW) значениям — то, для чего раньше приходилось писать триггеры или делать дополнительный SELECT.
UPDATE accounts
SET balance = balance - 100
WHERE id = 42
RETURNING OLD.balance AS before,
NEW.balance AS after;
Это особенно удобно для аудита и для логики, где нужно знать, как именно изменилось значение, в рамках одного запроса без гонок.
Темпоральные ограничения и улучшения EXPLAIN
PostgreSQL 18 добавил темпоральные ограничения — ограничения над диапазонами. Для PRIMARY KEY и UNIQUE используется конструкция WITHOUT OVERLAPS, а для внешних ключей — PERIOD. Это позволяет базе гарантировать, что, например, периоды бронирования одного ресурса не пересекаются.
CREATE TABLE room_bookings (
room_id int,
during tsrange,
EXCLUDE USING gist (room_id WITH =, during WITH &&),
PRIMARY KEY (room_id, during WITHOUT OVERLAPS)
);
Полезны и улучшения наблюдаемости. Теперь EXPLAIN ANALYZE автоматически показывает количество обращений к буферам, число index lookups при index scan, а EXPLAIN ANALYZE VERBOSE добавляет статистику по CPU, WAL и среднему чтению. Для диагностики медленных запросов это серьёзное подспорье.
Что учесть при апгрейде
Самое заметное улучшение для эксплуатации: pg_upgrade теперь сохраняет статистику планировщика при мажорном обновлении. Раньше после апгрейда план запросов «тупел», пока не отработает ANALYZE — теперь кластер быстрее выходит на штатную производительность. Добавлены флаги --jobs для параллельных проверок и --swap для обмена директориями вместо копирования.
Несколько важных моментов: кластеры, инициализированные через initdb в 18-й версии, по умолчанию включают контрольные суммы страниц; md5-аутентификация объявлена устаревшей — переходите на SCRAM-SHA-256; а из-за смены провайдера коллаций для полнотекстового поиска после pg_upgrade может потребоваться переиндексация FTS- и pg_trgm-индексов.
Итоги
PostgreSQL 18 — это не косметический релиз, а серьёзный шаг вперёд: асинхронный ввод-вывод даёт ускорение чтения без изменения кода, uuidv7() снимает извечный компромисс с UUID-ключами, skip scan заставляет работать индексы там, где раньше был seq scan, а RETURNING OLD/NEW и темпоральные ограничения убирают часть ручной логики из приложения. Для PHP- и Laravel-проектов миграция на 18-ю версию почти полностью прозрачна, а выгоду по производительности и удобству вы получаете сразу. Начать стоит с тестового стенда: проверьте io_method, прогоните EXPLAIN ANALYZE на горячих запросах и оцените, где skip scan и новые возможности дадут максимальный эффект.