Перейти к основному содержимому
ИнженерияMar 28, 2026

PostgreSQL 18: полный обзор — uuidv7, виртуальные колонки и новый движок ввода-вывода

OS
Open Soft Team

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 17PG 18Улучшение
Последовательное сканирование (холодный кэш)1,2 ГБ/с3,4 ГБ/с2,8x
Bitmap heap scan890 МБ/с2,6 ГБ/с2,9x
VACUUM (большая таблица)45 мин18 мин2,5x
Параллельное построение индекса12 мин5,5 мин2,2x
Пропускная способность записи WAL1,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 PKUUIDv7 PKBIGSERIAL PK
Скорость вставки (строк/сек)45 000112 000125 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"

Процесс работает следующим образом:

  1. Клиент подключается к PostgreSQL и получает OAuth-вызов
  2. Клиент получает JWT access-токен от настроенного провайдера идентификации
  3. Клиент отправляет токен PostgreSQL
  4. PostgreSQL проверяет подпись токена, издателя, аудиторию и срок действия
  5. 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

Чек-лист перед обновлением

  1. Проверьте совместимость расширений. Запустите SELECT * FROM pg_available_extensions; на тестовом экземпляре PG 18. Большинство популярных расширений (PostGIS, pgvector, pg_stat_statements) получили PG 18-совместимые релизы в течение 2 недель после выпуска.

  2. Проверьте pg_hba.conf. Новый метод OAuth аддитивен — существующие конфигурации продолжают работать без изменений.

  3. Протестируйте производительность ввода-вывода. Новая асинхронная подсистема включена по умолчанию. Запустите стандартный набор бенчмарков на тестовом экземпляре.

  4. Проверьте вычисляемые колонки. Если планируете конвертировать хранимые вычисляемые колонки в виртуальные, убедитесь, что от них не зависят индексы.

  5. Протестируйте запросы приложения. Оптимизация 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 поддерживают обновление мажорных версий с минимальным простоем.

Задачи после обновления

  1. Запустите ANALYZE на всех таблицах для обновления статистики планировщика
  2. Проверьте pg_stat_io для подтверждения активности асинхронного ввода-вывода
  3. Замените генераторы UUIDv4 на uuidv7() где уместно
  4. Оцените возможность конвертации хранимых колонок в VIRTUAL
  5. Мониторьте планы запросов первую неделю — 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.