PostgreSQLI · Architecture and the Process ModelArchitecture
Inside the data directory
What you'll learn
- Identify the purpose of each top-level directory and file under PGDATA
- Resolve a table name to the file that stores it, and back again
- Read postmaster.pid and explain what each line records
- State why manual file manipulation inside PGDATA destroys recoverability
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 top level
$ ls -la $PGDATAdrwx------ 5 postgres postgres 4096 base
drwx------ 2 postgres postgres 4096 global
drwx------ 2 postgres postgres 4096 pg_commit_ts
drwx------ 2 postgres postgres 4096 pg_dynshmem
-rw------- 1 postgres postgres 5753 pg_hba.conf
-rw------- 1 postgres postgres 2681 pg_ident.conf
drwx------ 4 postgres postgres 4096 pg_logical
drwx------ 4 postgres postgres 4096 pg_multixact
drwx------ 2 postgres postgres 4096 pg_notify
drwx------ 2 postgres postgres 4096 pg_replslot
drwx------ 2 postgres postgres 4096 pg_serial
drwx------ 2 postgres postgres 4096 pg_snapshots
drwx------ 2 postgres postgres 4096 pg_stat
drwx------ 2 postgres postgres 4096 pg_stat_tmp
drwx------ 2 postgres postgres 4096 pg_subtrans
drwx------ 2 postgres postgres 4096 pg_tblspc
drwx------ 2 postgres postgres 4096 pg_twophase
-rw------- 1 postgres postgres 3 PG_VERSION
drwx------ 4 postgres postgres 4096 pg_wal
drwx------ 2 postgres postgres 4096 pg_xact
-rw------- 1 postgres postgres 88 postgresql.auto.conf
-rw------- 1 postgres postgres 32657 postgresql.conf
-rw------- 1 postgres postgres 36 postmaster.opts
-rw------- 1 postgres postgres 99 postmaster.pidNote the permissions before anything else. Every entry is 0700 or
0600, owned by the postgres user. This is not advisory: PostgreSQL
refuses to start if the data directory is group- or world-readable
beyond what it permits, because the files contain data the database is
responsible for protecting. A well-meaning chmod -R 755 on a data
directory produces a cluster that will not start, and the error message
is clear about why.
The entries that matter operationally:
| Entry | Holds | Why you would look at it |
|---|---|---|
base/ | One subdirectory per database, containing every table and index | Disk usage by database |
global/ | Shared catalogues: pg_database, pg_authid | Where cluster-wide objects live |
pg_wal/ | The write-ahead log | The directory that fills up (Part XII) |
pg_xact/ | Transaction commit status | Referenced by wraparound material (Part VIII) |
pg_replslot/ | Replication slot state | Why WAL is being retained (Part XIV) |
pg_tblspc/ | Symbolic links to tablespaces outside PGDATA | Data that is not under this directory at all |
pg_stat/ | Statistics persisted at shutdown | Why statistics survive a clean restart |
postgresql.conf | The main configuration | The one file you edit (Part III) |
postgresql.auto.conf | What ALTER SYSTEM wrote | Overrides the above; a frequent surprise |
PG_VERSION | The major version, as text | Identifying an offline data directory |
postmaster.pid | Lock file and runtime facts | Proving which server owns this directory |
PG_VERSION deserves a mention out of proportion to its three bytes.
When you are handed a data directory with no running server — a
restored backup, a disk recovered from a failed host — this file tells
you which PostgreSQL major version can read it. A data directory can
only be opened by the major version that created it, so this is the
first thing to read before installing anything.
Finding the file behind a table
Databases and tables are directories and files named by numbers, not by name. The mapping is available from the server.
$ psql -U postgres -c 'SELECT oid, datname FROM pg_database ORDER BY oid' oid | datname
-------+-----------
1 | template1
4 | template0
5 | postgres
16389 | shop
(4 rows)
-- and the directories present under base/
1
16389
4
5For an individual relation, pg_relation_filepath() gives the answer
directly and correctly, including for tables in tablespaces outside
PGDATA.
$ psql -U postgres -d shop -c "SELECT pg_relation_filepath('orders'), pg_size_pretty(pg_relation_size('orders'))" filepath | size
------------------+---------
base/16389/16390 | 0 bytes
(1 row)The number in the filename is the relfilenode, and it is not the
same thing as the table’s OID, though on a newly created table the two
usually match. Operations that rewrite a table — VACUUM FULL,
CLUSTER, some forms of ALTER TABLE — write a new file with a new
relfilenode and drop the old one. That is why the correct way to find a
table’s file is always to ask the server, never to record the number
and assume it is stable.
Going the other way, from a large file to the table responsible, is the more common need during a disk-space investigation:
# Which databases are consuming the space?
psql -U postgres -c \
"SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database ORDER BY pg_database_size(datname) DESC"
# Within one database, which relations?
psql -U postgres -d shop -c \
"SELECT relname, pg_size_pretty(pg_total_relation_size(c.oid)) AS total
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname NOT IN ('pg_catalog','information_schema')
AND c.relkind IN ('r','m')
ORDER BY pg_total_relation_size(c.oid) DESC LIMIT 20"
pg_total_relation_size() includes the table, its indexes and its
TOAST data; pg_relation_size() is the table’s main fork alone. The
difference between them is frequently large and is the subject of
Part VI.
Reading postmaster.pid
$ cat $PGDATA/postmaster.pid1
/var/lib/postgresql/18/docker
1787848884
5432
/var/run/postgresql
*
527564 5
readyLine by line: the postmaster’s PID, the data directory, the start time
as a Unix timestamp, the port, the Unix socket directory, the
listen_addresses value, the shared memory key and ID, and the current
status.
Two operational uses. First, it is how you determine which server owns a data directory — the PID and the data directory together, so a host running several clusters can be untangled. Second, its presence is how PostgreSQL prevents two postmasters from opening the same directory simultaneously, which would corrupt it immediately.
What a backup actually copies
The map above explains something that matters in Part XIII. A physical
backup copies PGDATA in its entirety — every database, the shared
catalogues, the configuration, the transaction status — plus the WAL
generated while the copy was running.
It does not automatically include tablespaces. pg_tblspc/ holds
symbolic links to directories elsewhere on the filesystem, and a naive
archive of PGDATA that does not follow those links produces a backup
that restores into a cluster with missing tables. Backup tools handle
this correctly; a hand-rolled tar frequently does not, and the
failure appears only at restore time.
This is the first appearance of a theme this course returns to repeatedly: a backup procedure is not verified by the backup completing.
Production discipline
- Read the data directory; do not write to it. The map is for
interpretation. The only file edited by hand is
postgresql.conf. - Ask the server for a relation’s path.
pg_relation_filepath()is correct across tablespaces and after rewrites; a remembered number is not. - Check
PG_VERSIONbefore touching an offline data directory. It tells you which major version can open it, and that determines what you install. - Never delete
postmaster.pidto make a startup error go away. PostgreSQL already removes genuinely stale lock files; a refusal means it found a reason. - Account for tablespaces when reasoning about backups and disk
usage.
pg_tblspc/means data may not be underPGDATAat all.
Cross-course references
- Linux for Production Sysadmins — Part XIII (Disks) and Part XVIII (Storage) cover the filesystem layer this directory sits on, and Part III (Files) covers the permission model PostgreSQL enforces on it.
- Ceph & Distributed Storage — Part XXXV (RBD architecture) covers what a block device beneath this directory is actually doing with the writes.
- Docker & Containers — Part VIII (Storage) covers volume lifetime, which determines whether this directory outlives the container that created it.
Quiz
Knowledge check · 6 questions
Q1. A PostgreSQL server refuses to start, reporting a lock file left by a previous run. What is the correct interpretation?
Q2. You recorded that a table's data lives in base/16389/16390. A month later, after a VACUUM FULL was run on it, that path no longer contains the table's data. Why?
Q3. Which statements about PGDATA are correct? Select all that apply.
Q4. pg_relation_size() and pg_total_relation_size() return the same value for a table that has no indexes.
Q5. During a disk-space investigation you need to find which table is consuming the most space in a database. Name the function to use and explain why reading directory listings under base/ is the wrong approach.
Q6. Assess what happened and what must be checked before the cluster is trusted.
A host running PostgreSQL 18 ran out of inodes overnight. An engineer, working to free space, noticed that the data directory was owned by postgres and not readable by the monitoring user, ran chmod -R 755 on PGDATA so the monitoring agent could read it, and then restarted the service to clear the alert. The service now fails to start. Separately, the engineer reports having deleted several files from pg_wal that appeared old, and having removed postmaster.pid when the first restart attempt complained about it.
Passing score: 75%. Answers are checked in this browser.