PostgreSQL 18 на практике: асинхронный I/O, uuidv7() и skip scan для бэкенда — PROG-TIME

PostgreSQL 18 на практике: асинхронный I/O, uuidv7() и skip scan для бэкенда

24.06.2026

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 и новые возможности дадут максимальный эффект.