StormaticsStormatics

How to Calculate Fillfactor for Your Tables

A small setting most teams either ignore or guess at. Here’s the quick version, then the full breakdown.

Key Takeaways

  • Fillfactor sets how full Postgres packs each table page on write, leaving the rest as free space for future updates to land in-place (HOT updates), which cuts WAL and I/O.
  • Starting formula: 100 − (average row size ÷ 8,192 × 100). Get average row size from pg_relation_size() and n_live_tup, run after a recent ANALYZE.
  • For heavily updated tables, go lower than the starting number to leave more room for repeated in-place rewrites.
  • Don’t default every table to 70 or 80. Calculate from the table’s real row size and update pattern, then adjust based on observed HOT rate and bloat.

A few weeks back we were tuning a table that was getting hammered with updates all day long. Query performance kept creeping up, WAL volume was higher than it should have been, and when we dug into it, one small setting turned out to be part of the problem: fillfactor. Most people either leave it at the default or set it to some round number they saw in a blog post once, without actually working out what the table needs. So we worked out how to calculate it properly. Here’s the process.

What is Fillfactor?

Fillfactor controls how much space Postgres fills up on each table or index page when data is first written. The unfilled portion is left as free space on purpose, for later updates.

That free space matters because it gives Postgres a place to put an updated row right next to the old one, on the same page, instead of having to move it somewhere else entirely.

Why Does it Matter?

For tables that get updated a lot, a well-tuned fillfactor can improve HOT updates and reduce I/O and WAL generation. HOT updates are cheaper because Postgres can update the row in place and avoid touching every index on the table.

But there is no single number that works for every table. It really depends on how big your rows are and how often they get updated.

How to Calculate it?

  1. Get the average row size using pg_relation_size() and n_live_tup from pg_stat_user_tables.
  2. Remember Postgres pages are 8 KB, or 8,192 bytes.
  3. Work out how much of that 8 KB page one average row takes up:

Average row size ÷ 8,192 × 100

  1. Subtract that percentage from 100. That gives you a starting fillfactor, roughly enough free space on the page for one more row’s worth of updates before Postgres has to write to a new page.
  2. If the table is updated often, go lower than that starting number. More free space means more room for future updates and a better chance of getting HOT updates.
  3. Keep an eye on HOT update rates, bloat, WAL generation, and how the table is actually being used, then adjust from there.

A Worked Example

Say the average row size on a table comes out to 2,300 bytes.

(2,300 ÷ 8,192) × 100 ≈ 28%

So the starting fillfactor works out to:

100 − 28 = 72

That is a reasonable starting point. But if the table is heavily updated, you would probably want to go lower than 72, maybe down to 50, to leave more room for rows to be rewritten in place more than once before the page fills up.

A Query to Get You Started

Here is a simple query that pulls average row size for a set of tables, so you have real numbers to work from instead of guessing:

SELECT

  relname AS table_name,

  n_live_tup,

  pg_relation_size(relid) AS total_bytes,

  pg_relation_size(relid) / NULLIF(n_live_tup, 0) AS avg_row_size_bytes,

  pg_size_pretty(pg_relation_size(relid) / NULLIF(n_live_tup, 0)) AS avg_row_size_pretty

FROM pg_stat_user_tables

WHERE relname IN (‘your_table_1’, ‘your_table_2’, ‘your_table_3’)

ORDER BY relname;

n_live_tup is an estimate, and pg_relation_size includes dead tuples, so a bloated table will inflate the average row size; run this after a recent ANALYZE. Then plug avg_row_size_bytes into the formula above and you’ll have a starting fillfactor based on real data instead of a number picked at random.

The important point

Do not just set fillfactor to 70 or 80 across every table and call it done. Work out a starting point from the table’s real row size and update pattern, then watch how it performs and adjust from there. It is a small setting, but on the right table it can make a real difference.

Leave A Comment