Привет, Хабр!
Сегодня рассмотрим, как работает fillfactor в PostgreSQL — тот самый параметр, который никто не трогает, пока таблицы не начинают раздуваться как на дрожжах. Разберём, зачем он нужен, что происходит при UPDATE, когда стоит менять его вручную и как не наломать дров.
Что такое fillfactor и зачем он вообще нужен
Ты можешь представить себе таблицу как стопку листов. На каждом листе — строки. А теперь представь, что тебе нужно вставить или обновить строку, но на листе больше нет места. Приходится создавать новый лист. И вот этот overhead, эта своебразная перепаковка страницы — она стоит дорого. Особенно в OLTP-нагрузках.
Вот тут полезен fillfactor.
fillfactor — это параметр, который говорит PostgreSQL: оставь немного места на каждой странице, пожалуйста, заранее. То есть если ты ставишь fillfactor = 80, то Postgres будет заполнять каждую страницу на 80%, оставляя 20% под будущие апдейты.
По дефолту fillfactor = 100. Т.е без запаса. А значит, если часто происходят UPDATE или HOT UPDATE, можно влететь в проблему — Postgres не сможет обновить строку “на месте” и вынужден будет делать tuple forwarding (и создавать новый tuple), что ведёт к фрагментации и росту таблицы.
Пример настройки:
CREATE TABLE users (
id serial PRIMARY KEY,
name text,
email text
) WITH (fillfactor = 80);Такую штуку можно и менять post-factum:
ALTER TABLE users SET (fillfactor = 70);Но это не перезаписывает существующие страницы. Только новые будут следовать новому правилу.
Так же у каждого индекса тоже есть свой fillfactor.
CREATE INDEX idx_email ON users(email) WITH (fillfactor = 90);UPDATE строки может породить ещё и обновление всех связанных индексов. А значит, fillfactor стоит трогать не только на таблицах, но и на индексах.
Что происходит при UPDATE и как связан fillfactor
Теперь откроем капот. Допустим, ты делаешь вот такой запрос:
UPDATE users SET name = 'Artem 2.0' WHERE id = 1;Если строка помещается на ту же страницу и никакие индексы не задеваются — Postgres пытается сделать HOT update. Это дешевенький способ обновления: он просто пишет новую версию строки на той же странице и помечает старую как dead tuple.
Но… чтобы это произошло, должно быть:
достаточно места на текущей странице;
не затронут ни один индекс (иначе нужно обновлять и его).
Если fillfactor = 100, то скорее всего места не будет. А значит будет обычный update:
создается новый tuple на новой странице
старый помечается как dead
индекс обновляется
таблица пухнет
Смотрим руками:
-- включаем track_io_timing для анализа
SET track_io_timing = on;
-- делаем VACUUM FULL для чистоты
VACUUM FULL users;
-- обновляем строку
UPDATE users SET name = 'Test 123' WHERE id = 1;
-- смотрим, что произошло
SELECT * FROM pg_stat_user_tables WHERE relname = 'users';Или вообще можно использовать pgstattuple, чтобы увидеть степень фрагментации:
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple('users');Если dead tuples — не очень, то fillfactor — хороший способ заранее расставить ловушки.
Когда стоит занижать fillfactor вручную
Не трогай fillfactor, если не уверен в профиле нагрузки. По дефолту он справедлив — 100%. Но вот несколько сценариев, где может спасти:
Частые UPDATE’ы фиксированных строк
Типичный кейс — статус заказа. Меняется несколько байт, но меняется часто.
Frequent UPSERT паттерны
Особенно если UPSERT делаютUPDATEчаще, чемINSERT.Суровые OLTP-нагрузки
Где каждый микросервис по 100 раз в секунду пишет в те же записи (а ты ещё решил shard'ить вручную, да?).Частые UPDATE по jsonb / text
Где строка может даже слегка вырасти.
И вот тут ты ставишь, скажем:
ALTER TABLE orders SET (fillfactor = 75);И наблюдаешь, как VACUUM стал бегать реже, таблица меньше пухнет, а latency запросов падает.
Но обратная сторона: таблица станет больше в среднем на 25%. Плотность хранения упадёт. Значит — больше страниц, больше I/O.
Три кейса
Частые UPDATE по полю статуса
Таблица orders. Поля: id, status, created_at, updated_at. В течение жизни статус может меняться 5–10 раз. А status — это VARCHAR(20).
Решение:
CREATE TABLE orders (
id serial PRIMARY KEY,
status varchar(20),
created_at timestamptz,
updated_at timestamptz
) WITH (fillfactor = 80);Почему 80:
20% страницы оставляем под «будущие жизни» строк. Этого хватает в большинстве случаев для 1–2 HOT-обновлений без создания новых tuple.
Таблица справочника, INSERT-heavy, UPDATE-редкий
Таблица products_catalog. В ней 10 миллионов строк. Новые товары добавляютс�� каждый день, но обновление — раз в полгода.
Решение:
CREATE TABLE products_catalog (
id bigint PRIMARY KEY,
name text,
category text,
price numeric
) WITH (fillfactor = 100);Почему 100:
Зачем оставлять свободное место, если обновлений почти нет? Мы хотим максимальную плотность, чтобы не тратить дисковое пространство зря. Это даст минимальный размер таблицы и лучший cache hit ratio по страницам.
А если поставим 80, то будем просто сжигать диск, увеличим размер таблицы на 20% — и ради чего? Ни latency, ни write amplification не улучшатся, наоборот.
Очередь задач, запись + удаление
Таблица jobs_queue. Каждая запись создаётся, обрабатывается и удаляется. Частые INSERT и DELETE, очень высокая скорость записи и удаления.
При частом DELETE страница остаётся с dead tuples, и без fillfactor Postgres не сможет их использовать под новые вставки.
Решение:
CREATE TABLE jobs_queue (
id serial PRIMARY KEY,
payload jsonb,
created_at timestamptz
) WITH (fillfactor = 70);Почему 70:
Postgres будет держать свободное место, чтобы быстрее использовать его под новые записи.
Еще можно добавить индекс с меньшим fillfactor, если часто используем выборку по дате:
CREATE INDEX idx_jobs_created_at ON jobs_queue (created_at) WITH (fillfactor = 90);Делитесь в комментариях, в каких кейсах вы вручную настраивали fillfactor, и что это вам дало. Уверен, у многих накопился интересный опыт — будет полезно всем.
Если вы читаете про fillfactor, значит, уже не боитесь заглянуть под капот PostgreSQL. А хотите пойти ещё дальше?
Приходите на открытые уроки, где эксперты разбирают реальные кейсы по оптимизации БД, индексам и работе с высокими нагрузками:
Особенности реализации запросов в PostgreSQL — 9 июля в 20:00
SQL против бардака в данных: поиск по шаблону и регулярные выражения — 15 июля в 20:00
Мониторинг Postgres: Prometheus+Grafana — 23 июля в 20:00
А ещё — пройдите вступительное тестирование, чтобы понять, насколько вы готовы к полноценному обучению и подойдёт ли курс «PostgreSQL для администраторов баз данных и разработчиков» именно вам.
