Architecture
Physical layout on disk
A PostgreSQL cluster is rooted at PGDATA. Relations in the default and global tablespaces live there, while user-defined tablespaces can place relation files elsewhere through pg_tblspc links. Relation forks use numeric file names and are split into segments as they grow.
In plain words
A PostgreSQL cluster is rooted at the data directory (usually called PGDATA). Relations in the default tablespace are stored under base/<database OID>/, and shared relations use global/. User-defined tablespaces can store relation files outside PGDATA; PostgreSQL reaches them through symbolic links in pg_tblspc/. Relation files use numeric names rather than table names.
With the standard build, large relation forks are cut into 1 GB segments (16390, 16390.1, 16390.2 …). Heap relations can also have helper forks such as the free space map (_fsm) and visibility map (_vm); unlogged relations also use an _init fork.
Other folders hold the write-ahead log (pg_wal), the record of which transactions committed (pg_xact), and objects shared by the whole server (global). Configuration files and a lock file (postmaster.pid) sit at the top.
Never delete files from pg_wal or pg_xact to free space. They look like logs but they are part of the database; removing them corrupts it.
For DBAs and operators
Map a table to its files
SELECT pg_relation_filepath('sales.orders'); -- base/16384/16402
SELECT oid, datname FROM pg_database; -- 16384 = appdb
SELECT relfilenode, reltablespace FROM pg_class
WHERE oid = 'sales.orders'::regclass;
-- the other way round: which relation owns file 16402?
SELECT pg_filenode_relation(0, 16402);
SELECT pg_size_pretty(pg_relation_size('sales.orders')) AS main_fork,
pg_size_pretty(pg_relation_size('sales.orders', 'fsm')) AS fsm,
pg_size_pretty(pg_relation_size('sales.orders', 'vm')) AS vm,
pg_size_pretty(pg_total_relation_size('sales.orders')) AS with_indexes_toast;What each entry is
| Path | Contents |
|---|---|
base/<dboid>/ | One directory per database; files named by relfilenode. Forks: main, _fsm, _vm, _init (unlogged relations). |
global/ | Shared catalogs (pg_database is file 1262) and pg_control, the cluster's control file (checkpoint location, state). Read it with pg_controldata. |
pg_wal/ | 16 MB WAL segments named TTTTTTTTXXXXXXXXYYYYYYYY (timeline, then the segment number split in two: the high 32 bits of the WAL position and the segment within that 4 GB range), plus archive_status/ and, since PostgreSQL 17, summaries/. |
pg_xact/, pg_subtrans/, pg_multixact/, pg_commit_ts/, pg_serial/, pg_notify/ | SLRU files: transaction status and friends (see SLRU caches). |
pg_tblspc/ | Symbolic links named by tablespace OID, pointing to LOCATION/PG_18_202506291/. |
pg_logical/, pg_replslot/, pg_snapshots/, pg_twophase/ | Logical decoding state, replication slots, exported snapshots, prepared transactions. |
pg_stat/ | Cumulative statistics saved at clean shutdown (in shared memory while running, since PostgreSQL 15). |
base/pgsql_tmp/ | Temporary files from sorts and hashes that spilled (or under temp_tablespaces). |
Segments and blocks
Each file is a sequence of 8 kB blocks (block_size). After 131072 blocks (segment_size = 1 GB) a new segment file .1, .2 … starts. Both are fixed at compile time. Block n lives in segment n / 131072 at offset (n % 131072) × 8192.
Configuration files
postgresql.conf is read first, then postgresql.auto.conf, which ALTER SYSTEM writes and which therefore wins. Debian and Ubuntu packages keep the config files in /etc/postgresql/18/main/; check SHOW config_file and SHOW hba_file, and SELECT * FROM pg_file_settings WHERE error IS NOT NULL for typos.
postmaster.pid
The lock file holds the postmaster PID, data directory, start time, port, socket directory, listen address, shared memory key and status (starting, ready, standby, stopping). A stale file after a crash is detected automatically; never delete it while a postmaster might still be running, or two servers can open the same files.
relfilenode is not always the OID: it changes after TRUNCATE, VACUUM FULL, CLUSTER, REINDEX and table-rewriting ALTER TABLE. Mapped catalogs (like pg_class itself) have relfilenode 0 in pg_class and are found via pg_filenode.map.
PostgreSQL 18 source trail: tablespace.c documents pg_tblspc, default/global tablespaces and the on-disk path; rel.h defines mapped relations.
For developers: inside the source
From relation to file name
relfilelocator.h:RelFileLocator= {spcOid,dbOid,relNumber}, the physical identity of a relation.relpath.h:ForkNumber(MAIN_FORKNUM,FSM_FORKNUM,VISIBILITYMAP_FORKNUM,INIT_FORKNUM) andTABLESPACE_VERSION_DIRECTORY(PG_18_202506291).relpath.c:GetRelationPath()buildsglobal/N,base/DB/Norpg_tblspc/SPC/PG_18_…/DB/N, adds_fsm/_vm/_initfromforkNames[], and names temporary relationstPROC_N(proc number of the owning backend).md.c: the magnetic-disk storage manager._mdfd_getseg()maps a block to segmentblkno / RELSEG_SIZEand_mdfd_segpath()appends.1,.2…;mdreadv()and, for AIO in 18,mdstartreadv()do the reads.smgr.cis the dispatch layer above it.
RELSEG_SIZE and BLCKSZ are compile-time constants in pg_config.h (configure --with-segsize/--with-blocksize, meson -Dsegsize/-Dblocksize); the read-only settings segment_size and block_size just report them, and pg_control records them so a server built differently refuses to start on the cluster.
Other files in PGDATA
- The directory list is
subdirs[]ininitdb.c. global/pg_control(XLOG_CONTROL_FILE) holdsControlFileDatafrompg_control.h, padded toPG_CONTROL_FILE_SIZE(8192); the server reads it withReadControlFile(), tools withget_controlfile()(controldata_utils.c).pg_filenode.map(RELMAPPER_FILENAME) inglobal/and each database directory, managed byrelmapper.c.- WAL file names come from
XLogFileName()inxlog_internal.h;postmaster.pidlines frompidfile.h;pgsql_tmpisPG_TEMP_FILES_DIR. - Unlogged relations: at startup after a crash,
ResetUnloggedRelations()(reinit.c) copies each_initfork over the main fork.
Experiment
- SQL:
pg_relation_filepath()andpg_filenode_relation()(dbsize.c; the reverse lookup usesRelidByRelfilenumber()). - Tools:
pg_controldata,pg_waldump,pg_checksums,oid2name. A build with-Dsegsize_blocks=…(meson, for testing) makes segment boundaries easy to hit. - Tests:
src/bin/initdb/t/001_initdb.pl,src/test/recovery/t/014_unlogged_reinit.pl; README:storage/smgr/README.
Parameters that shape this
data_directory config_file hba_file external_pid_file segment_size block_size wal_segment_size temp_tablespaces
Related topics
Logical structure Page layout Free space map and visibility map Write-ahead log (WAL) Commit log, hint bits and subtransactions TOAST Archiving and point-in-time recovery