После выхода релиз-кандидата версии 17 в плане выпуска осталась последняя незакрытая дата: 26 сентября 2024 года. На этот день намечен официальный выпуск PostgreSQL 17.
В этой статье рассказывается о патчах, принятых в ходе последнего мартовского коммитфеста. Предыдущие статьи о коммитфестах 17-й версии: 2023-07, 2023-09, 2023-11, 2024-01.
Все вместе они дают подробное представление о новой версии СУБД.
Потери после заморозки кода
К сожалению, некоторые принятые патчи впоследствии были отменены. Среди отмененных можно выделить следующие:
- ALTER TABLE… MERGE/SPLIT PARTITION (отмена: 3890d90c)
- Темпоральные первичные, уникальные и внешние ключи (отмена: 8aee330a)
- Планировщик: исключение лишних соединений таблицы самой с собой (отмена: d1d286d8)
- pg_constraint: ограничения NOT NULL (отмена: 6f8bb7c1)
А теперь переходим к описанию новинок версии.
Команды SQL
Новые возможности команды MERGE
COPY… FROM: информация о незагруженных строках
Поддержка стандарта SQL/JSON
Производительность
Настройка кешей SLRU
Планировщик: Merge Append для реализации UNION
Планировщик: статистика материализованных CTE
Оптимизация параллельных планов с DISTINCT
Оптимизация поиска по набору значений в B-деревьях
VACUUM: новое хранилище идентификаторов мертвых строк
VACUUM: объединение WAL-записей об очистке и заморозке страницы
Функции с вложенными транзакциями в параллельных процессах
Мониторинг и управление
EXPLAIN (analyze, serialize): затраты на распаковку значений
EXPLAIN: улучшено отображение узлов SubPlan и InitPlan
pg_buffercache: вытеснение из кеша
Сервер
random: случайное число в заданном диапазоне
transaction_timeout: завершение сеанса по достижении таймаута транзакции
Запрет на использование ALTER SYSTEM
Привилегия MAINTAIN и предопределенная роль pg_maintain
Встроенный провайдер локалей для C.UTF8
pg_column_toast_chunk_id: идентификатор значения TOAST
Функция pg_basetype: базовый тип домена
pg_constraint: ограничения NOT NULL для доменов
Новая функция to_regtypemod
Хеш-индексы для ltree
Репликация
pg_createsubscriber: быстрое создание логической реплики из физической
Логические слоты: отслеживание причин конфликтов репликации
pg_basebackup -R: dbname в primary_conninfo
Синхронизация логических слотов репликации между мастером и репликой
Оптимизация логического декодирования вложенных транзакций
Клиентские приложения
libpq: неблокирующее прерывание запроса
libpq: прямое подключение по TLS
vacuumdb, clusterdb, reindexdb: обработка отдельных объектов в нескольких базах
reindexdb: --jobs и --index одновременно
psql: новая реализация FETCH_COUNT
pg_dump --exclude-extension
Резервное копирование и восстановление больших объектов
Новые возможности команды MERGE
commit: 5f2e179b, 0294df2f, c649fa24
Появившаяся в 15-й версии команда MERGE получила несколько полезных улучшений.
Для примера возьмем две таблицы:
SELECT * FROM source;
id | descr
----+-------
0 | Ноль
1 | Один
(2 rows)
SELECT * FROM target;
id | descr
----+-------
1 | Раз
2 | Два
(2 rows)
Первое улучшение касается target. В качестве целевого отношения можно использовать не только таблицу, но и представление. Представление должно быть обновляемым, либо для него должны быть определены триггеры INSTEAD OF.
Теперь выполним команду MERGE так, чтобы содержание target стало таким же, как и source. Для этого в target нужно добавить строку с id=0, обновить строку с id=1 и удалить строку с id=2. До 17-й версии в команде MERGE не было возможности обрабатывать строки целевого отношения, которых нет в источнике. Теперь для таких строк появилось условие WHEN NOT MATCHED BY SOURCE:
MERGE INTO target AS t
USING source AS s
ON (s.id = t.id)
WHEN matched AND s.descr IS DISTINCT FROM t.descr
THEN
UPDATE SET descr = s.descr
WHEN NOT MATCHED BY TARGET
THEN
INSERT (id, descr) VALUES (s.id, s.descr)
WHEN NOT MATCHED BY SOURCE
THEN
DELETE
RETURNING merge_action(), s.*, t.*;
merge_action | id | descr | id | descr
--------------+----+-------+----+-------
INSERT | 0 | Ноль | 0 | Ноль
UPDATE | 1 | Один | 1 | Один
DELETE | | | 2 | Два
(3 rows)
MERGE 3
Третье улучшение видно в выводе команды. Речь о поддержке предложения RETURNING. Чтобы понять, какие строки были добавлены, изменены или удалены, для предложения RETURNING предусмотрена новая функция merge_action. Также отметим, что в RETURNING можно обращаться как к столбцам целевого отношения, так и источника.
SELECT * FROM target;
id | descr
----+-------
0 | Ноль
1 | Один
(2 rows)
См. также
Waiting for PostgreSQL 17 – Add RETURNING support to MERGE (Hubert 'depesz' Lubaczewski)
COPY… FROM: информация о незагруженных строках
commit: f5a22789
В статье о январском коммитфесте уже рассказывалось, что в 17-й версии команда COPY может игнорировать ошибки преобразования форматов.
Теперь в команду добавлен параметр log_verbosity. Установка этого параметра в значение verbose включает в вывод команды сообщения NOTICE для каждой пропущенной строки.
CREATE TABLE t (id int PRIMARY KEY);
COPY t FROM STDIN (on_error 'ignore', log_verbosity verbose);
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
>> два
>> три
>> 4
>> \.
NOTICE: skipping row due to data type incompatibility at line 2 for column id: "два"
NOTICE: skipping row due to data type incompatibility at line 3 for column id: "три"
NOTICE: 2 rows were skipped due to data type incompatibility
COPY 2
SELECT * FROM t;
id
----
1
4
(2 rows)
См. также
Waiting for PostgreSQL 17 – Add new COPY option LOG_VERBOSITY (Hubert 'depesz' Lubaczewski)
Exploring PostgreSQL 17: A Developer’s Guide to New Features – Part 3: The COPY Command Gets More User-Friendly (Deepak Mahto)
Поддержка стандарта SQL/JSON
commit: 6185c973, de360045
Стандарт SQL регламентирует работу с типом JSON при помощи специальных
конструкторов, предикатов и функций. Все они были реализованы и включены в PostgreSQL 15 перед заморозкой кода. Однако перед самым выпуском 15-й версии поддержку SQL/JSON удалили из-за опасений деградации производительности.
Дело в том, что некоторые функции должны обрабатывать некорректные значения переданных параметров. Например, если в качестве значения JSON передается строка, не являющаяся JSON. Для обработки таких ошибок без прерывания работы использовался механизм точек сохранения и отката к ним. Он приводил к серьезным накладным расходам, но другого способа обработать ошибку и продолжить выполнение функции просто не существовало.
В 16-ю версию, с прицелом на SQL/JSON, были добавлены функции pg_input_error_info и pg_input_is_valid для «мягкой» обработки ошибок приведения типов. С их помощью часть функционала SQL/JSON была доработана и попала в 16-ю версию.
Теперь, за некоторыми исключениями (в описании второго коммита), реализована остальная часть стандарта.
См. также
Waiting for …: SQL/JSON is coming back. Hopefully (Hubert 'depesz' Lubaczewski)
Настройка кешей SLRU
commit: 53c2a97a, bcdfa5f2
В высоконагруженных системах размер кешей SLRU, точнее, невозможность увеличить размер того или иного кеша, может стать бутылочным горлышком.
В 17-й версии для каждого кеша появился соответствующий параметр конфигурации (с суффиксом _buffers в названии), что позволяет тонко настраивать систему под специфическую нагрузку. Кроме того, существенно оптимизирован поиск в кешах.
Новые параметры настройки кешей SLRU:
- commit_timestamp_buffers
- multixact_member_buffers
- multixact_offset_buffers
- notify_buffers
- serializable_buffers
- subtransaction_buffers
- transaction_buffers
Сами кеши в представлении pg_stat_slru немного переименовали (второй коммит), чтобы их было проще соотнести с именами параметров:
SELECT name FROM pg_stat_slru;
name
------------------
commit_timestamp
multixact_member
multixact_offset
notify
serializable
subtransaction
transaction
other
(8 rows)
Планировщик: Merge Append для реализации UNION
commit: 66c0185a
В запросах с UNION планировщик всегда использовал узел Append для объединения результатов подзапросов. На следующем этапе к результату объединения применялась либо сортировка (Sort), либо хеш-агрегирование (Hash Aggregate) и, наконец, устранение дубликатов (Unique).
Непрактичный, но простой пример:
16=# EXPLAIN (costs off)
SELECT book_ref FROM bookings
UNION
SELECT book_ref FROM bookings;
QUERY PLAN
---------------------------------------------------
Unique
-> Sort
Sort Key: bookings.book_ref
-> Append
-> Seq Scan on bookings
-> Seq Scan on bookings bookings_1
(6 rows)
Однако при наличии индексов или предложения ORDER BY, результаты подзапросов могут быть получены отсортированными еще до объединения. И если вместо узла Append использовать Merge Append, то повторная полная сортировка не понадобится, можно сразу устранять дубликаты.
План этого же запроса в 17-й версии:
QUERY PLAN
------------------------------------------------------------------------
Unique
-> Merge Append
Sort Key: bookings.book_ref
-> Index Only Scan using bookings_pkey on bookings
-> Index Only Scan using bookings_pkey on bookings bookings_1
(5 rows)
Планировщик: статистика материализованных CTE (продолжение)
commit: a65724df
О том, что планировщик научился использовать некоторую статистику столбцов из CTE для построения внешних частей плана, уже говорилось в статье о ноябрьском коммитфесте. Теперь планировщик будет учитывать сортировку набора строк, возвращаемых из CTE. Это позволит избежать ненужной повторной сортировки для ряда запросов.
В следующем примере внутри CTE используется индексный доступ к таблице bookings, а значит, результат возвращается упорядоченным. Но в предыдущих версиях планировщик об этом не знает и заново сортирует выборку из CTE (узел Sort):
16=# EXPLAIN (costs off)
WITH b AS MATERIALIZED (
SELECT * FROM bookings ORDER BY book_ref
)
SELECT * FROM b ORDER BY book_ref;
QUERY PLAN
----------------------------------------------------
Sort
Sort Key: b.book_ref
CTE b
-> Index Scan using bookings_pkey on bookings
-> CTE Scan on b
(5 rows)
В 17-й версии повторная сортировка не нужна:
QUERY PLAN
----------------------------------------------------
CTE Scan on b
CTE b
-> Index Scan using bookings_pkey on bookings
(3 rows)
Оптимизация параллельных планов с DISTINCT
commit: 7e0ade0f, 902900b3
Параллельное выполнение DISTINCT появилось в 15-й версии. Каждый из рабочих процессов обрабатывает свою часть данных, а затем передает результат ведущему процессу в узел Gather для сбора.
Цель нынешней оптимизации — дать выбор планировщику использовать для сбора результатов не только узел Gather, но и Gather Merge. Это может оказаться более эффективным, если уникальные значения должны быть отсортированы.
В следующем примере в узле Gather собирается результат от рабочих процессов. На этом параллельная часть плана заканчивается. Затем все строки результата сортируются и выдаются первые 10. (PostgreSQL умеет очень эффективно сортировать и выдавать первые несколько строк, тем не менее сортировать нужно весь набор.)
План запроса в 16-й версии:
16=# EXPLAIN (costs off)
SELECT DISTINCT flight_id FROM ticket_flights ORDER BY 1 LIMIT 10;
QUERY PLAN
-------------------------------------------------------------------
Limit
-> Sort
Sort Key: flight_id
-> HashAggregate
Group Key: flight_id
-> Gather
Workers Planned: 2
-> HashAggregate
Group Key: flight_id
-> Parallel Seq Scan on ticket_flights
(10 rows)
В 17-й версии каждый рабочий процесс сначала сортирует, и только потом передает свою часть ведущему процессу в узел Gather Merge. Ведущий процесс получает все строки сразу отсортированными, остается только убрать дубликаты (Unique) и выдать первые 10 строк. Общей сортировки больше не требуется.
План запроса в 17-й версии:
QUERY PLAN
-------------------------------------------------------------------
Limit
-> Unique
-> Gather Merge
Workers Planned: 2
-> Sort
Sort Key: flight_id
-> HashAggregate
Group Key: flight_id
-> Parallel Seq Scan on ticket_flights
(9 rows)
Оптимизация поиска по набору значений в B-деревьях
commit: 5bf748b8
Оптимизирован поиск по набору значений, особенно в многоколоночных индексах. Это можно увидеть на примере с таблицей ticket_flights, у которой есть уникальный индекс по двум столбцам: ticket_no и flight_id.
Поищем в таблице записи по заданному набору билетов и рейсов:
16=# EXPLAIN (analyze, buffers, costs off, timing off, summary off)
SELECT *
FROM ticket_flights
WHERE ticket_no IN ('123456789012', '234567890123', '345678901234')
AND flight_id IN (12345, 23456, 34567);
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using ticket_flights_pkey on ticket_flights (actual rows=0 loops=1)
Index Cond: ((ticket_no = ANY ('{123456789012,234567890123,345678901234}'::bpchar[])) AND (flight_id = ANY ('{12345,23456,34567}'::integer[])))
Buffers: shared hit=36
План запроса в 17-й версии не изменился, но прочитано всего 4 буфера, вместо прежних 36:
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using ticket_flights_pkey on ticket_flights (actual rows=0 loops=1)
Index Cond: ((ticket_no = ANY ('{123456789012,234567890123,345678901234}'::bpchar[])) AND (flight_id = ANY ('{12345,23456,34567}'::integer[])))
Buffers: shared hit=4
VACUUM: новое хранилище идентификаторов мертвых строк
commit: 30e14428, 667e65aa
Как известно, процесс очистки начинается со сканирования таблицы (с учетом карты видимости) в поисках ненужных версий строк. Найденные идентификаторы строк (TID) запоминаются в специальном массиве. Далее этот массив используется для освобождения места сначала во всех индексах (которые приходится сканировать полностью), а затем и в самой таблице. Если идентификаторы ненужных строк не поместились в массив ― процесс придется повторять. Это плохой расклад, его следует избегать настройками автоочистки.
Слабым местом в этом процессе до 17-й версии был массив для хранения идентификаторов строк. Его размер определяется параметром maintenance_work_mem, по умолчанию 64 МБ. Вся память выделяется сразу, даже если мертвых строк будет немного. При этом размер массива не может превышать 1 ГБ, даже если maintenance_work_mem установить в большее значение.
Что изменилось. В первом коммите реализована структура памяти и интерфейс работы с идентификаторами строк на основе сжатого префиксного дерева (radix tree). По оценке автора (ссылка ниже) в новом хранилище идентификаторы строк занимают примерно в двадцать раз меньше места, чем в массиве. А поиск идентификаторов стал в несколько раз быстрее. Кроме того, больше нет ограничения на 1 ГБ (хотя при таком компактном размещении это не так актуально), и память выделяется по мере необходимости, а не полностью maintenance_work_mem.
Всё это должно свести к минимуму ситуации, когда для очистки таблицы оказалось недостаточно одного прохода.
Увидеть результаты оптимизации можно на таком примере, который выполним для 16-й и 17-й версий.
Подготовка таблицы с одним индексом.
CREATE TABLE t(id integer) WITH (autovacuum_enabled = off);
INSERT INTO t SELECT gen.id FROM generate_series(1,3_500_000) gen(id);
CREATE INDEX t_id ON t(id);
VACUUM;
Обновим строки в таблице, а перед запуском очистки установим минимальное значение maintenance_work_mem. Нас интересует сколько раз за время очистки выполнится фаза очистки индексов.
UPDATE t SET id = id + 1;
SET maintenance_work_mem = '1MB';
VACUUM VERBOSE t;
Отчет VACUUM VERBOSE в 16-й версии начинается со строк:
INFO: vacuuming "postgres.public.t"
INFO: finished vacuuming "postgres.public.t": index scans: 21
...
Остальной вывод пропущен, главное мы уже увидели. Размер массива идентификаторов строк в 1МБ слишком мал и очистка индексов выполнялась 21 раз.
Результат в 17-й версии:
INFO: vacuuming "postgres.public.t"
INFO: finished vacuuming "postgres.public.t": index scans: 1
...
Все идентификаторы ненужных строк поместились в новое хранилище за раз.
Еще одно изменение касается представления pg_stat_progress_vacuum. Столбцы max_dead_tuples и num_dead_tuples заменены на max_dead_tuple_bytes и dead_tuple_bytes. Как и следует из названия столбцов, прогресс выполнения теперь отслеживается в байтах, а не в строках: больше не существует предсказуемой зависимости между количеством ненужных строк и местом, занимаемым в хранилище их идентификаторами.
См. также
PostgreSQL meets ART — Using Adaptive Radix Tree to speed up vacuuming (Masahiko Sawada)
VACUUM: объединение WAL-записей об очистке и заморозке страницы
commit: f83d7097, 6dbb4902
Если во время очистки страницы выполняется заморозка строк, то VACUUM будет записывать в WAL одну объединенную запись, а не две отдельные, как раньше. В результате при заморозке страниц уменьшается объем записи в WAL, что хорошо не только для сервера, но и для его реплик, а также архива WAL.
Оценить работу оптимизации поможет команда VACUUM с параметром VERBOSE, выводящая информацию о записи в WAL отдельной строкой.
В таблице для теста сразу заморозим все строки.
CREATE TABLE t(id integer) WITH (autovacuum_enabled = off);
INSERT INTO t SELECT gen.id FROM generate_series(1,3_500_000) gen(id);
VACUUM FREEZE t;
Затем изменяем строки и выполняем очистку с заморозкой. В отчете VACUUM смотрим на статистику работы c WAL.
UPDATE t SET id = id + 1;
VACUUM FREEZE VERBOSE t;
PostgreSQL 16:
INFO: aggressively vacuuming "postgres.public.t"
...
WAL usage: 77437 records, 28718 full page images, 142916493 bytes
...
PostgreSQL 17:
INFO: aggressively vacuuming "postgres.public.t"
...
WAL usage: 61949 records, 27227 full page images, 116712997 bytes
...
~62 тысячи записей WAL против ~77 тысяч ― очень приличное сокращение.
Функции с вложенными транзакциями в параллельных процессах
commit: 0075d789
Это исправление ошибки. Интересно тем, что функции с вложенными транзакциями (c блоком EXCEPTION в plpgsql) теперь можно помечать как PARALLEL SAFE. Но с учетом других ограничений, среди которых запрет на запись в базу данных.
Получается что если EXCEPTION используется для ловли деления на 0 и при этом ничего не пишется в базу данных, то функцию можно пометить для параллельной работы.
Однако стоит учесть, что использовать EXCEPTION в таких ситуациях не самая удачная идея. Если функция ничего не пишет в базу данных, то при попадании в секцию EXCEPTION нечего откатывать к точке сохранения. А установка точки сохранения далеко не бесплатна.
EXPLAIN (analyze, serialize): затраты на распаковку значений
commit: 06286709
Известно, что EXPLAIN ANALYZE выполняет запрос, позволяя посмотреть на реальные затраты времени. Тем не менее, некоторые действия не выполняются и в этой форме EXPLAIN.
Если посмотреть на время в плане запроса:
=# EXPLAIN (costs off, analyze) SELECT * FROM tickets;
QUERY PLAN
-----------------------------------------------------------------------
Seq Scan on tickets (actual time=0.080..212.350 rows=2949857 loops=1)
Planning Time: 0.060 ms
Execution Time: 275.396 ms
(3 rows)
… и сравнить с реальным временем выполнения, то разница существенная:
\timing on
Timing is on.
```pgsql
=# SELECT * FROM tickets\g /dev/null
Time: 1853,612 ms (00:01,854)
Команда EXPLAIN ANALYZE не преобразовывает результат запроса в текстовый или двоичный вид для отправки клиенту и экономит на сборке значений TOAST из отдельных кусочков. Поэтому чем больше строк возвращает запрос, тем больше погрешность.
Новый параметр SERIALIZE заставляет EXPLAIN проделать и эту работу. Затраты на распаковку значений отображены в строке Serialization:
=# EXPLAIN (costs off, analyze, serialize) SELECT * FROM tickets;
QUERY PLAN
-----------------------------------------------------------------------
Seq Scan on tickets (actual time=0.074..232.690 rows=2949857 loops=1)
Planning Time: 0.057 ms
Serialization: time=1215.195 ms output=338792kB format=text
Execution Time: 1568.133 ms
(4 rows)
Разница с реальным временем выполнения существенно сократилась, но по-прежнему заметна. Она объясняется затратами на пересылку данных с сервера клиенту и запись в файл. Эти затраты EXPLAIN уже не сможет учесть.
См. также
Waiting for PostgreSQL 17 – Invent SERIALIZE option for EXPLAIN (Hubert 'depesz' Lubaczewski)
EXPLAIN: улучшено отображение узлов SubPlan и InitPlan
commit: fd0398fc
Раздел документации EXPLAIN BASICS дополнен примерами планов с узлами SubPlan, hashed SubPlan и InitPlan. А мы рассмотрим сделанные изменения в отображении этих узлов на таком запросе:
EXPLAIN (costs off)
SELECT * FROM bookings
WHERE book_date > (SELECT current_date) AND
total_amount NOT IN (VALUES (1),(2));
План в 16-й версии:
QUERY PLAN
-----------------------------------------------------------
Seq Scan on bookings
Filter: ((book_date > $0) AND (NOT (hashed SubPlan 2)))
InitPlan 1 (returns $0)
-> Result
SubPlan 2
-> Values Scan on "*VALUES*"
(6 rows)
План в 17-й версии:
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------
Seq Scan on bookings
Filter: ((book_date > (InitPlan 1).col1) AND (NOT (ANY (total_amount = ((hashed SubPlan 2).col1)::numeric))))
InitPlan 1
-> Result
SubPlan 2
-> Values Scan on "*VALUES*"
(6 rows)
Нас интересуют два изменения, произошедшие со второй строкой.
- Выходные параметры узлов InitPlan и SubPlan теперь записываются не через знак доллара в виде $0, $1,.., $n, а в виде (InitPlan N).colX или (SubPlan N).colX, где N ― порядковый номер узла InitPlan или SubPlan, а X ― порядковый номер возвращаемого столбца в узле.
- Точнее указано второе условие. Малоинформативное
NOT (hashed SubPlan 2)
заменено наNOT (ANY (total_amount = ((hashed SubPlan 2).col1)::numeric))
. Теперь его легче соотнести с условием в тексте запроса.
pg_buffercache: вытеснение из кеша
commit: 13453eed
Новая функция pg_buffercache_evict позволяет вытеснить буфер из буферного кеша.
CREATE EXTENSION pg_buffercache;
Таблица для примера будет занимать один блок. И мы не хотим, чтобы автоочистка вмешивалась в работу.
CREATE TABLE t (id int) WITH (autovacuum_enabled=off);
INSERT INTO t VALUES (1);
Найдем буфер, относящийся к таблице:
SELECT bufferid, isdirty, pinning_backends
FROM pg_buffercache
WHERE relfilenode = (
SELECT relfilenode
FROM pg_class
WHERE oid = 't'::regclass
);
bufferid | isdirty | pinning_backends
----------+---------+------------------
824 | t | 0
(1 row)
Для вытеснения вызываем функцию pg_buffercache_evict, передавая ей только что полученный номер буфера:
SELECT pg_buffercache_evict(824);
pg_buffercache_evict
----------------------
t
(1 row)
Проверяем, что буферном кеше не осталось буферов, относящихся к таблице:
SELECT bufferid, isdirty, pinning_backends
FROM pg_buffercache
WHERE relfilenode = (
SELECT relfilenode
FROM pg_class
WHERE oid = 't'::regclass
);
bufferid | isdirty | pinning_backends
----------+---------+------------------
(0 rows)
random: случайное число в заданном диапазоне
commit: e6341323
Функция random выдает случайное число в диапазоне от 0 до 1. Однако часто нужно получить случайное число в произвольно заданном диапазоне. И такая возможность появилась.
SELECT random(2,5), random(0,3.14) FROM generate_series(1,5);
random | random
--------+--------
2 | 0.43
5 | 0.98
3 | 0.12
5 | 2.18
2 | 2.43
(5 rows)
У random теперь два параметра для указания минимального и максимального значения возвращаемого случайного числа. Диапазон можно указывать для типов int, bigint и numeric.
transaction_timeout: завершение сеанса по достижении таймаута транзакции
commit: 51efe38c
У нас уже есть два параметра для контроля за долгими транзакциями: statement_timeout и idle_in_transaction_session_timeout. Однако транзакция может длиться сколь угодно долго, если в ней выполняются достаточно короткие команды, не превышающие statement_timeout, а паузы между командами укладываются в idle_in_transaction_session_timeout.
В таких ситуациях transaction_timeout гарантирует, что длительность транзакции не превысит заданный таймаут. А в случае превышения, транзакция и сеанс, в котором она выполнялась, будут прерваны.
Запрет на использование ALTER SYSTEM
commit: d3ae2a24
Облачные провайдеры обычно предоставляют свои собственные инструменты для управления конфигурацией, в которых ряд параметров сервера либо вообще нельзя менять, либо не все значения допустимы. Это важно для правильной работы сервиса. Например если администратор клиента установит формат журнала сервера jsonlog вместо ожидаемого csvlog, то инструменты анализа журнала перестанут работать.
Чтобы не было соблазна редактировать postgresql.conf, доступ к файловой системе сервера можно закрыть. Но остается в запасе команда ALTER SYSTEM, меняющая postgresql.auto.conf без доступа к файловой системе.
Новый параметр конфигурации allow_alter_system позволяет запретить использование ALTER SYSTEM для всех, включая суперпользователей:
=# SHOW allow_alter_system;
allow_alter_system
--------------------
off
(1 row)
=# ALTER SYSTEM SET wal_level = minimal;
ERROR: ALTER SYSTEM is not allowed in this environment
Но очень важно иметь в виду, что эта возможность не относится к безопасности сервера. У суперпользователя есть множество других способов изменить конфигурацию сервера и совершить другие потенциально опасные действия. Главная задача нового параметра ― предотвратить непреднамеренное, без злого умысла, изменение конфигурации.
См. также
The PostgreSQL community debates ALTER SYSTEM (Jonathan Corbet)
Привилегия MAINTAIN и предопределенная роль pg_maintain
commit: ecb0fd33
Об этой работе было написано еще в статьях предыдущего цикла 16-й версии, но уже после заморозки кода патч был отменен по соображениям безопасности. Возникшие тогда проблемы решены, и в 17-й версии нас ждет новая привилегия MAINTAIN для таблиц и материализованных представлений.
Обладатели этой привилегии смогут выполнять следующие команды: ANALYZE, VACUUM (включая VACUUM FULL), CLUSTER, REINDEX, REFRESH MATERIALIZED VIEW и LOCK TABLE.
А членство в новой роли pg_maintain предоставляет привилегию MAINTAIN для всех отношений в базе данных.
Встроенный провайдер локалей для C.UTF8
commit: f69319f2
Долгое время в PostgreSQL использовался только один провайдер ― libc, предоставляющий локали системной библиотеки C. В 10-й версии появился второй провайдер ― icu, использующий внешнюю библиотеку ICU. В 17-й версии появился третий, встроенный провайдер, предоставляющий поддержку локалей C и C.UTF8.
С точки зрения функциональных возможностей ничего нового. Обе локали давно существуют предоставляются провайдером libc. Аскетичная локаль C по-прежнему понимает только английский алфавит, а на C.UTF8 стоит остановиться подробнее.
CREATE DATABASE test_builtin_provider
LOCALE_PROVIDER = 'builtin'
BUILTIN_LOCALE = 'C.UTF8'
TEMPLATE = 'template0';
В локали C.UTF8 корректно работает классификация символов для любых языков (какие символы являются буквами, к верхнему или нижнему регистру они принадлежат), но сортировка выполняется по кодам символов, без учета лингвистических особенностей. Следствием этого является неправильная сортировка буквы «ё» для кириллицы:
WITH russian_alphabet AS (
SELECT regexp_split_to_table('абвгдеёжзийклмнопрстуфхцчшщъыьэюя', '') AS letter
)
SELECT string_agg(lower(letter), '' ORDER BY lower(letter)) AS lower,
string_agg(upper(letter), '' ORDER BY upper(letter)) AS upper
FROM russian_alphabet\gx
-[ RECORD 1 ]----------------------------
lower | абвгдежзийклмнопрстуфхцчшщъыьэюяё
upper | ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
Если такая сортировка допустима, то использование локали C.UTF8 встроенного провайдера даст преимущества:
- Сортировка и преобразование символов к верхнему/нижнему регистру работают быстрее за счет оптимизаций внутри сервера. Примеры замеров можно найти в статье по ссылке ниже.
- Локаль не зависит от внешних библиотек и работает одинаково на любых платформах в пределах основной версии PostgreSQL.
См. также
Looking at the new built-in collation provider in PostgreSQL 17 (Daniel Vérité)
pg_column_toast_chunk_id: идентификатор значения TOAST
commit: d1162cfd
Значения столбцов с типами переменной длины могут переноситься в таблицу TOAST. Но как понять, какие именно значения не поместились в основной таблице и были перенесены в TOAST?
В следующем примере в таблицу добавлены три строки с большими значениями типа numeric:
CREATE TABLE t(
id integer,
n numeric
);
INSERT INTO t VALUES
(1, 123456789::numeric ^ 123::numeric),
(2, 123456789::numeric ^ 1234::numeric),
(3, 123456789::numeric ^ 12345::numeric);
Где хранятся эти значения, в основной таблице или в TOAST? Ответить на этот вопрос поможет новая функция pg_column_toast_chunk_id, возвращающая chunk_id ― идентификатор большого значения в таблице TOAST:
SELECT id, pg_column_toast_chunk_id(n)
FROM t;
id | pg_column_toast_chunk_id
----+--------------------------
1 |
2 |
3 | 24587
(3 rows)
Как видно, только значение из третьей строки не поместилось в основной таблице.
Функция pg_basetype: базовый тип домена
commit: b154d8a6
Функция pg_basetype возвращает базовый тип домена. В случае вложенных доменов, функция рекурсивно обойдет их в поисках базового типа:
CREATE DOMAIN posint AS integer CHECK (VALUE > 0);
CREATE DOMAIN dozen AS posint CHECK (VALUE < 13);
SELECT pg_basetype('dozen'::regtype);
pg_basetype
-------------
integer
(1 row)
pg_constraint: ограничения NOT NULL для доменов
commit: e5da0fe3
В статье о сентябрьском коммитфесте рассказывалось о том, что в pg_constraint стали записываться ограничения NOT NULL для столбцов таблиц. К сожалению тот патч был отменен.
Но ограничения NOT NULL всё-таки появятся в pg_constraint. Пока только для доменов.
CREATE DOMAIN int_nn AS integer NOT NULL;
SELECT * FROM pg_constraint WHERE conname ~ 'int_nn'\gx
-[ RECORD 1 ]--+----------------
oid | 16573
conname | int_nn_not_null
connamespace | 2200
contype | n
condeferrable | f
condeferred | f
convalidated | t
conrelid | 0
contypid | 16572
conindid | 0
conparentid | 0
confrelid | 0
confupdtype |
confdeltype |
confmatchtype |
conislocal | t
coninhcount | 0
connoinherit | f
conkey |
confkey |
conpfeqop |
conppeqop |
conffeqop |
confdelsetcols |
conexclop |
conbin |
Новая функция to_regtypemod
commit: 1218ca99
Комбинация из уже существующих функций format_type и to_regtype вместе с новой функцией to_regtypemod поможет преобразовать название типа данных, заданное в любом допустимом виде, к канонической форме представления.
WITH types (alias) AS (
VALUES ('timestamp(5)'),
('char(10)'),
('varchar(30)')
)
SELECT alias,
format_type(to_regtype(alias), to_regtypemod(alias))
FROM types;
alias | format_type
--------------+--------------------------------
timestamp(5) | timestamp(5) without time zone
char(10) | character(10)
varchar(30) | character varying(30)
(3 rows)
Хеш-индексы для ltree
commit: 485f0aa8
Тип данных ltree одноименного расширения поддерживает хеш-индексы.
pg_createsubscriber: быстрое создание логической реплики из физической
commit: d44032d0
Обычная настройка логической репликации для большой базы данных может занять много времени из-за начальной синхронизации данных. Ситуация усугубляется, если в базе данных при этом происходят активные изменения.
Для таких случаев за основу будущей логической реплики можно взять физическую реплику, ведь в ней все таблицы уже синхронизированы. Если физическую реплику перевести в режим чтения/записи, создать пары публикация ― подписка в одной или нескольких базах данных на обоих серверах, а в свойствах подписки указать «правильную» позицию, с которой продолжить получать изменения, то логическая реплика создастся достаточно быстро. Это упрощенное описание того, что делает новая утилита сервера pg_createsubscriber. Полное описание по ссылке в документации.
Инициализируем основной сервер primary на порту 5400. Сразу укажем wal_level для работы логической репликации. Для примера ограничимся одной таблицей в базе данных postgres.
$ PGPORT=5400; initdb -U postgres -D ./primary -c port=5400 -c wal_level=logical
$ pg_ctl start -D ./primary -l ./primary/log
primary=# CREATE TABLE test(id int);
primary=# INSERT INTO test (id) VALUES (1);
Физическая реплика standby будет работать на порту 5401.
$ pg_basebackup -d 'port=5400 user=postgres' -c fast -R -D ./standby
$ echo 'port=5401' >>./standby/postgresql.auto.conf
$ pg_ctl start -D ./standby -l ./standby/log
standby=# SELECT pg_is_in_recovery();
-------------------
t
(1 row)
Перед конвертацией реплику нужно остановить.
$ pg_ctl stop -D ./standby
Утилита pg_createsubscriber запускается на сервере с физической репликой.
$ pg_createsubscriber -d postgres -U postgres -D ./standby -P 'port=5400 user=postgres'
По ходу своей работы утилита выдает много диагностических сообщений о выполняемых действиях. Если конвертация завершилась без ошибок, то можно запускать логическую реплику.
$ pg_ctl start -D ./standby -l ./standby/log
В базе данных postgres на сервере primary создана публикация для всех таблиц.
primary=# \x \dRp
Expanded display is on.
List of publications
-[ RECORD 1 ]------------------------------
Name | pg_createsubscriber_5_d3ba035a
Owner | postgres
All tables | t
Inserts | t
Updates | t
Deletes | t
Truncates | t
Via root | f
primary=# INSERT INTO test (id) VALUES (2);
А на сервере standby в этой же базе данных создана подписка на публикацию.
standby=# SELECT * FROM test;
id
----
1
2
(2 rows)
standby=# \x \dRp
Expanded display is on.
List of subscriptions
-[ RECORD 1 ]---------------------------------
Name | pg_createsubscriber_5_dec1faeb
Owner | postgres
Enabled | t
Publication | {pg_createsubscriber_5_dec1faeb}
Логические слоты: отслеживание причин конфликтов репликации
commit: 007693f2, 6ae701b4
В представлении pg_stat_replication в столбце conflicting можно получить информацию о том, что логический слот недействителен, а в столбце invalidation_reason ― узнать причину. В настоящий момент отслеживаются следующие причины: wal_removed, rows_removed, wal_level_insufficient. Их описание есть в документации.
pg_basebackup -R: dbname в primary_conninfo
commit: a145f424
В статье о сентябрьском коммитфесте уже говорилось о новой возможности указывать при запуске pg_basebackup и pg_receivewal параметр dbname.
В продолжении этой работы, если при запуске pg_basebackup с параметром -R в строке подключения была указана база данных, то dbname будет включена в параметр primary_conninfo при записи в postgresql.auto.conf. Это нужно для облегчения настройки реплики с синхронизацией слотов репликации.
Синхронизация логических слотов репликации между мастером и репликой
commit: c393308b, 73292404, 776621a5, ddd5f4f5, 93db6cbd, bf279ddd, a11f330b, 2ec005b4, 6d49c8d4, 6f132ed6
Цель ― обеспечить бесперебойную работу логической репликации при переключении публикующего сервера на реплику. Проблема в том, что логические слоты не переносятся на реплики, поэтому в случае сбоя или планового переключения сервера публикации на реплику, логическая репликация на подписчиках не может быть продолжена.
Решение заключается в том, чтобы логические слоты переносить на реплику и поддерживать в актуальном состоянии. Тогда, после переключения на реплику, достаточно в настройках подписки указать строку подключения к новому серверу и продолжить получать изменения без потери данных.
Настроим три локальных сервера на разных портах:
- pub_primary ― публикующий сервер (порт 5400);
- pub_standby ― физическая реплика публикующего сервера (порт 5401);
- sub_server ― сервер, подписанный на публикацию с pub_primary (порт 5402).
Требуется после перехода pub_primary на pub_standby переключить sub_server на получение изменений с pub_standby без потери данных.
pub_primary
При инициализации pub_primary сразу укажем требуемый для логической репликации wal_level.
$ PGPORT=5400; initdb -U postgres -D ./pub_primary -c port=5400 -c wal_level=logical -c cluster_name=pub_primary
$ pg_ctl start -D ./pub_primary -l ./pub_primary/log
Будущая реплика, pub_standby, должна работать через физический слот. Создадим его вручную:
pub_primary=# SELECT pg_create_physical_replication_slot('standby_slot');
Публикация состоит из одной таблицы в схеме public базы данных postgres. Столбец descr по умолчанию заполняется именем текущего кластера.
pub_primary=# CREATE TABLE test(
id int,
descr text DEFAULT current_setting('cluster_name')
);
pub_primary=# INSERT INTO test (id) VALUES (1) RETURNING *;
id | descr
----+-------------
1 | pub_primary
(1 row)
pub_primary=# CREATE PUBLICATION test_pub FOR TABLE test;
pub_standby
Реплику готовим из физической копии pub_primary, полученной pg_basebackup. Используем ранее созданный слот.
$ pg_basebackup -d 'port=5400 user=postgres dbname=postgres' -S standby_slot -R -D ./pub_standby
Утилита pg_basebackup с параметром -R подготовит файл postgresql.auto.conf с параметрами для запуска в качестве реплики. Добавим еще несколько:
$ echo 'port=5401' >>./pub_standby/postgresql.auto.conf
$ echo 'cluster_name=pub_standby' >>./pub_standby/postgresql.auto.conf
$ echo 'sync_replication_slots=on' >>./pub_standby/postgresql.auto.conf
$ echo 'hot_standby_feedback=on' >>./pub_standby/postgresql.auto.conf
Параметры port и cluster_name не требуют комментариев. А вот на новом параметре sync_replication_slots остановимся подробнее. Включение этого параметра приведет к запуску на реплике специального процесса slotsync worker
, отвечающего за создание на pub_standby логических слотов с pub_primary и их последующую постоянную синхронизацию. (Слоты можно синхронизировать и вручную, вызовом на реплике функции pg_sync_replication_slots.)
Обратите внимание на параметр dbname в строке подключения pg_basebackup. Для создания копии кластера указывать базу данных не обязательно. Параметр dbname был нужен для того, чтобы pg_basebackup перенес его в строку подключения primary_conninfo. Это требуется для синхронизации логических слотов, которые определяются на уровне базы данных.
Также на реплике должна быть включена обратная связь, для этого добавлен hot_standby_feedback.
Запускаем реплику и проверяем настройки:
pg_ctl start -D ./pub_standby -l ./pub_standby/log
pub_standby=# \dconfig primary_*|hot_*|sync_*|wal_level
List of configuration parameters
Parameter | Value
------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
hot_standby | on
hot_standby_feedback | on
primary_conninfo | user=postgres passfile='/home/pal/.pgpass' channel_binding=disable port=5400 sslmode=disable sslnegotiation=postgres sslcompression=0 sslcertmode=disable sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=disable krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable dbname=postgres
primary_slot_name | standby_slot
sync_replication_slots | on
wal_level | logical
(6 rows)
Новый процесс slotsync worker
уже работает:
SELECT datname, pid, wait_event_type, wait_event, state
FROM pg_stat_activity
WHERE backend_type = 'slotsync worker'
\gx
-[ RECORD 1 ]---+------------------------
datname | postgres
pid | 171422
wait_event_type | Activity
wait_event | ReplicationSlotsyncMain
state |
Но на pub_primary пока нет слотов логической репликации, поэтому синхронизировать нечего. Самое время перейти к созданию и настройке сервера-подписчика.
sub_server
Инициализация и запуск кластера:
$ PGPORT=5402; initdb -U postgres -D ./sub_server -c port=5402 -c cluster_name=sub_server
$ pg_ctl start -D ./sub_server -l ./sub_server/log
Создаем таблицу и подписываемся на получение изменений с pub_primary:
sub_server=# CREATE TABLE test(
id int,
descr text DEFAULT current_setting('cluster_name')
);
sub_server=# CREATE SUBSCRIPTION test_sub
CONNECTION 'port=5400 user=postgres dbname=postgres'
PUBLICATION test_pub WITH (failover = true);
У подписки важно включить новый параметр failover. Этот параметр устанавливает одноименный признак логическому слоту репликации, который будет создан на pub_primary. Дело в том, что с репликой синхронизируются только слоты с таким признаком. (У функции pg_create_logical_replication_slot также появился параметр failover для создания отказоустойчивого слота.)
Проверка синхронизации слотов
Проверим, что созданный логический слот на pub_primary имеет признак failover:
pub_primary=# SELECT slot_name, slot_type, database, failover, synced, inactive_since
FROM pg_replication_slots
WHERE slot_name = 'test_sub'
-[ RECORD 1 ]--+---------
slot_name | test_sub
slot_type | logical
database | postgres
failover | t
synced | f
inactive_since |
Этот слот появился на реплике и синхронизирован с pub_primary (synced), время последней синхронизации ― в столбце inactive_since:
pub_standby=# SELECT slot_name, slot_type, database, failover, synced, inactive_since
FROM pg_replication_slots
-[ RECORD 1 ]--+------------------------------
slot_name | test_sub
slot_type | logical
database | postgres
failover | t
synced | t
inactive_since | 2024-08-12 14:20:07.351486+03
Есть еще одна ситуация, о которой следует позаботиться. Асинхронная реплика может получать изменения позже, чем сервер-подписчик, например из-за сетевых задержек. Если в такой момент произойдет сбой сервера публикации и переключение на реплику, то подписчик не сможет продолжить получать изменения с нового сервера публикации, т. к. находится впереди него. Для гарантии того, что подписчик не убежит вперед, нужно на сервере публикации добавить имя физического слота в новый параметр synchronized_standby_slots:
pub_primary=# ALTER SYSTEM SET synchronized_standby_slots = 'standby_slot';
pub_primary=# SELECT pg_reload_conf();
Параметр представляет собой список физических слотов репликации. Процессы wal sender, обслуживающие логическую репликацию, будут отправлять данные подписчикам только после того, как все слоты из этого списка подтвердят получение записей WAL. В случае задержек на реплике подписчики не получат записи раньше.
Теперь все подготовительные действия выполнены. Добавим вторую строку в таблицу на сервере публикации:
pub_primary=# INSERT INTO test (id) VALUES (2) RETURNING *;
id | descr
----+-------------
2 | pub_primary
(1 row)
Сбой и переключение на реплику
Для имитации сбоя остановим pub_primary в режиме immediate:
$ pg_ctl stop -m immediate -D ./pub_primary
Перед переключением на реплику рекомендуется отключить подписку:
sub_server=# ALTER SUBSCRIPTION test_sub DISABLE;
Переводим реплику в режим чтения/записи:
$ pg_ctl promote -D ./pub_standby
Меняем строку подключения в подписке и включаем ее:
sub_server=# ALTER SUBSCRIPTION test_sub
CONNECTION 'port=5401 user=postgres dbname=postgres';
sub_server=# ALTER SUBSCRIPTION test_sub ENABLE;
Проверка работы логической репликации
Добавим в таблицу третью строку уже с нового сервера публикации:
pub_standby=# INSERT INTO test (id) VALUES (3) RETURNING *;
id | descr
----+-------------
3 | pub_standby
(1 row)
Сервер-подписчик продолжает получать изменения:
sub_server=# SELECT * FROM test;
id | descr
----+-------------
1 | pub_primary
2 | pub_primary
3 | pub_standby
(3 rows)
Краткие итоги. Чтобы сервер-подписчик продолжал получать изменения после переключения сервера публикации на физическую реплику, нужно сделать следующее:
- Логический слот на сервере публикации должен иметь признак failover. Для этого в свойствах подписки включаем параметр failover.
- Реплика должна работать через физический слот с включенной обратной связью.
- На реплике нужно включить параметр sync_replication_slots для запуска процесса
slotsync worker
. Этот процесс синхронизирует слоты с мастером (основным сервером публикации). - На сервере публикации рекомендуется добавить физический слот в synchronized_standby_slots. Это гарантирует, что подписчики не будут применять изменения раньше реплики.
- После переключения на реплику в свойствах подписки достаточно изменить строку подключения.
См. также
Postgres 17 highlight: Logical replication slots synchronization (Bertrand Drouvot)
Оптимизация логического декодирования вложенных транзакций
commit: 5bec1d6b
Логическое декодирование, а вместе с ним логическую репликацию, существенно оптимизировали для обработки большого количества вложенных транзакций.
Тесты разработчиков показали более чем 30-кратное ускорение логического декодирования транзакции, содержащей 100 000 вложенных транзакций.
libpq: неблокирующее прерывание запроса
commit: 61461a30
В libpq добавили новый, неблокирующий интерфейс прерывания запроса. Существующий вызов PQcancel объявлен устаревшим, т. к. он не только заставляет приложение ждать завершения запроса, но и небезопасен.
Новым интерфейсом смогут воспользоваться драйверы, работающие через libpq. В частности планируется внедрить неблокирующее прерывание запросов в psycopg 3.2.
См. также
Improved query cancellation in PostgreSQL 17 and Psycopg 3.2 (Denis Laxalde)
libpq: прямое подключение по TLS
commit: d39a49c1
Клиентские приложения, работающие через libpq, в строке подключения могут использовать новый параметр sslnegotiation.
Если указать sslnegotiation=direct при подключении к серверам версии не ниже 17 и с поддержкой ALPN, то клиент и сервер могут обойтись без предварительного согласования использования TLS, что сокращает время подключения.
По умолчанию прямое подключение отключено.
vacuumdb, clusterdb, reindexdb: обработка отдельных объектов в нескольких базах
commit: 648928c7, 1b49d56d, 24c928ad
Если в разных базах данных одного кластера есть одинаковые объекты, то их можно обработать за один вызов утилитами vacuumdb, clusterdb и reindexdb. Например, выполним очистку всех схем pg_catalog:
$ vacuumdb --all --schema=pg_catalog
vacuumdb: vacuuming database "demo"
vacuumdb: vacuuming database "postgres"
vacuumdb: vacuuming database "template1"
reindexdb: --jobs и --index одновременно
commit: 47f99a40
Утилита reindexdb имела ограничение на работу в параллельном режиме (--jobs) для переиндексации отдельных индексов, указанных в нескольких параметрах --index. Причина в том, что индексы одной таблицы нельзя перестраивать параллельно. Но утилита не могла одновременно перестраивать и индексы разных таблиц, указанных в --index.
Теперь ограничение снято, reindexdb можно запускать с параметрами --jobs и --index одновременно, при этом индексы одной таблицы будут обработаны в одном процессе.
psql: новая реализация FETCH_COUNT
commit: 90f51782
Встроенную переменную FETCH_COUNT в psql можно использовать для получения от сервера строк результата запроса не целиком, а фрагментами.
Как это работало? Выполнение запросов (SELECT) начиналось с объявления курсора и последующей серии команд FETCH FORWARD с заданным в FETCH_COUNT количеством строк.
Однако не все запросы, возвращающие наборы строк, можно выполнять с применением курсора. Например, команды DML с фразой RETURNING. Результаты таких запросов всегда возвращались целиком, вне зависимости от значения FETCH_COUNT.
В новой версии для реализации FETCH_COUNT вместо курсоров используется однострочный режим получения результатов libpq, что позволяет получать фрагментами результат любых команд.
pg_dump --exclude-extension
commit: 522ed12f
В 14-й версии у pg_dump появился параметр --extension, в котором можно указать, какие расширения должны попасть в копию. Теперь добавлен обратный параметр --exclude-extension ― расширения, которые не нужно включать в копию.
Резервное копирование и восстановление больших объектов
commit: a45c78e3, 959b38d7
Большинство объектов системного каталога предназначены для определения схемы данных: таблицы, представления, последовательности, типы данных, функции и т. д. Количество объектов этих типов в реальных системах может быть достаточно большим, но всё-таки оно обозримо.
Особняком стоят большие объекты (large objects) ― необычные объекты системного каталога, которые, по сути, являются пользовательскими данными. И таких данных может накопиться невероятно много. Переписка разработчиков началась с сообщения о невозможности выполнить обновление через pg_upgrade для базы данных с ~ 200 миллионами больших объектов. Утилита pg_restore, восстанавливающая объекты системного каталога в новом кластере, завершалась ошибкой. Каждый большой объект восстанавливается отдельной командой, а значит и отдельной транзакцией. Это приводит к очень быстрому прокручиванию счетчика транзакций и грозит его переполнением.
Кроме того, даже если pg_dump/pg_restore отрабатывают без ошибок, при увеличении количества больших объектов падает скорость работы. Утилита pg_dump включает каждый большой объект в оглавление резервной копии, раздувая копию и замедляя работу с оглавлением. А pg_restore не только значительно прокручивает счетчик транзакций, но и пишет в WAL каждую транзакцию с последующей синхронизацией с диском (fsync).
Для оптимизации работы pg_dump и pg_restore предприняты следующие меры. Утилита pg_dump группирует большие объекты в оглавлении резервной копии фрагментами по 1000 штук (первый коммит).
В pg_restore добавлен параметр --transaction-size (второй коммит). Как и следует из названия, параметр определяет количество команд SQL, группируемых при восстановлении в одну транзакцию. Это повысит скорость восстановления за счет снижения частоты записи в WAL с синхронизацией с диском, а также замедлит прокручивание счетчика транзакций. Сейчас, по умолчанию, каждая команда выполняется в отдельной транзакции, если не выбран параметр --single-transaction.
А pg_upgrade будет вызывать pg_restore с жестко зашитым значением --transaction-size=1000.
Обзоры 17-й версии на этом завершены. Надеемся, что PostgreSQL 17 выйдет по расписанию, без задержек.
А разработка PostgreSQL продолжается, уже завершен первый июльский коммитфест 18-й версии и начался второй ― сентябрьский. А значит есть что изучать и чем поделиться.
Комментарии (4)
oller
07.09.2024 07:18Когда будет уже инструмент анализатор-тюнинг, который по сборам метрик будет уметь или авто настраивать или давать четкие реккомендации
ptr128
Непонятно, почему заглохли попытки реализации undo log. Нужен очередной Uber?
Sleuthhound
Могу предположить, что консервативному ядру разработчиков postgres это не нужно, ведь тогда postgres лишится родовой травмы и все коммерческие версии postgres пострадают.
ptr128
Не складывается, так как после Uber снизились доходы как раз коммерческих версий PostgreSQL, тогда как донат community не только не снизился, а даже вырос.
А исходя из бритвы Хэнлона, Ваше предположение вообще оказывается на последнем месте.