Продолжаем цикл статей с обзором изменений 19 версии. Сегодня о январском коммитфесте 2026 года.

Самое интересное из предыдущих коммитфестов можно прочитать здесь: 2025-07, 2025-09, 2025-11.


Секционирование: слияние и разделение секций
pg_dump[all]/pg_restore: выгрузка и восстановление расширенной статистики
file_fdw: пропуск нескольких начальных строк
Логическая репликация: включение и отключение логического декодирования WAL без перезапуска сервера
Мониторинг задержек синхронизации слотов логической репликации
pg_available_extensions показывает каталог установки расширения
Новая функция pg_get_multixact_stats: статистика использования мультитранзакций
Улучшения в мониторинге выполнения очистки и анализа
Очистка: информация об использовании памяти
vacuumdb --dry-run
Оптимизация jsonb_agg
Оптимизация LISTEN/NOTIFY
ICU: оптимизация функций преобразования символов
Параметр standard_conforming_strings больше нельзя отключить


Секционирование: слияние и разделение секций
commit: f2e4cc42795, 4b3d173629f

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

Предположим, что секционированная таблица бронирований авиабилетов разбита на секции по месяцам. Сейчас в ней три секции за последние три месяца прошлого года:

SELECT tableoid::regclass AS partition, count(*)
FROM bookings_range
GROUP BY ALL;
 partition | count  
-----------+--------
 p2025_10  | 434287
 p2025_11  | 410680
 p2025_12  | 411025
(3 rows)

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

ALTER TABLE bookings_range
    MERGE PARTITIONS (
        p2025_10,
        p2025_11,
        p2025_12
    )
    INTO p2025;

SELECT tableoid::regclass AS partition, count(*)
FROM bookings_range
GROUP BY ALL;
 partition |  count  
-----------+---------
 p2025     | 1255992
(1 row)

К разбиению по месяцам можно вернуться обратной командой:

ALTER TABLE bookings_range
    SPLIT PARTITION p2025
    INTO (
        PARTITION p2025_10 FOR VALUES FROM ('2025-10-01') TO ('2025-11-01'),
        PARTITION p2025_11 FOR VALUES FROM ('2025-11-01') TO ('2025-12-01'),
        PARTITION p2025_12 FOR VALUES FROM ('2025-12-01') TO ('2026-01-01')
    );

SELECT tableoid::regclass AS partition, count(*)
FROM bookings_range
GROUP BY ALL;
 partition | count  
-----------+--------
 p2025_10  | 434287
 p2025_11  | 410680
 p2025_12  | 411025
(3 rows)

Обе команды получают исключительную блокировку родительской таблицы на все время работы. Поэтому их не следует выполнять в нагруженных системах. Но в определенных ситуациях они могут быть полезны. К тому же наличие команд — это хорошая база для последующих оптимизаций: как по ослаблению уровня блокировок, так и по параллельному выполнению.

pg_dump[all]/pg_restore: выгрузка и восстановление расширенной статистики
commit: d756fa1019f, 0e80f3f88de, c32fb29e979, 302879bd68d, efbebb4e858

В 18-й версии pg_dump и pg_dumpall научились выгружать базовую статистику таблиц и индексов. А psql и pg_restore — ее восстанавливать. Теперь пришел черед всех видов расширенной статистики, включая статистику по выражениям.

В следующем примере создаются три вида расширенной статистики для таблицы:

CREATE TABLE test AS SELECT 1 AS id, 'a' AS descr;

CREATE STATISTICS test_stats (ndistinct, dependencies, mcv)
ON id, descr
FROM test;
ANALYZE test;

Расширенная статистика выгружается при помощи новой функции pg_restore_extended_stats, вызов которой можно найти в выводе pg_dump:

$ pg_dump --statistics-only| grep -A 12 pg_restore_extended_stats
SELECT * FROM pg_catalog.pg_restore_extended_stats(
    'version', '190000'::integer,
    'schemaname', 'public',
    'relname', 'test',
    'statistics_schemaname', 'public',
    'statistics_name', 'test_stats',
    'inherited', 'f'::boolean,
    'n_distinct', '[{"attributes": [1, 2], "ndistinct": 1}]'::pg_ndistinct,
    'dependencies', '[{"attributes": [1], "dependency": 2, "degree": 1.000000}, {"attributes": [2], "dependency": 1, "degree": 1.000000}]'::pg_dependencies,
    'most_common_vals', '{{1,a}}'::text[],
    'most_common_freqs', '{1}'::double precision[],
    'most_common_base_freqs', '{1}'::double precision[]
);

file_fdw: пропуск нескольких начальных строк
commit: 26cb14aea12

В статье об июльском коммитфесте 19-й версии говорилось о том, что при загрузке строк в таблицу командой COPY можно указать, сколько начальных строк пропустить, прежде чем начнутся данные. Теперь такая же возможность появилась в file_fdw.

Логическая репликация: включение и отключение логического декодирования WAL без перезапуска сервера
commit: 67c20979ce7

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

Теперь уровень записи в WAL будет определяться динамически, в зависимости от наличия слотов логической репликации. Как только появляется первый слот, фактический уровень журнала повышается до logical. И наоборот, после удаления последнего слота фактический уровень понизится до replica. Текущее значение фактического уровня журнала показывает новый параметр effective_wal_level, доступный только для чтения.

\dconfig+ *wal_level
                     List of configuration parameters
      Parameter      |  Value  | Type |  Context   |  Access privileges   
---------------------+---------+------+------------+----------------------
 effective_wal_level | replica | enum | internal   | 
 wal_level           | replica | enum | postmaster | postgres=sA/postgres+
                     |         |      |            | alice=sA/postgres
(2 rows)

Создаем слот логической репликации, фактический уровень журнала повышается:

SELECT pg_create_logical_replication_slot('slot', 'pgoutput');
 pg_create_logical_replication_slot 
------------------------------------
 (slot,2/4BB9C340)
(1 row)
\dconfig *wal_level
List of configuration parameters
      Parameter      |  Value  
---------------------+---------
 effective_wal_level | logical
 wal_level           | replica
(2 rows)

Удаляем единственный слот — и ближайшая контрольная точка снизит фактический уровень журнала:

SELECT pg_drop_replication_slot('slot');
CHECKPOINT;

\dconfig *wal_level
List of configuration parameters
      Parameter      |  Value  
---------------------+---------
 effective_wal_level | replica
 wal_level           | replica
(2 rows)

Мониторинг задержек синхронизации слотов логической репликации
commit: 76b78721ca, e68b6adad96, 5db6a344abc

Если ожидаемая синхронизация слотов логической репликации не срабатывает, хочется понять причину задержки. Для этой цели в представление pg_replication_slots добавлен столбец slotsync_skip_reason.

А в представлении pg_stat_replication_slots появились два новых столбца slotsync_skip_count и slotsync_last_skip, показывающие, сколько раз синхронизация слотов была пропущена и когда это случилось в последний раз.

pg_available_extensions показывает каталог установки расширения
commit: f3c9e341cdf

В представления pg_available_extensions и pg_available_extension_versions добавлен столбец location, показывающий каталог, где установлено расширение.

SELECT name, location
FROM pg_available_extensions
LIMIT 5;
       name       | location 
------------------+----------
 pg_surgery       | $system
 dict_int         | $system
 plpython3u       | $system
 seg              | $system
 ltree_plpython3u | $system
(5 rows)

Значение $system — это каталог по умолчанию для системы.

Новая функция pg_get_multixact_stats: статистика использования мультитранзакций
commit: 97b101776ce

Заблокируем одну и ту же строку в режиме KEY SHARE в двух сеансах:

1=# BEGIN;
1=*# SELECT * FROM bookings WHERE book_ref = '2EW1SQ' FOR KEY SHARE;
 book_ref |           book_date           | total_amount 
----------+-------------------------------+--------------
 2EW1SQ   | 2025-09-01 03:00:12.557744+03 |      8125.00
(1 row)
2=# BEGIN;
2=*# SELECT * FROM bookings WHERE book_ref = '2EW1SQ' FOR KEY SHARE;
 book_ref |           book_date           | total_amount 
----------+-------------------------------+--------------
 2EW1SQ   | 2025-09-01 03:00:12.557744+03 |      8125.00
(1 row)

Две транзакции в этих сеансах используют мультитранзакцию для удержания разделяемой блокировки на строку. Функция pg_get_multixact_stats покажет агрегированную статистику использования мультитранзакций:

SELECT *
FROM  pg_get_multixact_stats()
\gx
-[ RECORD 1 ]----+--
num_mxids        | 1
num_members      | 2
members_size     | 0
oldest_multixact | 1

Эта статистика включает:

  • общее количество мультитранзакций, присутствующих сейчас в системе;

  • общее количество членов (транзакций) этих мультиранзакций;

  • объем памяти (в байтах), требуемый для хранения информации в каталоге pg_multixact/members;

  • самый старый номер используемой сейчас мультитранзакции.

Улучшения в мониторинге выполнения очистки и анализа
commit: 0d789520619, ab40db3852d

В представление pg_stat_progress_vacuum добавлены два столбца:

  • mode с возможными значениями normal, aggressive и failsafe;

  • started_bymanual, autovacuum и autovacuum_wraparound.

Столбец started_by появился и в представлении pg_stat_progress_analyze. Здесь у него два варианта значения: manual и autovacuum.

Очистка: информация об использовании памяти
commit: 736f754eed0

Команда VACUUM с параметром verbose выводит новую строку (memory usage) с информацией о том, сколько всего памяти потребовалось для хранения идентификаторов мертвых строк, сколько раз (resets) список идентификаторов не удалось поместить в отведенный лимит памяти (maintenance_work_mem).

VACUUM(verbose) bookings;
INFO:  vacuuming "demo.bookings.bookings"
…
memory usage: dead item storage 0.02 MB accumulated across 0 resets (limit 64.00 MB each)
…
VACUUM

Эта же строка появится в журнале сервера, если включено журналирование автоочистки.

Стоит отметить, в 17-й версии для хранения списка идентификаторов мертвых строк процесс очистки стал использовать сжатое префиксное дерево (radix tree) вместо обычного массива. Благодаря этой прекрасной оптимизации получить значение resets больше 0 стало очень сложно — даже для целей тестирования. 🙂

vacuumdb --dry-run
commit: d107176d27c

По аналогии с некоторыми утилитами сервера, vacuumdb получила параметр --dry-run. С этим параметром утилита покажет, какие команды очистки и анализа предполагается отправить на сервер.

Создадим таблицу в схеме public и посмотрим, что vacuumdb посчитает нужным выполнить для этой схемы:

$ psql -d postgres -c 'CREATE TABLE public.test (id int)'
$ vacuumdb -d postgres -n public --dry-run
vacuumdb: Executing in dry-run mode.
No commands will be sent to the server.
vacuumdb: vacuuming database "postgres"
VACUUM (SKIP_DATABASE_STATS) public.test;
VACUUM (ONLY_DATABASE_STATS);

Оптимизация jsonb_agg
commit: 0986e95161c, b61aa76e458, 005a2907dc3

Функция jsonb_agg стала значительно быстрее. Особенно при работе с типами integer и numeric. Простой замер времени выполнения в 18-й и 19-й версиях:

18=# \timing
18=# SELECT jsonb_agg(g.x) FROM generate_series(1.0,1000000) AS g(x)\g /dev/null
Time: 677,667 ms

В 19-й версии:

19=# SELECT jsonb_agg(g.x) FROM generate_series(1.0,1000000) AS g(x)\g /dev/null
Time: 374,138 ms

Речь об ускорении именно jsonb_agg, просто json_agg и без этой оптимизации работает примерно с такой же скоростью.

Оптимизация LISTEN/NOTIFY
commit: 282b1cde9de

Механизм LISTEN/NOTIFY хорошо работает в ситуации, когда несколько получателей отслеживают уведомления из одного канала. Но производительность механизма страдает, если разные получатели ждут уведомлений из разных каналов. Процесс NOTIFY пытается доставить уведомление получателям всех каналов, а не только тем, кто подписался на канал.

В 19-й версии механизм LISTEN/NOTIFY поддерживает хеш-таблицу каналов и получателей, что позволило в разы увеличить производительность отправки уведомлений.

ICU: оптимизация функций преобразования символов
commit: c4ff35f1044

В базах данных с кодировкой UTF8 оптимизированы функции преобразования регистра символов с правилами сортировки ICU.

18=# SELECT count(upper(passenger_name COLLATE "ru-x-icu"))
FROM tickets;
  count   
----------
 10836563
(1 row)

Time: 3107,008 ms (00:03,107)

В 19-й версии этот же запрос выполняется существенно быстрее:

19=# SELECT count(upper(passenger_name COLLATE "ru-x-icu"))
FROM tickets;
  count   
----------
 10836563
(1 row)

Time: 1815,985 ms (00:01,816)

Параметр standard_conforming_strings больше нельзя отключить
commit: 45762084545

Параметр standard_conforming_strings больше нельзя отключить. А связанный параметр escape_string_warning удален.

Поведение в 18-й версии:

SET standard_conforming_strings = off;
SET escape_string_warning = off;
SELECT 'A\nB';
 ?column? 
----------
 A       +
 B
(1 row)

Т.е. в строке с одинарными кавычками символ \ воспринимается как специальный.

В 19-й версии такого поведения больше не будет:

SET standard_conforming_strings = off;
ERROR:  non-standard string literals are not supported
SET escape_string_warning = off;
ERROR:  unrecognized configuration parameter "escape_string_warning"
SELECT 'A\nB';
 ?column? 
----------
 A\nB
(1 row)

А для использования специальных символов перед строковой константой нужно ставить E:

SELECT E'A\nB';
 ?column? 
----------
 A       +
 B
(1 row)

Можно было бы спокойно пройти мимо этого патча, если бы не 1C. Для работы этой популярной в России (и не только) системы рекомендуется отключить standard_conforming_strings. Вероятно для перехода на 19-ю версию разработчикам 1С и СУБД на основе PostgreSQL придется протестировать работу со строками с учетом этих изменений.


На этом пока всё. Впереди события пятого мартовского коммитфеста 19-й версии.