Продолжаем цикл статей с обзором изменений 19 версии. Сегодня о ноябрьском коммитфесте 2025 года.Самое интересное из предыдущих коммитфестов можно прочитать здесь: 2025-07, 2025-09. Читать далее

Продолжаем цикл статей с обзором изменений 19 версии. Сегодня о ноябрьском коммитфесте 2025 года.
Самое интересное из предыдущих коммитфестов можно прочитать здесь: 2025-07, 2025-09.
Планировщик: предварительная группировка
Преобразование COUNT(1) и COUNT(not_null_col) в COUNT(*)
Параллельное сканирование по диапазону идентификаторов строк
COPY … TO с секционированными таблицами
Новая функция error_on_null
Вспомогательные функции планировщика для оптимизации функций, возвращающих набор строк (SRF)
Функции в стиле стандарта SQL с временными объектами
Индексы BRIN: применение интерфейса потокового чтения для очистки
WAIT FOR: ожидание синхронизации реплики с основным сервером
Логическая репликация последовательностей
pg_stat_replication_slots: счетчик превышения лимита памяти для декодирования транзакций
pg_buffercache: распределении буферов по страницам ОС
pg_buffercache: пометить буферы «грязными»
Время сброса статистики для отдельных отношений и функций
Мониторинг объема записанных в WAL полных образов страниц
Новый параметр log_autoanalyze_min_duration
psql: путь поиска в приглашении
psql: вывод логических значений
pg_rewind: пропуск копирования уже имеющихся на целевом сервере сегментов WAL
pgbench: продолжение работы после ошибок в командах SQL
Планировщик: предварительная группировка
commit: 8e11859102f
В следующем запросе большая таблица фактов (routes) соединяется с мелким справочником (airplanes_data). Результат группируется по столбцам справочника.
EXPLAIN (costs off)
SELECT a.airplane_code, a.model, count(*)
FROM routes r
JOIN airplanes_data a USING (airplane_code)
GROUP BY a.airplane_code, a.model;
План запроса в 18-й версии:
QUERY PLAN
--------------------------------------------------------
HashAggregate
Group Key: a.airplane_code
-> Hash Join
Hash Cond: (r.airplane_code = a.airplane_code)
-> Seq Scan on routes r
-> Hash
-> Seq Scan on airplanes_data a
(7 rows)
К этому плану есть серьезная претензия. Таблицы сначала соединяются и только потом выполняется группировка. А ведь можно было бы сначала выполнить группировку по большой таблице и только потом соединить оставшиеся строки со справочником. Такого плана можно добиться, переписав запрос:
EXPLAIN (costs off)
SELECT a.airplane_code, a.model, r.total
FROM (SELECT r.airplane_code, count(*) AS total
FROM routes r
GROUP BY r.airplane_code
) r
JOIN airplanes_data a USING (airplane_code);
QUERY PLAN
--------------------------------------------------
Hash Join
Hash Cond: (a.airplane_code = r.airplane_code)
-> Seq Scan on airplanes_data a
-> Hash
-> Subquery Scan on r
-> HashAggregate
Group Key: r_1.airplane_code
-> Seq Scan on routes r_1
(8 rows)
А теперь план исходного запроса в 19-й версии:
EXPLAIN (costs off)
SELECT a.airplane_code, a.model, count(*)
FROM routes r
JOIN airplanes_data a USING (airplane_code)
GROUP BY a.airplane_code, a.model;
QUERY PLAN
----------------------------------------------------------------------------
Finalize GroupAggregate
Group Key: a.airplane_code
-> Sort
Sort Key: a.airplane_code
-> Nested Loop
-> Partial HashAggregate
Group Key: r.airplane_code
-> Seq Scan on routes r
-> Index Scan using airplanes_data_pkey on airplanes_data a
Index Cond: (airplane_code = r.airplane_code)
(10 rows)
Благодаря новой оптимизации, планировщик сам догадался, что сначала лучше выполнить предварительную группировку по большой таблице, тем самым резко сократить количество строк, и только потом соединить результат со справочником. В конце выполняется финальная группировка результата.
Оптимизацию можно отключить новым параметром enable_eager_aggregate. По умолчанию она включена.
Но что если в результате предварительной группировки количество строк сильно не уменьшится? Тогда оптимизацию лучше не применять. Управлять этим можно через другой параметр min_eager_agg_group_size. Оптимизация сработает, если по оценкам планировщика среднее значение строк в группах будет не меньше, чем значение параметра. По умолчанию 8, т.е. для срабатывания оптимизации исходный набор должен уменьшиться не менее чем в 8 раз.
Преобразование COUNT(1) и COUNT(not_null_col) в COUNT(*)
commit: 42473b3b312
Если в агрегатную функцию COUNT в качестве параметра передать константу или столбец таблицы, объявленный как NOT NULL, то планировщик автоматически заменит вызов на COUNT(*):
EXPLAIN(verbose, costs off)
SELECT count(1), count(seat_no)
FROM seats;
QUERY PLAN
---------------------------------------------------------
Aggregate
Output: count(*), count(*)
-> Seq Scan on bookings.seats
Output: airplane_code, seat_no, fare_conditions
(4 rows)
Это сделано для оптимизации запроса, в частности для избежания ненужных расходов на распаковку строк.
Замена выполняется во вспомогательной функции планировщика для функции COUNT. А в список структур для реализации во вспомогательных функциях добавлена SupportRequestSimplifyAggref. Подробности можно найти в файле src/include/nodes/supportnodes.h
Параллельное сканирование по диапазону идентификаторов строк
commit: 0ca3b16973a
Сканирование по диапазону идентификаторов строк (Tid Range Scan) появилось в 14-й версии. Теперь эта операция может выполняться параллельно несколькими процессами (Parallel Tid Range Scan):
EXPLAIN(costs off)
SELECT count(*)
FROM bookings
WHERE ctid > '(42,0)';
QUERY PLAN
-------------------------------------------------------
Finalize Aggregate
-> Gather
Workers Planned: 2
-> Partial Aggregate
-> Parallel Tid Range Scan on bookings
TID Cond: (ctid > '(42,0)'::tid)
(6 rows)
COPY … TO с секционированными таблицами
commit: 4bea91f21f6, 266543a6205
До 19-й версии в команде COPY … TO нельзя было указать секционированную таблицу:
CREATE TABLE p (id int) PARTITION BY RANGE (id);
CREATE TABLE p1 PARTITION OF p FOR VALUES FROM (0) TO (100);
INSERT INTO p VALUES (42);
18=# COPY p TO stdout;
ERROR: cannot copy from partitioned table "p"
HINT: Try the COPY (SELECT ...) TO variant.
Теперь это работает:
19=# COPY p TO stdout;
42
А заодно такой вариант будет использоваться для начальной синхронизации секционированной таблицы в логической репликации (второй коммит).
Новая функция error_on_null
commit: 2b75c38b707
Если переданное в новую полиморфную функцию значение не является NULL, то возвращается это значение:
SELECT error_on_null(42),
error_on_null(ARRAY[1,NULL]),
error_on_null(ROW());
error_on_null | error_on_null | error_on_null
---------------+---------------+---------------
42 | {1,NULL} | ()
(1 row)
Иначе ошибка:
SELECT error_on_null(NULL::int);
ERROR: null value not allowed
Вспомогательные функции планировщика для оптимизации функций, возвращающих набор строк (SRF)
commit: b140c8d7a3f
Если в запросе вызывается функция на языке SQL, то при определенных условиях запрос из функции может быть подставлен в основной запрос. Это может существенно улучшить план запроса. Но для функций на других языках, например PL/pgSQL, такой возможности нет.
Этот патч добавляет возможность написать для пользовательской функции, возвращающей набор строк, вспомогательную функцию на C (через реализацию структуры SupportRequestInlineInFrom), которая подскажет планировщику, каким запросом можно заменить результат функции. Зная об этом, планировщик сможет подставить в основной запрос результат, полученный от вспомогательной функции.
Вместе с патчем готовые вспомогательные функции не поставляются, их нужно писать самостоятельно для оптимизации конкретных пользовательских функций.
Функции в стиле стандарта SQL с временными объектами
commit: 572c40ba9, 698fa924b11
Представления и функции, ссылающиеся на временные объекты, создаются как временные.
CREATE TEMP TABLE tmp AS SELECT 42 col;
При создании представления будет выдано предупреждение об этом:
CREATE VIEW v_tmp AS SELECT * FROM tmp;
NOTICE: view "v_tmp" will be a temporary view
CREATE VIEW
Однако при создании функции в стиле стандарта SQL такого предупреждения не было. Функция спокойно создавалась в схеме public:
18=# CREATE FUNCTION f_tmp() RETURNS bigint LANGUAGE sql RETURN (SELECT count(*) FROM tmp);
CREATE FUNCTION
18=# \df
List of functions
Schema | Name | Result data type | Argument data types | Type
--------+-------+------------------+---------------------+------
public | f_tmp | bigint | | func
(1 row)
А в новых сеансах пропадала:
18=# \c
You are now connected to database "postgres" as user "postgres".
18=# SELECT f_tmp();
ERROR: function f_tmp() does not exist
LINE 1: SELECT f_tmp();
В 19-й версии при создании функции будет выдаваться предупреждение о том, что функция временная:
19=# CREATE FUNCTION f_tmp() RETURNS bigint LANGUAGE sql RETURN (SELECT count(*) FROM tmp);
NOTICE: function "f_tmp" will be effectively temporary
DETAIL: It depends on temporary table tmp.
CREATE FUNCTION
Индексы BRIN: применение интерфейса потокового чтения для очистки
commit: a6eac2273e6
В 18-й версии интерфейс потокового чтения стал использоваться в том числе и для очистки индексов Btree, GiST и SP-GiST. А теперь и для очистки индексов BRIN.
WAIT FOR: ожидание синхронизации реплики с основным сервером
commit: 447aae13b03
Для распределения нагрузки приложения могут писать данные на основной сервер, а читать с реплики. Но если требуется получить с реплики изменения, только что записанные приложением, то нужен механизм, гарантирующий, что эти изменения уже доступны на реплике. А в случае асинхронной репликации задержки с применением WAL вполне возможны.
И такой механизм появился в виде новой команды SQL WAIT FOR LSN. Принцип работы такой: приложение запоминает текущую позицию LSN на основном сервере после сделанных изменений и дожидается применения этой позиции LSN на реплике при помощи команды WAIT FOR LSN.
На основном сервере создадим таблицу.
primary=# CREATE TABLE test (id int);
primary=# INSERT INTO test VALUES (42);
Запомним текущую позицию LSN. Именно ее применения будем дожидаться на реплике.
primary=# SELECT pg_current_wal_insert_lsn() AS lsn
\gset
Не покидая psql, подключаемся к реплике.
primary=# \connect postgres postgres localhost 5402
replica=# SELECT pg_is_in_recovery();
pg_is_in_recovery
-------------------
t
(1 row)
Для целей демонстрации на реплике настроено отложенное на 30 секунд применение WAL. Поэтому таблица test здесь еще не появилась.
replica=# \dconfig recovery_min_apply_delay
List of configuration parameters
Parameter | Value
--------------------------+-------
recovery_min_apply_delay | 30s
(1 row)
replica=# SELECT * FROM test;
ERROR: relation "test" does not exist
Запускаем команду WAIT FOR LSN и дожидаемся, когда реплика применит запись с запомненным LSN.
replica=# WAIT FOR LSN :'lsn' WITH (MODE 'standby_replay');
status
---------
success
(1 row)
После успешного завершения команды можно быть уверенными, что изменения, сделанные на основном сервере, доступны на реплике.
replica=# SELECT * FROM test;
id
----
42
(1 row)
Логическая репликация последовательностей
commit: b93172ca59f, 96b37849734, f0b3573c3aa, 5509055d695, f6a4c498dcf, 55cefadde87
До сих пор подписчики логической репликации получали только изменения табличных строк. Теперь стало возможным синхронизировать значения последовательностей на сервере подписки со значениями на сервере публикации. Синхронизация выполняется в ручном режиме и для всех последовательностей. Вот как это работает.
Создадим последовательность на сервере публикации:
pub=# CREATE SEQUENCE seq;
Выбрать отдельные последовательности для включения в публикацию (пока?) нельзя, команда CREATE PUBLICATION включает в публикацию все последовательности:
pub=# CREATE PUBLICATION pub FOR ALL SEQUENCES;
Теперь создадим такую же последовательность на подписчике и подпишемся на публикацию:
sub=# CREATE SEQUENCE seq;
sub=# CREATE SUBSCRIPTION sub CONNECTION 'port=5401 dbname=postgres' PUBLICATION pub;
Выбираем несколько значений из последовательности:
pub=# SELECT nextval('seq') FROM generate_series(1,3);
nextval
---------
1
2
3
(3 rows)
Новая функция pg_get_sequence_data показывает информацию о текущем состоянии последовательности:
pub=# SELECT * FROM pg_get_sequence_data('seq');
last_value | is_called | page_lsn
------------+-----------+------------
3 | t | 2/650B51C0
(1 row)
Первые два столбца легко получить, просто выполнив запрос к последовательности, как к таблице. А последний — page_lsn — показывает LSN записи WAL, которая последний раз изменяла значение последовательности.
Состояние последовательности на подписчике можно посмотреть в pg_subscription_rel:
sub=# SELECT srrelid::regclass, srsubstate, srsublsn
FROM pg_subscription_rel;
srrelid | srsubstate | srsublsn
---------+------------+------------
seq | r | 2/650B3270
(1 row)
Если последовательности синхронизированы между серверами, то значение srsublsn должно совпасть с вышеприведенным page_lsn на сервере публикации. Сейчас это не так. Потому что продвижение последовательности на сервере публикации автоматически не переносится на сервер подписки. Требуется ручная синхронизация, которую можно выполнить командой:
sub=# ALTER SUBSCRIPTION sub REFRESH SEQUENCES;
sub=# SELECT srrelid::regclass, srsubstate, srsublsn FROM pg_subscription_rel;
srrelid | srsubstate | srsublsn
---------+------------+------------
seq | r | 2/650B51C0
(1 row)
Другая команда ALTER SUBSCRIPTION ... REFRESH PUBLICATION дополнительно включит в подписку и синхронизирует все новые последовательности, появившиеся на сервере публикации. Работу по синхронизации последовательностей выполняет процесс sequence synchronization worker.
Теперь на подписчике такое же состояние, как и на сервере публикации:
sub=# SELECT * FROM seq;
last_value | log_cnt | is_called
------------+---------+-----------
3 | 0 | t
(1 row)
Если же синхронизация завершилась ошибкой, то узнать об этом можно не только в журнале сервера, но и в представлении pg_stat_subscription_stats. Здесь появился новый столбец со счетчиком ошибок синхронизации последовательностей:
SELECT subname, sync_seq_error_count
FROM pg_stat_subscription_stats
WHERE subname = 'sub';
subname | sync_seq_error_count
---------+----------------------
sub | 0
(1 row)
pg_stat_replication_slots: счетчик превышения лимита памяти для декодирования транзакций
commit: d3b6183dd98
Параметр logical_decoding_work_mem задает объем памяти для логического декодирования транзакций. Если для декодирования требуется больше памяти, часть данных будет сбрасываться на диск. Но как часто это происходит в реальной жизни?
Ответить на этот вопрос поможет новый столбец mem_exceeded_count в представлении pg_stat_replication_slots. Значение столбца — счетчик, показывающий сколько раз не хватало памяти logical_decoding_work_mem и приходилось использовать диск. Эта информация пригодится для настройки logical_decoding_work_mem.
\d pg_stat_replication_slots
View "pg_catalog.pg_stat_replication_slots"
Column | Type | Collation | Nullable | Default
---------------------+--------------------------+-----------+----------+---------
slot_name | text | | |
spill_txns | bigint | | |
spill_count | bigint | | |
spill_bytes | bigint | | |
stream_txns | bigint | | |
stream_count | bigint | | |
stream_bytes | bigint | | |
mem_exceeded_count | bigint | | |
total_txns | bigint | | |
total_bytes | bigint | | |
slotsync_skip_count | bigint | | |
slotsync_last_skip | timestamp with time zone | | |
stats_reset | timestamp with time zone | | |
pg_buffercache: распределении буферов по страницам ОС
commit: 4b203d499c6
Появившееся в 18-й версии представление pg_buffercache_numa показывает распределение каждого буфера общего буферного кеша по узлам NUMA (numa_node), а также по страницам операционной системы (os_page_num):
CREATE EXTENSION pg_buffercache;
SELECT *
FROM pg_buffercache_numa
WHERE bufferid = 42;
bufferid | os_page_num | numa_node
----------+-------------+-----------
42 | 82 | 0
42 | 83 | 0
(2 rows)
В системах без поддержки NUMA это представление недоступно, но информация о распределении буферов по страницам ОС может быть полезной. Теперь ее можно получить из нового представления pg_buffercache_os_pages:
SELECT *
FROM pg_buffercache_os_pages
WHERE bufferid = 42;
bufferid | os_page_num
----------+-------------
42 | 82
42 | 83
(2 rows)
pg_buffercache: пометить буферы «грязными»
commit: 9ccc049dfe6
В 17-й и 18-й версиях в расширение pg_buffercache были добавлены функции для вытеснения буферов из кеша на диск: pg_buffercache_evict, pg_buffercache_evict_relation, pg_buffercache_evict_all.
Теперь, в основном для целей тестирования, добавлены функции для пометки буфера «грязным»: pg_buffercache_mark_dirty, pg_buffercache_mark_dirty_relation, pg_buffercache_mark_dirty_all. Первая помечает грязным один указанный буфер, вторая — все буферы указанного отношения, третья — все буферы кеша.
Время сброса статистики для отдельных отношений и функций
commit: a5b543258aa, b71bae41a0c
Функция pg_stat_reset_single_table_counters позволяет сбросить накопленную статистику для отдельного отношения (таблицы, индекса, …). Аналогичная функция
pg_stat_reset_single_function_counters сбрасывает статистику отдельной функции. Однако дата/время сброса нигде не фиксировались.
Теперь в представления pg_stat_{all|user}_{tables|indexes}, а также pg_stat_user_functions добавлен столбец stats_reset.
SELECT pg_stat_reset_single_table_counters('bookings'::regclass);
pg_stat_reset_single_table_counters
-------------------------------------
(1 row)
SELECT stats_reset
FROM pg_stat_user_tables
WHERE relid = 'bookings'::regclass;
stats_reset
-------------------------------
2026-01-13 22:08:12.686371+03
(1 row)
Мониторинг объема записанных в WAL полных образов страниц
commit: ad25744f436, f9a09aa29520
В общесистемное представление pg_stat_wal и функцию pg_stat_get_backend_wal добавлен столбец wal_fpi_bytes. Он показывает общий размер записанных в WAL полных образов страниц.
CREATE TABLE bookings_copy
AS SELECT * FROM bookings;
SELECT 4905238
SELECT *
FROM pg_stat_get_backend_wal(pg_backend_pid())
\gx
-[ RECORD 1 ]----+----------
wal_records | 9842578
wal_fpi | 160
wal_bytes | 759246449
wal_fpi_bytes | 1014192
wal_buffers_full | 84566
stats_reset |
Также значение full page image bytes добавлено в вывод команд VACUUM и ANALYZE с параметром verbose, а также в журнал сервера:
VACUUM(verbose) bookings_copy;
INFO: vacuuming "demo.bookings.bookings_copy"
…
WAL usage: 31248 records, 4 full page images, 1876574 bytes, 32768 full page image bytes, 0 buffers full
…
VACUUM
Новый параметр log_autoanalyze_min_duration
commit: dd3ae378301
Параметр log_autovacuum_min_duration срабатывал при превышении рабочим процессом автоочистки заданного времени выполнения как очистки, так и сбора статистики.
Теперь каждая из двух операций автоочистки журналируется собственным параметром. log_autovacuum_min_duration теперь относится только к очистке, а новый параметр log_autoanalyze_min_duration к сбору статистики.
\dconfig+ log_auto*_min_duration
List of configuration parameters
Parameter | Value | Type | Context | Access privileges
------------------------------+-------+---------+---------+-------------------
log_autoanalyze_min_duration | 10min | integer | sighup |
log_autovacuum_min_duration | 10min | integer | sighup |
(2 rows)
psql: путь поиска в приглашении
commit: b3ce55f413c
В приглашение psql теперь можно включить путь поиска, точнее, значение параметра search_path. Для этого в переменные PROMPT1 и PROMPT2 нужно добавить %S.
postgres@demo=# \set PROMPT1 '%n%@%/:%S%R%x%# '
postgres@demo:bookings, "$user", public=#
postgres@demo:bookings, "$user", public=# SET search_path = pg_catalog;
SET
postgres@demo:pg_catalog=#
psql: вывод логических значений
commit: 645cb44c549
psql выводит истину и ложь как t и f:
SELECT 1=1, 1<>1;
?column? | ?column?
----------+----------
t | f
(1 row)
Новые переменные команды \pset позволяют переопределить отображение логических значений.
\pset display_true 'True'
\pset display_false 'False'
SELECT 1=1, 1<>1;
?column? | ?column?
----------+----------
True | False
(1 row)
Стоит напомнить, что вывод значений NULL переопределяется через \pset null.
pg_rewind: пропуск копирования уже имеющихся на целевом сервере сегментов WAL
commit: 6ae08d9583e, 5173bfd0443
Работа pg_rewind станет быстрее за счет того, что сегменты WAL, созданные до момента расхождения исходного и целевого серверов, не будут копироваться. В предыдущих версиях копирование сегментов WAL выполнялось всегда, теперь — только если файла нет или размеры файлов отличаются.
pgbench: продолжение работы после ошибок в командах SQL
commit: 0ab208fa505
Утилита pgbench теперь может продолжать работу не только после ошибок сериализации или взаимоблокировок, но и после других ошибок в командах SQL. Это пригодится при работе с собственными скриптами тестирования.
Предположим, у нас есть таблица с первичным ключом:
CREATE TABLE test (id int PRIMARY KEY);
И скрипт для заполнения таблицы произвольными числами:
$ cat load.sql
INSERT INTO test VALUES (random(1,100));
Очевидно, что многократное выполнение скрипта будет приводить к ошибкам уникальности. Чтобы их игнорировать, запустим pgbench с новым параметром --continue-on-error, а параметр --failures-detailed покажет процент ошибочных транзакций в строке number of other failures.
$ pgbench -d postgres -f load.sql --continue-on-error --failures-detailed -t 100
pgbench (19devel)
starting vacuum...end.
transaction type: load.sql
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
number of transactions per client: 100
number of transactions actually processed: 65/100
number of failed transactions: 35 (35.000%)
number of serialization failures: 0 (0.000%)
number of deadlock failures: 0 (0.000%)
number of other failures: 35 (35.000%)
latency average = 0.519 ms (including failures)
initial connection time = 4.194 ms
tps = 1251.974267 (without initial connection time)
Из 100 выполненных транзакций 35 завершились ошибкой, значит, в таблице сейчас 65 записей, что легко проверить:
$ psql -d postgres -c 'SELECT count(*) FROM test'
count
-------
65
(1 row)
На этом пока всё. Впереди события четвертого январского коммитфеста 19-й версии.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | PostgreSQL 19: Часть 2 или Коммитфест 2025-09 | 0 | 15.26 | 19-01-2026 |
| 2 | PostgreSQL 19: Часть 1 или Коммитфест 2025-07 | 0 | 18.73 | 12-01-2026 |
| 3 | PostgreSQL 19: Часть 4 или Коммитфест 2026-01 | 0 | 17.99 | 12-02-2026 |
| 4 | Posstgresso #1 (86) | 0 | 14.37 | 19-02-2026 |
| 5 | Postgresso 4 (89) | 0 | 13.18 | 10-06-2026 |
| 6 | Книга «PostgreSQL 18 изнутри»: архитектура «слона» под новым капотом | 0 | 11.87 | 01-07-2026 |
| 7 | Postgres Pro Enterprise 18: встроенный in-memory кеш и новые горизонты отказоустойчивости | 0 | 16.84 | 27-01-2026 |
| 8 | [Перевод] Проектирование системы хранения POSTGRES | 0 | 8.21 | 15-07-2026 |
| 9 | Вышло 12-е издание книги «Postgres. Первое знакомство» | 0 | 7.59 | 03-04-2026 |
| 10 | PGConf.Nepal 2026 приглашает | 0 | 16.4 | 20-07-2026 |