Команда Go for Devs подготовила перевод обзора PostgreSQL 18. Новый релиз не ограничился громкой подсистемой асинхронного ввода-вывода — он принёс ряд функций, заметных именно в повседневной разработке. Нативная поддержка UUID v7, виртуальные генерируемые столбцы, расширенные возможности RETURNING и новые средства диагностики делают жизнь разработчиков проще и продуктивнее.


PostgreSQL 18 официально вышел 25 сентября 2025 года. Несомненно, самым значимым изменением стала новая подсистема асинхронного ввода-вывода (AIO). Однако в этой статье основное внимание уделено функциям, которые повлияют на повседневную работу разработчиков, начиная с нативной поддержки UUID v7.

Нативная поддержка UUID v7

PostgreSQL 18 представляет функцию uuidv7() для генерации значений UUID версии 7. Эта нативная поддержка практически завершает давние споры о том, использовать ли типы SERIAL/IDENTITY или UUID в качестве первичных ключей. UUIDv7 объединяет лучшее из обоих миров: глобальную уникальность UUID с последовательной природой, что обеспечивает высокую производительность serial ключей.

-- Сгенерировать UUIDv7
SELECT uuidv7();

-- Создать таблицу, используя UUIDv7 в качестве первичного ключа
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    customer_id INT,
    total DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT NOW()
);

Хотя расширения для PostgreSQL могли генерировать UUIDv7, наиболее широко используемое расширение uuid-ossp не реализует седьмую версию UUID. Менее известные расширения, которые поддерживают v7, обычно не поставляются с дистрибутивами PostgreSQL, особенно у облачных провайдеров. Это означало, что разработчикам, желающим использовать UUIDv7, приходилось реализовывать логику генерации в своих приложениях. Нативная поддержка полностью устраняет это препятствие.

Виртуальные генерируемые столбцы

Генерируемые столбцы впервые появились в PostgreSQL 12, но только с параметром STORED, что означало, что сгенерированный столбец хранится при записи и занимает место на диске. Добавление генерируемого столбца STORED приводило к перезаписи всей таблицы.

PostgreSQL 18 представляет параметр VIRTUAL и делает его поведением по умолчанию для генерируемых столбцов. Эти столбцы вычисляют свои значения при чтении, а не при записи. Это более распространенный подход, и то, что он стал по умолчанию, приводит PostgreSQL в соответствие с другими основными базами данных.

-- Create table with virtual generated column
CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    first_name TEXT NOT NULL,
    last_name TEXT NOT NULL,
    salary DECIMAL(10,2),
    -- You can also skip VIRTUAL since it's the default option
    annual_salary DECIMAL(12,2) GENERATED ALWAYS AS (salary * 12) VIRTUAL
);

-- Insert data (generated columns are computed automatically)
INSERT INTO employees (first_name, last_name, salary) VALUES
    ('John', 'Doe', 5000.00),
    ('Jane', 'Smith', 6500.00);

SELECT first_name, last_name, salary, annual_salary
FROM employees;

Преимущества виртуальных столбцов:

  • Экономия места: для производных данных, которым не требуется физическое хранение, виртуальные столбцы исключают избыточное использование диска.

  • Динамические вычисления: некоторые вычисления выгодно выполнять во время выполнения запроса, особенно когда они зависят от текущего состояния системы.

С другой стороны, генерируемые столбцы типа STORED остаются ценными для сложных вычислений, которые редко меняются, когда затраты на производительность при многократных вычислениях перевешивают соображения хранения.

Однако существуют определённые ограничения, как указано в коммите. Наиболее заметное из них заключается в том, что нельзя создавать индексы на генерируемых столбцах типа VIRTUAL.

Кстати, PostgreSQL 18 также добавляет поддержку генерируемых столбцов в логической репликации (коммит, документация). Эта функция доступна только для генерируемых столбцов типа STORED.

Старые и новые значения в RETURNING

PostgreSQL 18 улучшает выражение RETURNING, позволяя получать доступ как к старым, так и к новым значениям строк в DML-операциях. Это устраняет необходимость в отдельных запросах для захвата значений до/после в журналах аудита.

Операции UPDATE

-- Обновить цены и увидеть как старые, так и новые значения
UPDATE products
SET price = price * 1.10
WHERE price <= 99.99
RETURNING
    name,
    old.price AS old_price,
    new.price AS new_price,
    new.price - old.price AS price_change;

Операция INSERT с ON CONFLICT

-- Upsert с отслеживанием изменений
INSERT INTO products (name, price) VALUES ('Widget', 25.00)
ON CONFLICT (name) DO UPDATE SET price = EXCLUDED.price
RETURNING
    name,
    old.price AS previous_price,
    new.price AS current_price,
    (old.price IS NULL) AS is_new_record;

Операции DELETE

-- Отслеживать, что было удалено
DELETE FROM products
WHERE price < 10.00
RETURNING
    old.name AS deleted_product,
    old.price AS deleted_price;

EXPLAIN ANALYZE с BUFFERS по умолчанию

PostgreSQL 18 улучшает команду EXPLAIN для автоматического предоставления более подробной информации о выполнении запросов.

Теперь EXPLAIN ANALYZE по умолчанию включает информацию об использовании буферов, что устраняет необходимость в параметре BUFFERS. Такое поведение по умолчанию помогает разработчикам выявлять проблемы с производительностью ввода-вывода, которые в противном случае остались бы незамеченными:

postgres=# EXPLAIN ANALYZE SELECT * FROM users WHERE age > 25;

-- PostgreSQL 18: BUFFERS включены автоматически
 Seq Scan on users  (cost=0.00..18.75 rows=125 width=64) (actual time=0.029..0.087 rows=178 loops=1)
   Filter: (age > 25)
   Rows Removed by Filter: 89
   Buffers: shared hit=12
 Planning:
   Buffers: shared hit=156 read=3
   I/O Timings: shared read=0.024
 Planning Time: 0.512 ms
 Execution Time: 0.734 ms

pg_get_acl() для проверки привилегий

PostgreSQL 18 представляет функцию pg_get_acl() для программного получения списков контроля доступа (ACL) для объектов базы данных. Если вы когда-либо тратили время на отладку печально известных сообщений ERROR 42501: permission denied, вы оцените наличие унифицированного способа проверки привилегий объектов.

Ранее для устранения проблем с разрешениями требовалось запрашивать различные системные каталоги (pg_class, pg_proc, pg_namespace, pg_attribute) в зависимости от типа объекта, каждый из которых имел свой формат ACL.

Функция pg_get_acl() предоставляет унифицированный интерфейс для получения ACL из любого объекта базы данных, устраняя необходимость запоминать, какой каталог запрашивать для разных типов объектов.

postgres=# SELECT
    (pg_identify_object(s.classid,s.objid,s.objsubid)).*,
    pg_catalog.pg_get_acl(s.classid,s.objid,s.objsubid) AS acl
FROM pg_catalog.pg_shdepend AS s
JOIN pg_catalog.pg_database AS d
    ON d.datname = current_database() AND
       d.oid = s.dbid
JOIN pg_catalog.pg_authid AS a
    ON a.oid = s.refobjid AND
       s.refclassid = 'pg_authid'::regclass
WHERE s.deptype = 'a';
-[ RECORD 1 ]-----------------------------------------
type     | table
schema   | public
name     | testtab
identity | public.testtab
acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}

Хотя эти улучшения не так сенсационны, как новый AIO, они упрощают повседневную разработку — а более понятные интерфейсы, такие как pg_get_acl(), приносят пользу не только разработчикам-людям, но и ИИ-агентам.

Русскоязычное Go сообщество

Друзья! Эту статью подготовила команда «Go for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Go. Подписывайтесь, чтобы быть в курсе и ничего не упустить!

Комментарии (9)


  1. VladimirFarshatov
    30.09.2025 07:31

    А вот про асинхронный ввод-вывод, почитал бы тоже с удовольствием, особенно режим записи при вставке данных в табличку, да ещё и с имеющимися индексами.


    1. Sleuthhound
      30.09.2025 07:31

      AIO работает только на чтение, запись по прежнему работает в синхронном режиме


  1. Akina
    30.09.2025 07:31

    Однако существуют определённые ограничения, как указано в коммите. Наиболее заметное из них заключается в том, что нельзя создавать индексы на генерируемых столбцах типа VIRTUAL.

    Крайне огорчительно. Как раз наибольший профит дают индексируемые виртуальные поля - тут тебе и индекс, и возможность извлечения значения из индексной записи без его вычисления в процессе выполнения. Особенно когда выражение вычисляемого поля - вычислительноёмкое.

    А в нынешней форме это не более чем скрытое представление поверх таблицы, пусть и с прозрачной передачей индексов

    Кстати, ведь поди и статистики по вычисляемым полям - тоже не существует, да?


    1. gryll
      30.09.2025 07:31

      Подскажите, пожалуйста, в чём разница между виртуальными генерируемыми столбцами и конструкцией view?


      1. Akina
        30.09.2025 07:31

        Представление ведь в процессе выполнения может и материализоваться. Кроме того, индекс таблицы может "не подхватиться" планом, если доступ к таблице выполняется через представление.


    1. KivApple
      30.09.2025 07:31

      Если строить по такому значению индекс, то оно будет сохранено куда-то на диск как минимум в индексе. А значит можно воспользоваться STORED.

      VIRTUAL имеет смысл для того, что на диск попадать таки не будет. По сути сахар для VIEW, чтобы какие-то часто используемые вычисляемые данные проще задействовать в запросе.


      1. Akina
        30.09.2025 07:31

        А значит можно воспользоваться STORED.

        Нет. Если воспользоваться STORED и индексацией по нему, то значение выражения будет присутствовать на диске в двух экземплярах. А при индексации по VIRTUAL на диске будет только одна копия значения выражения.

        VIRTUAL имеет смысл для того, что на диск попадать таки не будет. По сути сахар для VIEW, чтобы какие-то часто используемые вычисляемые данные проще задействовать в запросе.

        Вот именно - в нынешней форме это сахар, о чём я и сказал выше. А разрешение индексировать по нему добавит достаточно полезный функционал, если выражение поля тяжёлое. Всё то же, что даёт STORED, но плюс индексация. Просто сравните связывание двух таблиц по вычисляемым полям для STORED и VIRTUAL... и представьте, что STORED к тому же индексированное.


        1. soloma123
          30.09.2025 07:31

          Но ведь индекс всё равно не может быть виртуальным, он реален и хранится на диске. Никто не мешает создать индекс по выражению, совпадающему с выражением в вычисляемом поле VIRTUAL.


          1. Akina
            30.09.2025 07:31

            А вот тут мы натыкаемся на ограничение функциональных индексов - необходимым (хотя и не достаточным) требованием применимости индекса является его точное литеральное соответствие выражению в запросе. А с вычисляемым столбцом, который идентифицируется по правилам идентификации объектов, такой проблемы нет. Да и повторение текста выражения, особенно когда это какое-нибудь многоэтажное безобразие, тоже такое себе...

            Дело в том, что я чаще использую MySQL, а там индекс по виртуальному вычисляемому полю вполне себе может строиться и использоваться. А потому на практике ощутил, что это полезная фича.

            Кстати. Есть и ещё одна достаточно важная, но не реализованная штука. А именно - в принципе отсутствие возможности сокрытия такого вычисляемого (да и невычисляемого тоже) столбца. Опять же так, как это реализовано в MySQL - при обычном SELECT и в других типах запросов без указания конкретных полей скрытое поле НЕ выводится. Для его вывода/использования нужно явное указание его в списке полей. Почему важно? Ну представьте, что у вас SELECT * является источником данных контрола, или вы используете INSERT INTO table VALUES... - и меняете структуру, добавляя поле. Вперёд, начинаем переписывать все такие запросы. Понятно, что это старый косяк - но увы, штука на практике нередкая. И сильно тормозящая именно переписывание имеющегося кода на использование новой фичи.