PostgreSQL 19 Beta 4: пять изменений, которые я бы проверил на staging до релиза

24 сентября вышла PostgreSQL 19 Beta 4. До release candidate осталось совсем немного, и на этом этапе уже интереснее смотреть не на длинный список новых возможностей, а на те изменения, которые реально способны поменять повседневную работу backend-команды.
В релизе есть заметные вещи вроде REPACK, нового WAIT FOR LSN, параллельного autovacuum, репликации sequence и pg_plan_advice. Но почти у каждой из них есть важное «да, но». REPACK (CONCURRENTLY) не означает «VACUUM FULL без блокировок». WAIT FOR LSN не превращает асинхронную реплику в синхронную. Планировщик теперь можно подталкивать, но это не делает hint-ы хорошей идеей по умолчанию.
Именно поэтому вместо обзора «50 новых фич PostgreSQL 19» я выбрал пять изменений, которые стоит прогнать на собственном staging до GA. Не чтобы немедленно включить их в production, а чтобы заранее понять, какие старые костыли после обновления можно будет убрать, а где появятся новые точки контроля.
Важная оговорка: PostgreSQL 19 сейчас находится в beta. PostgreSQL Project прямо не рекомендует использовать beta-релизы в production. Более того, в Beta 4 несколько крупных возможностей были удалены из релиза, включая SQL/PGQ, online-переключение checksums,
FOR PORTION OFдля UPDATE/DELETE и merge/split partitions. Это хороший повод тестировать именно актуальную Beta 4, а не ориентироваться на июньские обзоры Beta 1.
1. REPACK: наконец штатный путь между VACUUM и «остановите мир»
Старый неприятный сценарий знаком почти всем, кто долго живёт с большой PostgreSQL-базой. Таблица пережила массовые UPDATE/DELETE, физический файл раздулся, обычный VACUUM сделал место повторно используемым внутри таблицы, но операционной системе его не вернул.
VACUUM FULL возвращает место, но переписывает таблицу и требует ACCESS EXCLUSIVE. На горячей таблице это часто означает maintenance window или отдельную процедуру с внешними инструментами.
В PostgreSQL 19 появляется отдельная команда:
REPACK orders;Она переписывает таблицу в новый файл и возвращает неиспользуемое место ОС. Без дополнительных опций поведение с точки зрения доступности всё ещё жёсткое: на время операции берётся ACCESS EXCLUSIVE.
Интереснее другой вариант:
REPACK (CONCURRENTLY) orders;При concurrent-режиме PostgreSQL сначала создаёт новые файлы таблицы и индексов, а изменения, которые приходят параллельно, отслеживает через logical decoding. Эксклюзивная блокировка всё равно нужна — но в основном на финальный swap старых и новых файлов.
Это уже намного ближе к тому, что обычно хочется от обслуживания большой production-таблицы.
Но я бы не превращал это в cron сразу после обновления.
У CONCURRENTLY есть ограничения: таблица должна иметь подходящую replica identity, нельзя использовать режим для partitioned и unlogged tables, нужен дополнительный replication slot, операция требует дополнительного дискового пространства. И главное: короткий ACCESS EXCLUSIVE — всё ещё ACCESS EXCLUSIVE.
Если во время копирования таблицы накопилось много изменений, PostgreSQL должен применить их перед swap. Поэтому «concurrent» здесь означает «основную часть времени не блокируем DML», а не «блокировок гарантированно не будет».
Для backend-команды я бы тестировал не только время REPACK, а четыре вещи:
peak temporary disk usage
+
WAL / replication lag
+
длительность финального ACCESS EXCLUSIVE
+
поведение при активном DDLНа таблице в 20 ГБ результат может быть прекрасным, а на горячей таблице в несколько сотен гигабайт эксплуатационный профиль окажется совсем другим.
Ещё одна приятная деталь: прогресс можно смотреть через pg_stat_progress_repack. То есть операция изначально проектируется как наблюдаемая, а не как «запустили и ждём».
2. WAIT FOR LSN закрывает старую дыру primary → replica
Один из самых практичных паттернов масштабирования чтения выглядит просто:
WRITE -> primary
READ -> replicaА потом пользователь меняет имя, сразу открывает профиль и видит старое значение.
Это не баг реплики. Это обычная eventual consistency: HTTP-запрос с UPDATE уже завершился на primary, а WAL ещё не успел примениться на standby.
Обычно приложение решает это одним из трёх способов: некоторое время после записи читает с primary, тащит sticky-флаг в сессии либо строит собственный механизм ожидания replication lag.
В PostgreSQL 19 появляется WAIT FOR LSN.
После записи на primary приложение получает позицию WAL:
UPDATE users
SET display_name = 'Alice'
WHERE id = 42;
SELECT pg_current_wal_insert_lsn();
-- например 0/0306EE20Перед зависимым чтением на replica можно выполнить:
WAIT FOR LSN '0/0306EE20'
WITH (
MODE 'standby_replay',
TIMEOUT '200ms',
NO_THROW
);Результат — success, timeout или not in recovery.
Если получили success, standby уже replay-нула WAL до нужной позиции и зависимое чтение может идти туда. Если timeout, приложение может уйти на primary.
Получается довольно аккуратный routing:
async def consistent_read(lsn, replica, primary):
status = await replica.wait_for_lsn(
lsn,
timeout_ms=200,
)
if status == "success":
return await replica.fetch_user()
return await primary.fetch_user()Смысл не в конкретном Python API — драйверу всё равно придётся выполнить SQL-команду. Важен контракт: вместо «надеемся, что replica догнала» у нас появляется конкретная позиция WAL.
Но здесь тоже есть ловушка. LSN нужно получить после commit, на уровне приложения или pooler-а, и перенести между соединениями. PostgreSQL сам не знает, что следующий запрос пользователя логически связан с предыдущим.
Кроме того, WAIT нельзя бездумно поставить в середину длинной транзакции. Документация отдельно ограничивает работу со snapshot и locks, потому что standby replay сама может ждать блокировки, удерживаемой сессией, которая одновременно ждёт replay. Получается классический цикл ожидания.
Практический паттерн поэтому такой:
primary:
transaction -> COMMIT -> capture LSN
replica:
WAIT FOR LSN
-> success -> SELECT
-> timeout -> fallback to primaryДля API с read replicas это, пожалуй, одна из самых полезных возможностей PostgreSQL 19.
3. Autovacuum теперь не только решает «что вакуумить», но и «что важнее»
На больших базах проблема autovacuum часто не в том, что он выключен или неправильно настроен. Проблема в очереди.
Представим несколько крупных таблиц, которые почти одновременно пересекли свои thresholds. Worker-ы заняты, а следующая таблица ждёт. До PostgreSQL 19 порядок выбора внутри базы был гораздо менее выразительным с точки зрения реального риска.
В 19 появляется scoring system. PostgreSQL оценивает кандидатов по нескольким причинам: возраст XID, multixact, объём UPDATE/DELETE, INSERT и необходимость ANALYZE. Весами можно менять приоритет компонентов.
Появилась и диагностическая view:
SELECT *
FROM pg_stat_autovacuum_scores
ORDER BY score DESC;Это маленькое изменение с большим эксплуатационным эффектом: теперь вопрос «почему autovacuum пошёл именно в эту таблицу?» становится наблюдаемым.
Параллельно autovacuum научился использовать parallel workers для обработки индексов. Глобальный предел задаётся через:
autovacuum_max_parallel_workersа для конкретной таблицы можно использовать storage parameter autovacuum_parallel_workers.
Важно не спутать это с магическим «VACUUM стал N раз быстрее». Parallel Vacuum распараллеливает прежде всего index vacuuming и cleanup, причём отдельный индекс должен быть достаточно большим, а число реально запущенных workers может оказаться меньше настроенного.
То есть для таблицы с одним небольшим индексом эффект может быть нулевым. Для большой таблицы с несколькими тяжёлыми индексами — уже заметным.
Я бы после перехода на 19 не переносил старый autovacuum config вслепую. Сначала собрал бы baseline на текущей версии: длительность vacuum, lag по dead tuples, I/O, случаи wraparound pressure. Потом прогнал бы тот же workload на 19 и посмотрел, как изменились порядок обработки и конкуренция за I/O.
Новая автоматика полезна только тогда, когда мы видим, что именно она делает.
4. Logical replication наконец знает про sequence — но не так, как кажется
При логической миграции базы есть классическая мелкая неприятность.
Таблицы синхронизировались, изменения продолжают ехать, переключение почти готово. А потом выясняется, что sequence на subscriber осталась позади.
После cutover новый nextval() может выдать значение, которое уже использовалось на старом primary.
В PostgreSQL 19 sequence можно публиковать:
CREATE PUBLICATION app_pub
FOR ALL TABLES, ALL SEQUENCES;При создании subscription с copy_data = true начальные значения sequence синхронизируются. Есть и отдельное обновление:
ALTER SUBSCRIPTION app_sub
REFRESH SEQUENCES;Звучит как «sequence теперь реплицируются постоянно», но это было бы слишком удобно.
Документация прямо отмечает: значения на publisher продолжают уходить вперёд, поэтому subscriber со временем снова может отстать. То есть механизм решает синхронизацию sequence, но не превращает каждый nextval() в потоковое logical-replication event.
Для миграции это всё равно сильно упрощает runbook. Вместо собственного скрипта, который обходит pg_sequences, вычисляет максимумы и делает setval(), появляется штатная операция.
Но перед cutover я всё равно оставил бы явный шаг:
stop writes
-> wait for table replication
-> REFRESH SEQUENCES
-> validate sequence state
-> switch trafficНовая возможность уменьшает количество самописного кода. Она не отменяет проверку состояния перед переключением.
5. pg_plan_advice: когда «плохой план» можно стабилизировать, но лучше не привыкать
Последняя возможность потенциально самая опасная именно потому, что выглядит очень заманчиво.
Есть запрос, который вчера выполнялся 20 мс, после изменения статистики стал выполняться 4 секунды. Вы смотрите EXPLAIN ANALYZE и видите, что planner выбрал неудачный join order или scan.
Раньше типичные варианты были такими: исправить статистику, переписать запрос, добавить индекс, менять planner settings на уровне session либо использовать внешние расширения для hints.
PostgreSQL 19 добавляет pg_plan_advice.
Можно получить или задать advice и ограничить выбор planner-а. Например:
SET pg_plan_advice.advice = 'JOIN_ORDER(f d)';
EXPLAIN
SELECT *
FROM join_fact f
JOIN join_dim d ON f.dim_id = d.id;Важно, как это реализовано: расширение не подменяет planner собственным планом. Оно ограничивает набор вариантов, которые core planner рассматривает.
Рядом есть pg_stash_advice, которое позволяет привязать advice к query identifier и применять его автоматически.
На первый взгляд это почти «plan pinning», которого PostgreSQL исторически избегал.
Я бы относился к нему как к аварийному стабилизатору, а не как к новому способу оптимизации запросов.
Если planner выбрал плохой план из-за неверной cardinality estimate, advice может быстро вернуть latency в норму. Но первопричина никуда не исчезает. Распределение данных завтра поменяется, старый «правильный» план станет плохим, а мы сами запретили planner-у адаптироваться.
Сама документация предупреждает: planner обычно принимает хорошие решения, и вмешательство легко ухудшает ситуацию. Для pg_stash_advice отдельно отмечена и дополнительная стоимость применения advice.
Я бы использовал такой workflow:
регрессия плана
|
v
зафиксировать хороший вариант advice
|
v
временно стабилизировать production
|
v
найти причину:
statistics / index / query / data skew
|
v
исправить причину
|
v
удалить adviceТо есть это хороший предохранитель, если он имеет срок жизни.
Что я бы реально проверил до обновления
У PostgreSQL major release редко бывает одна «killer feature», ради которой стоит немедленно обновлять production. Ценность обычно складывается из десятков небольших изменений. В 19 особенно заметно, что много работы сделано вокруг эксплуатации: vacuum, I/O, observability, replication, управление планами.
Поэтому тестовый стенд я бы строил не как «поднять PostgreSQL 19 и запустить unit-тесты», а как копию характерных неприятных сценариев.
Большая bloat-таблица — проверить REPACK (CONCURRENTLY), временное место, WAL и финальную блокировку.
Primary + async replica — измерить WAIT FOR LSN на p50/p95/p99 replication lag и выбрать разумный timeout для fallback.
Таблица с несколькими большими индексами — сравнить autovacuum с parallel workers и посмотреть pg_stat_autovacuum_scores.
Логическая миграция — проверить sequence sync и свой cutover runbook.
Несколько запросов с историей plan regressions — попробовать pg_plan_advice именно как временную страховку.
И отдельно стоит проверить то, что изменилось без новых SQL-команд. Например, в PostgreSQL 19 JIT теперь выключен по умолчанию: разработчики PostgreSQL сочли существующую cost-модель недостаточно надёжной. Для обычного OLTP это может пройти незаметно, а аналитические запросы, которые раньше выигрывали от JIT, стоит прогнать отдельно.
Почему Beta 4 важнее ранних обзоров PostgreSQL 19
Есть ещё одна причина посмотреть на релиз именно сейчас.
Между Beta 1 и Beta 4 PostgreSQL Project не только исправлял баги. Из релиза удалили несколько уже заявленных возможностей. В официальном announcement это объяснено довольно прямо: надёжность важнее попытки удержать фичу в конкретном major release.
Для production-разработчика это хороший reminder: планировать архитектуру по beta feature list рано.
Но тестировать beta — как раз вовремя. Не потому что через неделю нужно обновляться, а потому что после GA самое дорогое место — не установка пакета. Самое дорогое — понять, как новая версия ведёт себя именно на вашем workload.
PostgreSQL 19 выглядит интересным не потому, что в нём появилась одна революционная возможность. Скорее наоборот: несколько старых operational pain points получили встроенные механизмы, которые раньше приходилось собирать вокруг базы самостоятельно.
Если REPACK позволит убрать часть внешнего tooling, WAIT FOR LSN — упростить read-after-write на репликах, а autovacuum scoring — сделать обслуживание базы наблюдаемее, этого уже достаточно, чтобы обновление влияло не только на DBA, но и на архитектуру backend.
Главное — не путать появление встроенного механизма с исчезновением эксплуатационного риска.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.