Привет, Хабр!
Сегодня рассмотрим, как работает 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 для администраторов баз данных и разработчиков» именно вам.
ссылка на оригинал статьи https://habr.com/ru/articles/923648/
Добавить комментарий