This 2018 article explains physical storage for ordinary PostgreSQL tables.
Table Storage Layout
An ordinary table has its own storage and may also have a TOAST table:
Main table: stores row data, relkind='r'.
TOAST table: stores out-of-line values when needed, relkind='t'. Some tables have no TOAST table.
A table and each of its indexes are separate relations:
Main relation: stores tuples.
Index relation: stores index tuples.
Each relation may have four forks:
main: the relation’s main file, numbered 0
fsm: stores information about free space in the main fork, numbered 1
vm: stores information about visibility in the main fork, numbered 2
init: used for unlogged tables and indexes, numbered 3
Each fork is stored in one or more files. The default segment size is 1 GiB; it can be changed when PostgreSQL is built.
To account for a table’s storage, include these relations:
Main table’s main relation (single)
Main table’s indexes (multiple)
TOAST table’s main relation (single)
TOAST table’s index (single)
Include every fork that exists for each relation: main, fsm, vm, and init. The init fork applies to unlogged relations.
Getting Table’s Associated Relations
The following query lists relation OIDs. Forks share a relation OID; they do not have independent OIDs. The left join keeps tables that have no TOAST table:
Entire relation, including table, indexes, TOAST, etc.
pg_indexes_size(oid)
Space occupied by relation’s index portion
pg_table_size(oid)
Table forks and TOAST, including the TOAST index; excludes the main table’s indexes
pg_relation_size(oid)
Get size of a relation’s main file part (main fork)
pg_relation_size(oid, 'main')
Get relation’s main fork size
pg_relation_size(oid, 'fsm')
Get relation’s fsm fork size
pg_relation_size(oid, 'vm')
Get relation’s vm fork size
pg_relation_size(oid, 'init')
Get relation’s init fork size
For routine monitoring, group storage into table and index space. For ordinary tables, pg_table_size plus pg_indexes_size equals pg_total_relation_size.
The following identities describe the components; they are formulas, not a complete SQL query:
A TOAST table has one index. Use pg_total_relation_size(reltoastrelid) to include both the TOAST table and its index.
Example: Table and Related Relation Sizes
SELECToid,relname,relnamespace::RegNamespace::Textasnspname,relkindasrelkind,reltuplesastuples,relpagesaspages,pg_total_relation_size(oid)assizeFROMpg_classWHEREoid=ANY(array(SELECT16418asid-- main
UNIONALLSELECTindexrelidFROMpg_indexWHEREindrelid=16418-- index
UNIONALLSELECTreltoastrelidFROMpg_classWHEREoid=16418));-- toast
Wrap the query in a table-returning function for reuse. Replace the example OID with a table in your database:
CREATEORREPLACEFUNCTIONpg_table_size_detail(relationRegClass)RETURNSTABLE(idoid,pidoid,relnamename,nspnametext,relkind"char",tuplesbigint,pagesinteger,sizebigint)AS$$BEGINRETURNQUERYSELECTrel.oid,relation::oid,rel.relname,rel.relnamespace::RegNamespace::Textasnspname,rel.relkindasrelkind,rel.reltuples::bigintastuples,rel.relpagesaspages,pg_total_relation_size(rel.oid)ASsizeFROMpg_classrelWHERErel.oid=ANY(array(SELECTrelation::oidASid-- main
UNIONALLSELECTindexrelidFROMpg_indexWHEREindrelid=relation-- index
UNIONALLSELECTreltoastrelidFROMpg_classWHEREoid=relation));-- toast
END;$$LANGUAGEPlPgSQL;SELECT*FROMpg_table_size_detail(16418);
Each row’s size includes that relation’s attached storage. The main table row
already includes its indexes and TOAST, so do not sum this column. tuples and
pages come from planner statistics and can lag behind current data.