6.8.3. HOT-обновления в PostgreSQL и управление нагрузкой#
6.8.3.1. Область применения#
Документ ориентирован на технические таблицы ERP-систем:
Таблицы свёрток и агрегатов.
Регистры и промежуточные расчётные данные.
Таблицы состояний, балансов, накоплений.
Staging- и buffer-таблицы.
Для таких таблиц характерны:
Большое количество операций
UPDATE.Повторное обновление одних и тех же строк.
Высокая чувствительность к bloat, WAL и частоте
VACUUM.
Для пользовательских таблиц (справочники, транзакции) HOT обычно не критичен и редко требует настройки.
6.8.3.2. Как работает UPDATE в PostgreSQL#
6.8.3.2.1. Ключевой факт#
Примечание
Любой обычный UPDATE в PostgreSQL обновляет все индексы таблицы, даже если изменяемые столбцы в индексах не участвуют.
Это следует из MVCC-модели: при обновлении создаётся новая версия строки с новым TID (ctid). Поскольку индексы хранят ссылки на TID, они обязаны быть обновлены.
6.8.3.2.2. Почему так происходит#
UPDATEне изменяет строку на месте.Создаётся новая версия строки.
Новая версия получает новый TID.
Все индексы ссылаются на TID, а не на данные.
Следовательно, даже обновление неиндексированного числового поля приводит к обновлению всех индексов.
6.8.3.2.3. Иллюстрация обычного UPDATE#
Heap
+-+
| (5,12) version 1 |
+-+
| (8,42) version 2 | <-- новая версия строки
+-+
Indexes (любые, даже 100 штук)
idx_a -> (5,12) [устарело]
idx_b -> (5,12) [устарело]
idx_c -> (5,12) [устарело]
После UPDATE:
idx_a -> (8,42) [актуально]
idx_b -> (8,42) [актуально]
idx_c -> (8,42) [актуально]
6.8.3.3. HOT: единственное исключение#
HOT (Heap-Only Tuple) — оптимизация, позволяющая выполнять UPDATE без обновления индексов.
6.8.3.3.1. Суть HOT#
HOT возможен, если PostgreSQL может:
Сохранить старый TID в индексах.
Связать новую версию строки с предыдущей внутри одной heap-страницы.
6.8.3.3.2. UPDATE в HOT-режиме#
Heap page
+--+
| (17,3) version 1 | <-- индекс указывает сюда
| | |
| v HOT chain |
| (17,4) version 2 (HOT) |
+--+
Indexes
idx_a -> (17,3)
idx_b -> (17,3)
idx_c -> (17,3)
Индекс продолжает указывать на старый TID. При чтении PostgreSQL проходит HOT-цепочку и находит актуальную версию строки.
Примечание
Это единственная причина, по которой HOT не требует обновления индексов.
6.8.3.4. Когда HOT невозможен#
HOT невозможен, если индекс больше не может безопасно указывать на старый TID.
6.8.3.4.1. Expression-индексы#
CREATE INDEX ON t ((payload->>'status'));
Если UPDATE изменяет результат выражения, индекс обязан указывать на новую версию строки -> HOT невозможен.
6.8.3.4.2. Partial-индексы#
CREATE INDEX ON t(id) WHERE active;
Если UPDATE меняет значение условия WHERE, строка может быть добавлена в индекс или удалена из него -> HOT невозможен.
6.8.3.4.3. Недостаток места на heap-странице#
Даже при логической возможности HOT он не сработает, если:
fillfactor = 100;страницы плотно заполнены;
новая версия строки не помещается на той же странице.
В этом случае создаётся новая heap-строка -> HOT невозможен.
6.8.3.4.4. Триггеры, изменяющие индексируемые поля#
Типовой сценарий:
UPDATEменяет техническое поле.BEFORE UPDATE-триггер обновляетupdated_at.Столбец
updated_atиндексирован.
HOT невозможен, несмотря на «безопасный» SQL.
6.8.3.5. Диагностика: поиск таблиц без HOT#
6.8.3.5.1. Основной запрос#
Используйте статистику PostgreSQL для выявления проблемных таблиц:
SELECT
relname AS table_name,
n_tup_upd AS updates,
n_tup_hot_upd AS hot_updates,
round(
100.0 * n_tup_hot_upd / NULLIF(n_tup_upd, 0),
2
) AS hot_ratio_percent
FROM pg_stat_user_tables
WHERE n_tup_upd > 100000
ORDER BY hot_ratio_percent ASC;
6.8.3.5.2. Интерпретация результатов#
hot_ratio < 20%— таблица активно обновляется без HOT.hot_ratio 70–95%— оптимально для технических таблиц.hot_ratio = 0— HOT принципиально невозможен или таблица спроектирована неверно.
6.8.3.5.3. Косвенные признаки#
Большое количество «мёртвых» строк при активных UPDATE также указывает на отсутствие HOT:
SELECT
relname,
n_tup_upd,
n_dead_tup
FROM pg_stat_user_tables
WHERE n_tup_upd > 0
ORDER BY n_dead_tup DESC;
6.8.3.6. Управление HOT: fillfactor таблицы#
6.8.3.6.1. Что делает fillfactor#
Параметр fillfactor задаёт, сколько места PostgreSQL оставляет свободным на heap-страницах для будущих UPDATE.
Пример:
ALTER TABLE tech_table SET (fillfactor = 70);
Это означает, что ~30% страницы остаётся свободной, что повышает вероятность размещения новых версий строк на той же странице.
6.8.3.6.2. Рекомендуемые значения#
Тип таблицы |
fillfactor |
|---|---|
Пользовательские |
100 |
Технические |
60–80 |
Очень «горячие» |
50–60 |
Внимание
fillfactor — это компромисс между производительностью UPDATE и размером таблицы. Применяйте его только для таблиц с высокой нагрузкой.
6.8.3.6.3. Важное ограничение#
Команда:
ALTER TABLE ... SET (fillfactor = X);
Не перепаковывает существующие данные.
Эффект проявляется постепенно, по мере выполнения
UPDATE.Для немедленного эффекта используйте
VACUUM FULL,CLUSTERилиpg_repack.
6.8.3.7. Fillfactor индексов#
6.8.3.7.1. Что это и зачем#
Индексы (особенно B-tree) имеют собственный параметр fillfactor. По умолчанию он равен 90.
Внимание
fillfactor индекса не влияет на возможность HOT. HOT определяется только heap-страницей.
Однако при отсутствии HOT каждый UPDATE обновляет все индексы. Низкий fillfactor индекса снижает частоту page split и рост bloat.
6.8.3.7.2. Рекомендации#
Для технических таблиц с активными UPDATE:
Устанавливайте
fillfactorиндексов в диапазоне 70–80.Особенно актуально для крупных B-tree-индексов.
Пример:
ALTER INDEX idx_big SET (fillfactor = 75);
REINDEX INDEX idx_big;
Это не ускоряет HOT, но снижает ущерб, когда HOT невозможен.
6.8.3.8. Итоги#
Любой обычный
UPDATEобновляет все индексы таблицы.HOT — единственный механизм, позволяющий избежать этого.
HOT критичен для технических таблиц с массовыми
UPDATE.Основной рычаг управления HOT —
fillfactorтаблицы.Снижение
fillfactor— осознанный компромисс между размером и производительностью.fillfactorиндексов не влияет на HOT, но важен для контроля bloat при отсутствии HOT.