Skip to main content
RunBook Academy

PostgreSQLVI · Storage, Pages and TOASTStorage

Relations, forks and segments

Intermediate⏱ ~25 minpsql

What you'll learn

  • Name the forks of a relation and what each one stores
  • Explain why a table is split into 1 GB segments
  • Distinguish pg_relation_size, pg_table_size and pg_total_relation_size
  • Attribute disk consumption to the correct relation and fork

Prerequisites

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

“The orders table is 40 GB” is an incomplete statement, because it does not say which of several things is being measured. A relation is stored as a set of files, each holding something different, and the indexes and TOAST data may be larger than the rows.

The forks

ForkFilenameHolds
main16390, 16390.1, …The rows themselves
fsm16390_fsmApproximate free space per page
vm16390_vmWhich pages are all-visible and all-frozen
init16390_initThe empty state of an unlogged relation
Read-only / Safethe forks of two real tables
$ psql -U postgres -c "SELECT relname, pg_size_pretty(pg_relation_size(oid,'main')) AS main, pg_size_pretty(pg_relation_size(oid,'fsm')) AS fsm, pg_size_pretty(pg_relation_size(oid,'vm')) AS vm FROM pg_class WHERE relname IN ('narrow','wide')"
 relname |  main   |  fsm  |   vm
---------+---------+-------+---------
narrow  | 3544 kB | 24 kB | 0 bytes
wide    | 464 kB  | 24 kB | 0 bytes
(2 rows)

The visibility map being empty is informative rather than a problem: those forks are created and populated by VACUUM, and neither table had been vacuumed. Part VIII returns to what that costs.

Segments

The main fork is split into files of 1 GB. A 3.5 GB table is 16390, 16390.1, 16390.2 and a partial 16390.3.

The historical reason was filesystem limits on maximum file size. The reason it survives is more practical: extending and truncating a relation touches one segment rather than one enormous file, and a filesystem is generally better at managing several large files than one gigantic one.

The operational consequence is that a large table appears in a directory listing as many files with numeric suffixes, and counting them is a rough size estimate. It also means ls -la on a data directory is not a useful way to find large tables, because a 400 GB table is 400 files rather than one obvious entry.

The three size functions

This is the distinction that decides whether a disk investigation finds the right table.

FunctionIncludes
pg_relation_size(oid)The main fork only, by default
pg_relation_size(oid, 'fsm')One named fork
pg_table_size(oid)Main + fsm + vm + TOAST
pg_indexes_size(oid)All indexes on the relation
pg_total_relation_size(oid)Table size + indexes: everything
Read-only / Safethe same three tables under each measure
$ psql -U postgres -c 'SELECT relname, pg_size_pretty(pg_relation_size(oid)) AS main_fork, pg_size_pretty(pg_table_size(oid)) AS table_size, pg_size_pretty(pg_total_relation_size(oid)) AS total FROM pg_class WHERE relname IN (...)'
   relname   | main_fork | table_size | total
-------------+-----------+------------+--------
narrow      | 3544 kB   | 3568 kB    | 5776 kB
wide        | 464 kB    | 496 kB     | 560 kB
wide_random | 104 kB    | 16 MB      | 16 MB
(3 rows)

Three readings worth drawing out.

narrow is 3.5 MB of rows and 5.8 MB in total — the primary key index is nearly two-thirds the size of the data. That ratio is entirely normal and is the reason index size belongs in capacity planning.

wide_random has a 104 kB main fork and a 16 MB table. The rows are almost all somewhere else, and pg_relation_size reports the 104 kB. An investigation using that function would rank this table as one of the smallest in the database.

Finding the file behind a relation

# The path, relative to the data directory. Correct across tablespaces.
psql -U postgres -c "SELECT pg_relation_filepath('orders')"

# Every fork of that relation, with sizes
psql -U postgres -c \
  "SELECT fork, pg_size_pretty(pg_relation_size('orders', fork)) AS size
     FROM unnest(ARRAY['main','fsm','vm']) AS fork"

# The TOAST relation, if one exists
psql -U postgres -c \
  "SELECT c.relname, t.relname AS toast_relation,
          pg_size_pretty(pg_relation_size(t.oid)) AS toast_size
     FROM pg_class c LEFT JOIN pg_class t ON t.oid = c.reltoastrelid
    WHERE c.relname = 'orders'"

Going the other way — from a large file to the relation responsible — is the disk-full case, and it needs the relfilenode:

SELECT c.relname, c.relkind, n.nspname
  FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
 WHERE c.relfilenode = 16390;

That query returns nothing for a relation whose relfilenode was changed by a rewrite, and nothing for a file left behind by an interrupted operation, which is itself a useful signal.

Production discipline

  1. Use pg_total_relation_size for capacity questions. pg_relation_size reports the main fork and understates by a factor that can exceed a hundred.
  2. Compare total against heap to see immediately whether space is in rows, indexes or TOAST.
  3. Ask the server for a relation’s path each time. A rewrite changes the relfilenode and a cached path becomes wrong silently.
  4. Do not size tables from a directory listing. A large table is many 1 GB segments, and TOAST data is under a different name entirely.
  5. Budget free space for rewrites. Any operation that rewrites a relation needs room for a second complete copy.
  6. Remember index size in capacity planning. On the capture above a single primary key was nearly two-thirds the size of the data.

Cross-course references

  • Linux for Production Sysadmins — Part XIII (Disks) and Part XVIII (Storage) cover the filesystem beneath these files, and Part XLI (Disk performance) covers measuring their I/O.
  • Ceph & Distributed Storage — Part LXIII (Capacity) covers planning for a workload whose real size is larger than its apparent one, which is the same problem this lesson describes.
  • Observability for Production Sysadmins — Part LIX (Database observability) covers exporting per-relation sizes so growth is visible before it is urgent.

Quiz

Knowledge check · 6 questions

  1. Q1. A disk-space investigation ranks tables using pg_relation_size and finds nothing large enough to explain the usage. What is the most likely oversight?

  2. Q2. Why does VACUUM FULL on a 400 GB table require roughly 400 GB of free space?

  3. Q3. Which statements about relation storage are correct? Select all that apply.

  4. Q4. Listing the files in a database's base directory is a reliable way to identify the largest tables.

  5. Q5. Name the four forks a relation can have and say what each holds.

  6. Q6. Work the evidence and give the investigation order.

    A database filesystem is at 91% and rising by roughly 4 GB per day. A capacity report built on pg_relation_size lists the ten largest tables, totalling 180 GB against a database size of 620 GB. The team cannot account for the difference and suspects the reporting query is broken or that something outside PostgreSQL is consuming the space. The database has a documents table storing uploaded files as bytea, and every table has between two and six indexes.

Passing score: 75%. Answers are checked in this browser.