PostgreSQLVI · Storage, Pages and TOASTStorage
Relations, forks and segments
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
“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
| Fork | Filename | Holds |
|---|---|---|
| main | 16390, 16390.1, … | The rows themselves |
| fsm | 16390_fsm | Approximate free space per page |
| vm | 16390_vm | Which pages are all-visible and all-frozen |
| init | 16390_init | The empty state of an unlogged relation |
$ 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.
| Function | Includes |
|---|---|
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 |
$ 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
- Use
pg_total_relation_sizefor capacity questions.pg_relation_sizereports the main fork and understates by a factor that can exceed a hundred. - Compare total against heap to see immediately whether space is in rows, indexes or TOAST.
- Ask the server for a relation’s path each time. A rewrite changes the relfilenode and a cached path becomes wrong silently.
- 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.
- Budget free space for rewrites. Any operation that rewrites a relation needs room for a second complete copy.
- 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
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?
Q2. Why does VACUUM FULL on a 400 GB table require roughly 400 GB of free space?
Q3. Which statements about relation storage are correct? Select all that apply.
Q4. Listing the files in a database's base directory is a reliable way to identify the largest tables.
Q5. Name the four forks a relation can have and say what each holds.
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.