Продолжаем цикл статей с обзором изменений 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-й версии.