01 · How PostgreSQL Is Built: Clusters, Processes & Setup¶
Most SQL courses treat the database as a black box you send queries to. This course does the opposite. PostgreSQL is unusually transparent: almost everything it does is visible as an ordinary operating-system process, an ordinary file on disk, or a row in a catalog table you can query. Once you can see those pieces, things that look like magic later — VACUUM, replication, crash recovery — turn into mechanisms you can reason about.
This lesson gets a server running on your machine and then takes it apart.
Where this course starts
You should already be comfortable with everyday SQL: SELECT, joins, GROUP BY, creating
tables. If not, work through the
SQL Mastery Path first — this course builds
on it rather than repeating it.
Vocabulary: cluster, database, schema¶
PostgreSQL uses three nested containers, and the words are easy to mix up:
| Term | What it is | Created by |
|---|---|---|
| Cluster | One running server: one data directory, one port, one set of roles, many databases | initdb |
| Database | An isolated namespace inside the cluster. A connection belongs to exactly one database | CREATE DATABASE |
| Schema | A folder for tables, functions and types inside one database | CREATE SCHEMA |
"Cluster" here has nothing to do with multiple machines — it is an old term for "a collection of
databases managed by one server". Roles (users) are cluster-wide; tables are per-database. You
cannot join a table in database shop to a table in database billing with plain SQL — that is
one of the reasons most applications use one database with several schemas (lesson 4).
Installing PostgreSQL¶
Pick whichever route suits your machine. Every command in this course was run against PostgreSQL 18.6; anything version-specific is called out.
# the PGDG repository carries every supported major version
sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt install -y postgresql-18
Debian's packaging creates and starts a cluster for you and adds wrapper tools such as
pg_lsclusters and pg_ctlcluster.
Package managers usually create a cluster for you. To see how it works, create one by hand — it takes two commands and you can delete the directory afterwards.
Creating a cluster yourself¶
export LC_ALL=en_US.UTF-8 # see the pitfall below
initdb -D ~/pgdata -U postgres --auth=trust -E UTF8 --locale=en_US.UTF-8
pg_ctl -D ~/pgdata -l ~/pgdata.log start
psql -h localhost -U postgres -c "select version();"
initdb creates the data directory, the template databases and the postgres superuser.
--auth=trust means "no password for local connections" — acceptable on a laptop you alone use,
never on a server (lesson 5 replaces it).
When I first started the server for this course on macOS, it refused to start, and the log said:
FATAL: postmaster became multithreaded during startup
HINT: Set the LC_ALL environment variable to a valid locale.
Exporting LC_ALL=en_US.UTF-8 before pg_ctl fixed it. Two other real errors you may meet:
Unix-domain socket path "..." is too long (maximum 103 bytes)— your data directory orunix_socket_directoriespath is too deep. Use a shorter path, or connect over TCP with-h localhost.could not bind IPv4 address "127.0.0.1": Address already in use— another server already owns port 5432. Setport = 5433inpostgresql.confor stop the other server.
The process model: one process per connection¶
List the server's processes right after start-up:
44133 1 /opt/homebrew/Cellar/postgresql@18/18.6_1/bin/postgres -D .../pgdata
44134 44133 postgres: io worker 0
44135 44133 postgres: io worker 1
44136 44133 postgres: io worker 2
44137 44133 postgres: checkpointer
44138 44133 postgres: background writer
44140 44133 postgres: walwriter
44141 44133 postgres: autovacuum launcher
44142 44133 postgres: logical replication launcher
(The data directory path is shortened here.) The first process is the postmaster. Every other process is its child:
| Process | Job | Covered in |
|---|---|---|
| checkpointer | Periodically flushes dirty pages to data files and marks a safe restart point | Level 2 · 09 |
| background writer | Trickles dirty pages to disk so backends rarely have to | Level 2 · 09 |
| walwriter | Flushes the write-ahead log | Level 2 · 09 |
| autovacuum launcher | Starts autovacuum workers when tables need cleaning | Level 2 · 02 |
| logical replication launcher | Starts subscription workers | Level 4 · 04 |
| io worker | Performs asynchronous reads (new in PostgreSQL 18, io_method = worker) |
Level 4 · 01 |
Now connect with psql and ask the server the same question from the inside:
pid | backend_type
-------+------------------------------
44134 | io worker
44135 | io worker
44136 | io worker
44137 | checkpointer
44138 | background writer
44140 | walwriter
44141 | autovacuum launcher
44142 | logical replication launcher
44439 | client backend
(9 rows)
The new row, client backend, is your connection. When a client connects, the postmaster forks a
dedicated backend process for it, and that process does all the parsing, planning and executing for
that one session until it disconnects. SELECT pg_backend_pid(); tells you which one is yours.
This design has consequences you will meet throughout the course:
- A crash in one backend cannot corrupt another backend's memory — but the postmaster still restarts every backend to be safe, because they share memory.
- Each connection costs a process plus its private memory, so thousands of idle connections are expensive. That is why connection poolers exist (Level 4 · 02).
- Backends communicate through shared memory: the buffer cache (
shared_buffers), the lock table and the WAL buffers.
The data directory on disk¶
PG_VERSION base/ global/ pg_hba.conf pg_ident.conf pg_wal/ pg_xact/
pg_multixact/ pg_logical/ pg_replslot/ pg_stat/ pg_tblspc/ postgresql.conf
postgresql.auto.conf postmaster.opts postmaster.pid ...
The parts worth knowing now:
base/— one sub-directory per database, named by the database's OID.global/— cluster-wide catalogs such aspg_databaseandpg_authid(roles).pg_wal/— the write-ahead log, 16 MB segment files. Never delete files here by hand; a "disk full, let me clean up pg_wal" moment is a classic way to destroy a database.pg_xact/— commit status for every transaction (did transaction 1234 commit or abort?).postgresql.conf,postgresql.auto.conf,pg_hba.conf— configuration (lesson 5 and Level 4).
Watch an object go from SQL to a file:
\c shop
CREATE TABLE customers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL
);
INSERT INTO customers (email)
SELECT 'user' || g || '@example.com' FROM generate_series(1, 1000) g;
SELECT pg_relation_filepath('customers');
-rw------- 1 you staff 65536 Oct 11 11:54 .../base/16384/16386
-rw------- 1 you staff 24576 Oct 11 11:54 .../base/16384/16386_fsm
(Owner, group and path are shortened; the sizes are as observed.)
The table is that 64 kB file — eight 8 kB pages. The _fsm file is its free-space map. The
primary key index is a separate file again, which is why the two size functions disagree:
SELECT pg_size_pretty(pg_relation_size('customers')) AS heap,
pg_size_pretty(pg_total_relation_size('customers')) AS total;
Template databases¶
CREATE DATABASE shop does not build a database from nothing: it copies template1. Anything you
put in template1 (an extension, a table) appears in every database created afterwards.
template0 is a pristine copy that you should never modify; use it when you need a clean database
with a different encoding or locale, or when restoring a dump:
Copying a template requires that nobody is connected to it. If someone has a session open on
template1, CREATE DATABASE fails with "source database "template1" is being accessed by other
users".
Starting, stopping and reloading¶
pg_ctl -D ~/pgdata status
pg_ctl -D ~/pgdata reload # re-read config files without dropping connections
pg_ctl -D ~/pgdata stop -m fast # default mode: roll back sessions, checkpoint, exit
pg_ctl -D ~/pgdata restart
stop has three modes. smart waits for every client to disconnect (it can wait forever).
fast — the default — rolls back open transactions, disconnects clients, writes a checkpoint and
exits cleanly. immediate kills everything without a checkpoint; the next start performs crash
recovery from the WAL. Use immediate only when fast hangs.
From SQL, SELECT pg_reload_conf(); does the same as pg_ctl reload.
How It Actually Works¶
When pg_ctl start runs, it launches postgres -D <dir>. The postmaster reads the configuration,
allocates the shared-memory segment (buffer cache, WAL buffers, lock tables), writes its PID to
postmaster.pid so a second server cannot start on the same directory, and checks pg_control —
an 8 kB file recording the last checkpoint location and whether the previous shutdown was clean.
If it was not clean, the startup process replays WAL from the last checkpoint before accepting
connections. Then it forks the auxiliary processes and starts listening.
A connection arrives: the postmaster accepts the socket and forks a child. The child — not the
postmaster — performs authentication against pg_hba.conf, loads the target database's catalog
caches and then loops: read a query, parse it, rewrite it (views and rules), plan it, execute it,
send rows back. The postmaster stays deliberately simple, so that a bug in query execution cannot
take down the process that supervises everything.
Data files are read and written in 8 kB pages. A backend that needs a page first looks in
shared_buffers; on a miss it reads the page from the operating system (which has its own page
cache) into a free buffer. Changes are made in the buffer and recorded in the WAL; the data file
itself is written later by the background writer or checkpointer. That split — log first, data
later — is what lets PostgreSQL survive a crash, and Level 2 · 09 follows it in detail.
Common mistakes¶
- Confusing database and schema. Splitting one application across several databases makes cross-joins impossible; separate schemas in one database is usually what you wanted.
- Running as the operating-system root user.
initdbandpostgresrefuse to run as root on purpose; create or use a dedicatedpostgresOS user on servers. - Deleting WAL files to free disk space. WAL that has not been checkpointed is required for recovery. Find out why it is accumulating (often an abandoned replication slot — Level 4 · 03).
- Leaving
trustauthentication in place on anything reachable from a network. - Two servers, one port.
psqlconnects you to whichever server owns the port, which may not be the one you just configured.SELECT version(), current_setting('data_directory');settles it.
Exercise¶
- Create a throwaway cluster in a new directory on a non-default port (for example 5440).
- Connect, create a database named
lab, and record its OID. - Create a table, insert 10,000 rows, and find its file with
pg_relation_filepath. Check the file size withls -land compare it withpg_relation_size. - Open a second
psqlsession and find both backends inpg_stat_activity. Identify which PID is which withpg_backend_pid(). - Stop the server with
-m immediate, start it again, and read the log. Find the lines showing that the server noticed the unclean shutdown and ran recovery.