Завершаем обзор мартовского коммитфеста 19-й версии. В четвертой и последней статье серии рассмотрим изменения, относящиеся к языку SQL и материалам курсов DEV1 и DEV2.

Напоминаю план статей о последнем коммитфесте 19-й версии:

Об изменениях в предыдущих коммитфестах рассказано здесь: 2025-07, 2025-09, 2025-11, 2026-01.

В этом обзоре:

INSERT … ON CONFLICT DO SELECT
commit: 88327092ff0

Команда INSERT … ON CONFLICT DO NOTHING RETURNING корректно обрабатывает конфликт, но не возвращает существующую строку. В следующем примере после добавления новой строки команда ее возвращает:

CREATE TABLE test (
id int PRIMARY KEY,
value numeric(4,2) DEFAULT random()
);
INSERT INTO test VALUES (1)
ON CONFLICT (id) DO NOTHING
RETURNING id, value;
 id | value 
----+-------
  1 |  0.74
(1 row)

Но если строка уже есть, то фраза RETURNING ничего не вернет:

INSERT INTO test VALUES (1)
ON CONFLICT (id) DO NOTHING
RETURNING id, value;
 id | value 
----+-------
(0 rows)

Если приложению требуется, чтобы команда INSERT всегда возвращала строку с указанным идентификатором, то приходилось проверять статус завершения команды и в случае конфликта делать отдельный SELECT. Это лишний запрос, а между двумя командами строку может удалить или изменить другая транзакция.

Новый вариант ON CONFLICT DO SELECT гарантированно возвращает через RETURNING либо новую, либо уже существующую конфликтующую строку:

INSERT INTO test VALUES (1)
ON CONFLICT (id) DO SELECT
RETURNING id, value;
 id | value 
----+-------
  1 |  0.74
(1 row)

Столбцы для выборки по-прежнему перечисляются во фразе RETURNING, а после SELECT можно указать режим блокировки строки (FOR UPDATE, FOR SHARE и др.) и условие WHERE.

COPY FROM: замена ошибочных значений на NULL
commit: 2a525cc97e1

В 17-й версии COPY научилась игнорировать строки с ошибками преобразования формата данных. Для этого в команду нужно добавить параметр ON_ERROR ignore, и ошибочные строки исключаются из загрузки. Теперь ошибочные значения можно заменять на NULL и загружать строку. Ограничения NOT NULL (в том числе у доменов) при этом проверяются: если в такой столбец попадет NULL, команда завершится ошибкой.

CREATE TABLE test (col int);
COPY test FROM STDIN WITH (on_error set_null);
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself, or an EOF signal.
>> 1
>> два
>> 3
>> \.
NOTICE:  in 1 row, columns were set to null due to data type incompatibility
COPY 3
SELECT * FROM test;
 col 
-----
   1
    
   3
(3 rows)

COPY TO: выгрузка данных в формате json
commit: 7dadd38cda9, 4c0390ac53b

Команда COPY теперь может выгружать данные не только в форматах text, csv и binary, но и в формате json:

COPY (SELECT * FROM bookings LIMIT 3) TO STDOUT WITH (format json);
{"book_ref":"2EW1SQ","book_date":"2025-09-01T03:00:12.557744+03:00","total_amount":8125.00}
{"book_ref":"3ZY3O5","book_date":"2025-09-03T03:25:35.278453+03:00","total_amount":7500.00}
{"book_ref":"756UAS","book_date":"2025-09-10T19:24:02.978559+03:00","total_amount":6325.00}

А дополнительный параметр FORCE_ARRAY оборачивает вывод в массив:

COPY (SELECT * FROM bookings LIMIT 3) TO STDOUT WITH (format json, force_array);
[
 {"book_ref":"2EW1SQ","book_date":"2025-09-01T03:00:12.557744+03:00","total_amount":8125.00}
,{"book_ref":"3ZY3O5","book_date":"2025-09-03T03:25:35.278453+03:00","total_amount":7500.00}
,{"book_ref":"756UAS","book_date":"2025-09-10T19:24:02.978559+03:00","total_amount":6325.00}
]

Речь только о выгрузке (COPY TO), а не о загрузке (COPY FROM). Кроме того, с форматом json не поддерживаются некоторые параметры команды, а именно HEADER, DEFAULT, NULL, DELIMITER, FORCE QUOTE, FORCE NOT NULL и FORCE NULL.

jsonpath: новые методы работы со строками
commit: bd4f879a9cd

В язык jsonpath добавили новые методы для работы со строками: lower(), upper(), initcap(), replace(), split_part(), а также btrim(), ltrim() и rtrim():

SELECT jsonb_path_query('"  hello, world!  "', '$.btrim()'),
       jsonb_path_query('"hello, world!"', '$.upper()'),
       jsonb_path_query('"HELLO, WORLD!"', '$.lower()'),
       jsonb_path_query('"hello, world!"', '$.initcap()'),
       jsonb_path_query('"hello, world?"', '$.replace("?","!")'),
       jsonb_path_query('"hello, world!"', '$.split_part(", ",1)')
\gx
-[ RECORD 1 ]----+----------------
jsonb_path_query | "hello, world!"
jsonb_path_query | "HELLO, WORLD!"
jsonb_path_query | "hello, world!"
jsonb_path_query | "Hello, World!"
jsonb_path_query | "hello, world!"
jsonb_path_query | "hello"

Предикат IS JSON с доменными типами
commit: 3b4c2b9db25

Предикат IS JSON раньше не принимал домены, базовым типом которых являются json, jsonb, bytea или text — сервер не определял базовый тип домена.

CREATE DOMAIN js AS jsonb;
18=# SELECT '"Hello, World!"'::js IS JSON;
ERROR:  cannot use type js in IS JSON predicate

Проверка исправлена:

19=# SELECT '"Hello, World!"'::js IS JSON;
 ?column? 
----------
 t
(1 row)

postgres_fdw: удаленные транзакции наследуют режимы READ ONLY и DEFERRABLE
commit: de28140ded8

Удаленная транзакция, открываемая postgres_fdw на внешнем сервере, раньше не наследовала режимы READ ONLY и DEFERRABLE локальной транзакции. Из-за этого локальная транзакция, объявленная как READ ONLY, могла изменить данные на внешнем сервере, например, через вызов функций. А транзакция, объявленная как DEFERRABLE, могла прерваться из-за ошибки сериализации на внешнем сервере.

Теперь эти параметры локальной транзакции передаются на внешний сервер для открытия удаленной транзакции. Это несовместимое изменение с предыдущими версиями: если приложение в транзакциях READ ONLY изменяло данные через postgres_fdw, такие транзакции придется объявить как READ WRITE.

Ограничения целостности: включение/отключение ENFORCED для ограничений CHECK
commit: 342051d73b3

В 18-й версии стало возможным создавать ограничения целостности CHECK как NOT ENFORCED:

CREATE TABLE test (id int);
ALTER TABLE test ADD CONSTRAINT check_id CHECK (id > 0) NOT ENFORCED;

Но изменить ограничение на ENFORCED было нельзя.

18=# ALTER TABLE test ALTER CONSTRAINT check_id ENFORCED;
ERROR:  cannot alter enforceability of constraint "check_id" of relation "test"

Требовалось его удалить и создать заново. В 19-й версии это стало возможным. При переводе в ENFORCED таблица сканируется, чтобы проверить существующие строки:

19=# ALTER TABLE test ALTER CONSTRAINT check_id ENFORCED;
ALTER TABLE

Ограничения целостности: изменение выражения виртуальных столбцов с ограничением CHECK
commit: f80bedd52b1

В 18-й версии появилась поддержка виртуальных вычисляемых столбцов. Например:

CREATE TABLE test (
  id int,
  amount numeric,
  tax numeric GENERATED ALWAYS AS (amount*0.2) VIRTUAL,
  CONSTRAINT check_tax CHECK (tax > 0)
);

Однако изменить выражение для вычисляемого столбца было нельзя, если для этого столбца определено ограничение CHECK:

18=# ALTER TABLE test ALTER COLUMN tax SET EXPRESSION AS (amount*0.22);
ERROR:  ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables with check constraints
DETAIL:  Column "tax" of relation "test" is a virtual generated column.

В 19-й версии это ограничение снято. Таблица при этом не перезаписывается, а только сканируется для проверки ограничений CHECK:

19=# ALTER TABLE test ALTER COLUMN tax SET EXPRESSION AS (amount*0.22);
ALTER TABLE

Преобразование между uuid и bytea и поддержка формата base32hex
commit: 497c1170cb1, ba21f5bf8af

Преобразование между uuid и bytea раньше требовало обходных путей через текстовое представление, а функции encode/decode не поддерживали формат base32hex. Теперь доступно явное приведение типов. Формат base32hex удобен для UUIDv7: строка получается короче шестнадцатеричной и сохраняет порядок сортировки (при использовании правила сортировки C). Значения UUID после конвертации в base32hex дополняются справа знаками «=». Для более компактного представления эти знаки можно обрезать функцией rtrim, ведь функция decode для формата base32hex понимает оба варианта:

WITH cte(val) AS (
  VALUES (uuidv7())
)
SELECT
  val "text",
  val::bytea "bytea",
  encode(val::bytea, 'base32hex') "base32hex",
  rtrim(encode(val::bytea, 'base32hex'),'=') "base32hex trimmed",
  decode(rtrim(encode(val::bytea, 'base32hex'),'='), 'base32hex') "bytea decoded"
FROM cte
\gx
-[ RECORD 1 ]-----+-------------------------------------
text              | 01a013f7-edf0-760d-a654-f2ec6b3a9709
bytea             | \x01a013f7edf0760da654f2ec6b3a9709
base32hex         | 06G17TVDU1R0R9IKUBM6MEKN14======
base32hex trimmed | 06G17TVDU1R0R9IKUBM6MEKN14
bytea decoded     | \x01a013f7edf0760da654f2ec6b3a9709

PL/pgSQL: оптимизация SELECT … INTO
commit: ce8d5fe0e28

В PL/pgSQL обе конструкции делают одно и то же: вычисляют выражение и присваивают значение переменной:

DECLARE
  x int;
BEGIN
  x := 2+2;
  SELECT 2+2 INTO x;
END;

Но делают это по-разному. В первом случае выражение вычисляется прямо в PL/pgSQL, а во втором выполняется полноценный запрос через SPI, что значительно медленнее. Теперь выражения, не обращающиеся к таблицам, вычисляются напрямую, без накладных расходов SPI, если результат присваивается одной переменной. Побочный эффект: такие выражения больше не попадают в статистику pg_stat_statements.

Функции tid_block и tid_offset
commit: df6949ccf7a

Извлечь номер блока и смещение внутри блока из значения ctid раньше можно было через приведение к строке, а затем к типу point:

SELECT ctid,
       (ctid::text::point)[0]::bigint AS block,
       (ctid::text::point)[1]::int    AS offset
FROM bookings LIMIT 1;
 ctid  | block | offset 
-------+-------+--------
 (0,1) |     0 |      1
(1 row)

Что не совсем удобно. Новые функции делают это напрямую:

SELECT ctid,
       tid_block(ctid),
       tid_offset(ctid)
FROM bookings LIMIT 1;
 ctid  | tid_block | tid_offset 
-------+-----------+------------
 (0,1) |         0 |          1
(1 row)

TOAST: алгоритм lz4 для сжатия по умолчанию
commit: 7c1849311e4, 34dfca29343

Исторически для сжатия TOAST используется встроенный в сервер алгоритм pglz. Однако еще в 14-й версии появилась поддержка более эффективного (как по степени сжатия, так и по использованию процессорного времени) алгоритма lz4.

В 19-й версии, если сервер собран с поддержкой lz4, то для сжатия TOAST-таблиц будет использоваться именно этот алгоритм:

SELECT setting ~ '--with-lz4' FROM pg_config() WHERE name = 'CONFIGURE';
 ?column? 
----------
 t
(1 row)
\dconfig default_toast_compression
 List of configuration parameters
         Parameter         | Value 
---------------------------+-------
 default_toast_compression | lz4
(1 row)

На этом обзор изменений 19-й версии завершен. Остается дождаться официального выпуска PostgreSQL 19, запланированного на 29 октября, и не забыть проверить финальный список изменений в Замечаниях к выпуску.

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