PostgreSQL 19: Коммитфест 2026-03 Часть 5.2 (DBA1, DBA2)


Продолжаем обзор мартовского коммитфеста 19-й версии. Во второй статье серии рассмотрим изменения, относящиеся к материалам курсов DBA1 и DBA2.
Напоминаю план статей о последнем коммитфесте 19-й версии:
Часть 5.2 (DBA1, DBA2) – мы здесь 🙂
Часть 5.3 (DBA3, DBS)
Часть 5.4 (SQL, DEV1, DEV2)
Об изменениях в предыдущих коммитфестах рассказано здесь: 2025-07, 2025-09, 2025-11, 2026-01.
В этом обзоре:
Уточнение предупреждения о приближении зацикливания счетчика транзакций
Журналирование причины запуска контрольной точки в сообщении о завершении
Время сброса статистики для pg_stat_database_conflicts, pg_statio_all_sequences
REPACK: перезапись таблицы с освобождением места на диске
commit: ac58465e061, 28d534e2ae0
Команды CLUSTER и VACUUM FULL решают проблему излишнего разрастания таблиц. Они перезаписывают таблицу и ее индексы в новые файлы без «мертвых» строк, тем самым возвращая свободное место операционной системе. Отличие между ними в том, что CLUSTER перезаписывает таблицу, физически упорядочивая строки в соответствии с указанным индексом.
Новая команда REPACK объединяет возможности VACUUM FULL и CLUSTER под более понятным именем:
REPACK bookings; -- замена VACUUM FULL
REPACK bookings USING INDEX bookings_pkey; -- замена CLUSTER
В таком варианте запуска REPACK, как и CLUSTER/VACUUM FULL, захватывает исключительную блокировку на все время работы, что обычно неприемлемо в активно работающей системе.
Вторым коммитом в команду REPACK добавлен параметр CONCURRENTLY, позволяющий перестраивать таблицу без остановки работы приложений, пишущих в эту таблицу. В таком режиме команда создает файлы с копией таблицы и индексов и при помощи логического декодирования запоминает изменения, которые были сделаны за время ее работы. В конце применяет эти изменения и запрашивает кратковременную исключительную блокировку для замены старых файлов на новые.
REPACK(concurrently) bookings;
REPACK(concurrently) имеет ряд серьезных ограничений. Например команда удерживает горизонт транзакций на все время своей работы, что не дает [авто]очистке удалять мертвые строки из других таблиц. Не работает по секционированной таблице целиком, но работает по отдельным секциям. У таблицы должен быть первичный ключ или уникальный ключ по обязательным для заполнения столбцам (replica identity по первичному или уникальному ключам). Также нельзя перезаписывать таблицы системного каталога или таблицы TOAST.
Часть ограничений, возможно, будет снята в будущих версиях. Но сейчас стоит внимательно с ними ознакомиться в документации.
См. такжe
REPACK в PostgreSQL 19: перепаковка в ядре и, как всегда, дьявол в деталях (Алексей Лесовский).
Автоочистка: порядок обработки таблиц рабочими процессами
commit: d7965d65fc5, 87f61f0c828
Рабочий процесс автоочистки должен обработать все отношения базы данных, требующие очистки или анализа. Есть пять критериев для попадания в список для обработки и они не изменились:
возраст самой старой транзакции,
возраст самой старой мультитранзакции,
количество мертвых строк,
количество добавленных строк,
количество изменений после последнего анализа.
Но в каком порядке рабочий процесс автоочистки должен обрабатывать отношения из этого списка? Раньше порядок был произвольный: рабочий процесс читал pg_class, вычислял критерии и на их основе составлял список таблиц и материализованных отношений для обработки. Затем второй раз читал pg_class и добавлял в список требующие обработки таблицы TOAST. А затем начинал обработку в том порядке, в каком отношения попали в список.
В 19-й версии появилась простая система приоритизации обработки таблиц. Рабочий процесс автоочистки для каждого отношения рассчитывает специальный балл по каждому из вышеперечисленных критериев. Наибольший из пяти баллов становится общим баллом для отношения.
Для упрощения расчетов отключим параметры, определяющие долю строк (autovacuum_scale_factor). Включение отношения в обработку будет определяться только параметрами autovacuum_threshold. Также увеличим интервал запуска рабочих процессов автоочистки, чтобы успеть посмотреть на расчеты:
ALTER SYSTEM SET autovacuum_analyze_scale_factor = 0;
ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0;
ALTER SYSTEM SET autovacuum_vacuum_insert_scale_factor = 0;
ALTER SYSTEM SET autovacuum_naptime = '1h';
SELECT pg_reload_conf();
Теперь создадим таблицу и добавим в нее 2000 строк.
CREATE TABLE test1 (id int);
INSERT INTO test1 SELECT g.x FROM generate_series(1,2000) AS g(x);
Новое представление pg_stat_autovacuum_scores показывает общий балл (score) и расчетные баллы по каждому критерию. Последние три столбца показывают логические признаки — требует ли таблица очистки, анализа или заморозки:
SELECT relname,
round(score) score,
round(xid_score) xid_score,
round(mxid_score) mxid_score,
round(vacuum_score) vacuum_score,
round(vacuum_insert_score) vacuum_insert_score,
round(analyze_score) analyze_score,
do_vacuum,
do_analyze,
for_wraparound
FROM pg_stat_autovacuum_scores
WHERE schemaname = 'public' AND relname ~ 'test'
\gx
-[ RECORD 1 ]-------+------
relname | test1
score | 40
xid_score | 0
mxid_score | 0
vacuum_score | 0
vacuum_insert_score | 2
analyze_score | 40
do_vacuum | t
do_analyze | t
for_wraparound | f
Для таблицы сработали два критерия:
vacuum_insert_score = 2 2000 строк в таблице в 2 раза превышает autovacuum_vacuum_insert_threshold(1000).
analyze_score = 40 2000 строк в 40 раз превышает autovacuum_analyze_threshold(50).
Общий балл для таблицы берется как наибольший из индивидуальных, соответственно он равен 40 (score).
В другую таблицу добавим 1000 строк и половину сразу удалим, чтобы в таблице было 500 мертвых строк:
CREATE TABLE test2 (id int);
INSERT INTO test2 SELECT g.x FROM generate_series(1,1000) AS g(x);
DELETE FROM test2 WHERE id > 500;
Посмотрим на рейтинги очистки и анализа обеих таблиц:
SELECT relname,
round(score) score,
round(vacuum_score) vacuum_score,
round(vacuum_insert_score) vacuum_insert_score,
round(analyze_score) analyze_score
FROM pg_stat_autovacuum_scores
WHERE schemaname = 'public' AND relname ~ 'test'
ORDER BY score DESC;
relname | score | vacuum_score | vacuum_insert_score | analyze_score
---------+-------+--------------+---------------------+---------------
test1 | 40 | 0 | 2 | 40
test2 | 30 | 10 | 1 | 30
(2 rows)
У второй таблицы 10 баллов по мертвым строкам, а по анализу 30. Итоговый балл получился ниже, чем у первой таблицы.
Но что, если мы хотим, чтобы таблицы с большим количеством мертвых строк имели приоритет в обработке? Для этого появился новый параметр autovacuum_vacuum_score_weight, представляющий собой мультипликатор веса критерия:
ALTER SYSTEM SET autovacuum_vacuum_score_weight = 10;
SELECT pg_reload_conf();
Теперь расчетный vacuum_score умножается на 10, а таблица test2 будет обработана раньше, чем test1:
SELECT relname,
round(score) score,
round(vacuum_score) vacuum_score,
round(vacuum_insert_score) vacuum_insert_score,
round(analyze_score) analyze_score
FROM pg_stat_autovacuum_scores
WHERE schemaname = 'public' AND relname ~ 'test'
ORDER BY score DESC;
relname | score | vacuum_score | vacuum_insert_score | analyze_score
---------+-------+--------------+---------------------+---------------
test2 | 100 | 100 | 1 | 30
test1 | 40 | 0 | 2 | 40
(2 rows)
Мультипликаторы появились для каждого из пяти критериев и по умолчанию они равны 1:
\dconfig+ autovacuum_*_weight
List of configuration parameters
Parameter | Value | Type | Context | Access privileges
------------------------------------------+-------+------+---------+-------------------
autovacuum_analyze_score_weight | 1 | real | sighup |
autovacuum_freeze_score_weight | 1 | real | sighup |
autovacuum_multixact_freeze_score_weight | 1 | real | sighup |
autovacuum_vacuum_insert_score_weight | 1 | real | sighup |
autovacuum_vacuum_score_weight | 10 | real | sighup |
(5 rows)
Если нужно отключить новую систему приоритизации, достаточно выставить все пять параметров в 0.
Автоочистка: параллельная обработка индексов таблицы
commit: 1ff3180ca01
Ручной запуск VACUUM(PARALLEL N) еще с 13-й версии умеет обрабатывать индексы таблицы параллельно, но автоочистка всегда работала в один поток на таблицу, даже если у неё много индексов. Теперь автоочистка может запускать несколько параллельных рабочих процессов для обработки индексов одной таблицы.
Максимальное количество параллельных рабочих процессов для каждого рабочего процесса автоочистки настраивается новым конфигурационным параметром autovacuum_max_parallel_workers:
\dconfig+ autovacuum_max_parallel_workers
List of configuration parameters
Parameter | Value | Type | Context | Access privileges
---------------------------------+-------+---------+---------+-------------------
autovacuum_max_parallel_workers | 0 | integer | sighup |
(1 row)
По умолчанию параллельная автоочистка отключена, что соответствует поведению в предыдущих версиях.
Количество процессов для очистки индексов можно определить и на уровне таблицы:
ALTER TABLE flights SET (autovacuum_parallel_workers = 2);
Уточнение предупреждения о приближении зацикливания счетчика транзакций
commit: e646450e609, 48f11bfa06c
Предупреждение о приближении зацикливания счетчика транзакций раньше начинало выдаваться за 40 млн транзакций. В наш скоростной век это может быть слишком поздно, поэтому теперь предупреждение будет выдаваться за 100 млн транзакций. Кроме того, предупреждение будет включать не только число оставшихся транзакций, но и процент еще доступных идентификаторов транзакций, примерно так:
WARNING: database "mydb" must be vacuumed within 99985967 transactions
DETAIL: Approximately 4.66% of transaction IDs are available for use.
Предполагается, что небольшой оставшийся процент нагляднее покажет администратору срочность, чем внушительное на вид число транзакций.
Журналирование причины запуска контрольной точки в сообщении о завершении
commit: 5b93a5987bd
Раньше причина запуска процесса контрольной точки (по расписанию, вручную, из-за нехватки сегментов WAL) записывалась в журнал сервера только в сообщении о начале контрольной точки, но не в сообщении о завершении. Это осложняло поиск причины запуска контрольной точки. Теперь причина дублируется и в завершающем сообщении:
CHECKPOINT;
\! tail -2 logfile
2026-09-10 13:30:05.951 MSK [22469] LOG: checkpoint starting: fast force wait
2026-09-10 13:30:05.965 MSK [22469] LOG: checkpoint complete: fast force wait: wrote 0 buffers (0.0%), wrote 0 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.015 s; sync files=0, longest=0.000 s, average=0.000 s; distance=0 kB, estimate=2403 kB; lsn=F/2F962CF0, redo lsn=F/2F962C90
Сокращение объема WAL и обновление карты видимости на лету
commit: a881cc9c7e8, b46e1e54d07, 378a216187a
Очистка больше не будет записывать в WAL специальную запись о том, что в карту видимости внесена отметка для страницы all_visible/all_frozen. Информация об изменении карты видимости теперь будет в записи, относящейся к самой странице таблицы. Это уменьшает размер WAL при массовых операциях с таблицей.
Кроме того, это открывает возможность обновлять карту видимости на лету, не только во время очистки (и COPY FREEZE). Что значит «на лету»? Известно, что при обращении к странице, как операциями чтения, так и обновления, может происходить самоочистка страницы. Самоочистка убирает ненужные версии строк на этой странице. В 19-й версии самоочистка дополнительно сможет обновлять карту видимости, проставляя признак all_visible, если на странице остались только видимые всем версии строк. Таким образом, обычные запросы на чтение смогут, выполняя самоочистку страницы, проставлять для нее признак all_visible в карте видимости.
Это хорошо тем, что не надо ждать, когда придет очистка для обновления карты видимости. Обычные операции с таблицей могут обновлять карту для отдельных страниц. Кроме того, самой очистке останется меньше работы, если предварительно карта для некоторых страниц была обновлена.
Расширение применения интерфейса потокового чтения
commit: ae58189a4d5, 213f0079b34, 4c910f3bbe9, d841ca2d149, 6c228755add, bfa3c4f106b
Появившийся ещё в 17-й версии интерфейс потокового чтения проникает во все большее число подсистем:
расширение pgstattuple: для функций pgstattuple_approx, pgstatindex и pgstathashindex;
индексы bloom: очистка индексов, массовое удаление(bulk delete) и сканирование по битовой карте;
очистка индексов GIN;
массовое удаление для индексов hash.
pg_stat_lock: статистика длительных блокировок
commit: 4019f725f5d
Представление pg_locks показывает только текущую картину блокировок. После освобождения ресурса информация о блокировке может остаться только в журнале сервера, при условии, что блокировка длилась дольше, чем deadlock_timeout, и включен параметр log_lock_waits (в 19-й версии он включен по умолчанию).
Новое представление pg_stat_lock накапливает агрегированную по типам ресурсов статистику блокировок, длящихся дольше deadlock_timeout.
Вот пример. Для начала сбросим статистику блокировок:
SELECT pg_stat_reset_shared('lock');
В первом сеансе заблокируем строку в таблице:
1=# BEGIN;
1=*# SELECT * FROM bookings WHERE book_ref = '2EW1SQ' FOR UPDATE;
book_ref | book_date | total_amount
----------+-------------------------------+--------------
2EW1SQ | 2025-09-01 03:00:12.557744+03 | 8125.00
(1 row)
Во втором сеансе пытаемся заблокировать эту же строку:
2=# SELECT * FROM bookings WHERE book_ref = '2EW1SQ' FOR UPDATE;
Пока команда ждет освобождения строки, неспешно переключаемся в первый сеанс и завершаем транзакцию:
1=*# COMMIT;
Второй сеанс продолжает работу:
2=# SELECT * FROM bookings WHERE book_ref = '2EW1SQ' FOR UPDATE;
book_ref | book_date | total_amount
----------+-------------------------------+--------------
2EW1SQ | 2025-09-01 03:00:12.557744+03 | 8125.00
(1 row)
А в представлении pg_stat_lock появилась информация о блокировке:
SELECT locktype, waits, wait_time, fastpath_exceeded
FROM pg_stat_lock;
locktype | waits | wait_time | fastpath_exceeded
------------------+-------+-----------+-------------------
relation | 0 | 0 | 0
extend | 0 | 0 | 0
frozenid | 0 | 0 | 0
page | 0 | 0 | 0
tuple | 0 | 0 | 0
transactionid | 1 | 10060.469 | 0
virtualxid | 0 | 0 | 0
spectoken | 0 | 0 | 0
object | 0 | 0 | 0
userlock | 0 | 0 | 0
advisory | 0 | 0 | 0
applytransaction | 0 | 0 | 0
(12 rows)
Статистика собирается по всем базам данных кластера и агрегируется по столбцу locktype, имеющему те же значения, что и аналогичный столбец в pg_locks. В столбцах waits и wait_time (в миллисекундах) накапливается только статистика длительных (дольше deadlock_timeout) блокировок. А столбец fastpath_exceeded показывает, сколько раз не удалось захватить блокировку по быстрому пути. Если значение велико, стоит подумать об увеличении max_locks_per_transaction.
pg_upgrade: оптимизация переноса больших объектов, часть 2
commit: b33f7536128
В статье о первом коммитфесте уже писалось о том, что при обновлении сервера через pg_upgrade метаданные больших объектов (pg_largeobject_metadata) переносятся более быстрой командой COPY вместо отдельных INSERT. Но оптимизация работала только при обновлении с 12-й версии и новее.
Сейчас снято и это ограничение. Команда COPY для переноса pg_largeobject_metadata будет использоваться независимо от версии сервера.
oid2name: путь к файлам относительно PGDATA
commit: 3c5ec35dea2
Утилита oid2name в расширенном режиме вывода показывает путь к файлам относительно PGDATA:
$ oid2name -d demo -t bookings --extended
From database "demo":
Filenode Table Name Oid Schema Tablespace Path
---------------------------------------------------------------------
17258 bookings 17258 bookings pg_default base/16384/17258
Этот же путь можно получить функцией pg_relation_filepath:
$ psql -d demo -c "SELECT pg_relation_filepath('bookings')"
pg_relation_filepath
----------------------
base/16384/17258
(1 row)
psql: комментарии к служебным запросам
commit: 41d69e6dcca
Служебные запросы, которые psql выполняет для получения информации из системного каталога, раньше было сложно отличить от пользовательских в журнале сервера или выводе скрипта. Теперь каждый служебный запрос обрамлен соответствующими комментариями.
\set ECHO_HIDDEN on
\sf now
/**** INTERNAL QUERY ****/
/* Get function's OID */
SELECT 'now'::pg_catalog.regproc::pg_catalog.oid
/************************/
/**** INTERNAL QUERY ****/
/* Get function's definition */
SELECT pg_catalog.pg_get_functiondef(1299)
/************************/
CREATE OR REPLACE FUNCTION pg_catalog.now()
RETURNS timestamp with time zone
LANGUAGE internal
STABLE PARALLEL SAFE STRICT
AS $function$now$function$
log_min_messages для разных типов процессов
commit: 38e0190ced7
Для отладки системных процессов может быть полезно изменить значение log_min_messages на более детальное, например с WARNING на DEBUG1. Но тогда в журнал сервера посыпется огромное количество сообщений от всех процессов, в первую очередь клиентских.
В 19-й версии в параметре log_min_messages можно задавать разные уровни сообщений для разных типов процессов. Зададим уровень DEBUG5 для процесса контрольной точки и оставим WARNING для всех остальных типов:
ALTER SYSTEM SET log_min_messages = 'warning, checkpointer:debug5';
SELECT pg_reload_conf();
Теперь после выполнения контрольной точки в журнале сервера появятся отладочные сообщения:
CHECKPOINT;
\! tail logfile | grep DEBUG
2026-09-10 14:28:56.991 MSK [22469] DEBUG: checkpointer updated shared memory configuration values
2026-09-10 14:29:05.484 MSK [22469] DEBUG: performing replication slot checkpoint
2026-09-10 14:29:05.555 MSK [22469] DEBUG: snapshot of 0+0 running transaction ids (lsn F/2F962E08 oldest xid 1383 latest complete 1382 next xid 1383)
2026-09-10 14:29:05.561 MSK [22469] DEBUG: attempting to remove WAL segments older than log file 000000000000000F0000002E
2026-09-10 14:29:05.562 MSK [22469] DEBUG: SlruScanDirectory invoking callback on pg_subtrans/0000
Время сброса статистики для pg_stat_database_conflicts, pg_statio_all_sequences
commit: 723619eaa3a, 8fe315f18d4
Представления pg_stat_database_conflicts и pg_statio_all_sequences не имели столбца stats_reset, который есть в остальных статистических представлениях и показывает время последнего сброса счётчиков. Добавили.
Похожая работа была проделана в этом релизном цикле для представлений pg_stat_{all|user}_{tables|indexes} и pg_stat_user_functions.
На этом пока все. В следующем обзоре рассмотрим изменения, относящиеся к курсам DBA3 и DBS.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.