PostgreSQLI · Architecture and the Process ModelArchitecture
The operator's first instruments
What you'll learn
- Read pg_settings including source, context and pending_restart
- Query pg_stat_activity for the four states an operator acts on differently
- Use pg_stat_io to attribute I/O to a backend type and a context
- Assemble a first-five-minutes triage sequence and know what each query rules out
Prerequisites
Practice
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
PostgreSQL describes itself. Its configuration, its running sessions,
its locks, its I/O and its table statistics are all queryable through
ordinary SQL against catalogue views, which means that almost every
question asked in the opening minutes of an incident has an answer
available from a psql prompt.
This lesson covers the three views that answer the most questions per
keystroke. The rest of the course adds more — pg_locks in Part IX,
pg_stat_replication in Part XIV, pg_stat_statements in Part XVI —
but these three come first because they are where you start.
pg_settings: what is this server configured to do
SHOW work_mem gives you a value. pg_settings gives you the value
plus everything you need to reason about it: where it came from,
whether you can change it now, and whether a change you already made
has actually taken effect.
$ psql -U postgres -c "SELECT name, setting, source, pending_restart FROM pg_settings WHERE source <> 'default' ORDER BY source, name" name | setting | source | pending_restart
----------------------------+--------------------+--------------------+-----------------
application_name | psql | client | f
autovacuum_worker_slots | 16 | configuration file | f
DateStyle | ISO, MDY | configuration file | f
default_text_search_config | pg_catalog.english | configuration file | f
dynamic_shared_memory_type | posix | configuration file | f
listen_addresses | * | configuration file | f
max_connections | 100 | configuration file | f
max_wal_size | 1024 | configuration file | f
shared_buffers | 16384 | configuration file | f
wal_level | replica | command line | f
config_file | .../postgresql.conf| override | f
... (this cluster: 33 rows in total)Four columns carry most of the value.
source distinguishes a deliberate choice from a default. Filtering
to source <> 'default' is the fastest way to see what somebody
actually configured on a server you have just inherited, and it is a
much shorter list than the full parameter set.
context is the change-planning column from earlier in this part:
postmaster needs a restart, sighup needs a reload, superuser and
user can be set in a session.
pending_restart is the honesty column. It is t when the
configuration file holds a value that differs from the one the server
is running, for a parameter that needs a restart. This is the
difference between a change you made and a change that is in effect,
and it is the column that settles the question “did that change apply?”
sourcefile and sourceline name the file and line the value came
from, which matters once postgresql.auto.conf and include files are
in play. Part III builds on this.
# Has anything been edited but not yet restarted into effect?
psql -U postgres -c \
"SELECT name, setting, sourcefile, sourceline
FROM pg_settings WHERE pending_restart"
# Where did this specific value come from?
psql -U postgres -c \
"SELECT name, setting, source, sourcefile, sourceline
FROM pg_settings WHERE name = 'shared_buffers'"
pg_stat_activity: what is it doing right now
One row per backend, including the background processes. The columns
that decide your next action are backend_type, state,
wait_event_type and the two age calculations you derive from the
timestamps.
The single most useful query in this course:
psql -U postgres -c \
"SELECT pid,
usename,
application_name,
client_addr,
state,
wait_event_type || ':' || wait_event AS waiting_on,
now() - xact_start AS xact_age,
now() - state_change AS in_state,
left(query, 60) AS query
FROM pg_stat_activity
WHERE backend_type = 'client backend'
AND pid <> pg_backend_pid()
ORDER BY xact_start NULLS LAST"
Two details in that query are deliberate. Filtering to client backend
removes the background processes, which are permanently in an
Activity wait and would otherwise dominate the output — the reason
was covered in the background-processes lesson. Excluding
pg_backend_pid() removes your own session, which is always active and
always running the query you are looking at.
The four states, and why they need different responses
state | What it means | The right response |
|---|---|---|
active | Executing a query now | Look at wait_event: working, or blocked? |
idle | Connected, no transaction open | Holds a process and its memory; harmless to the database |
idle in transaction | Transaction open, doing nothing | Actively harmful. Holds locks and blocks cleanup |
idle in transaction (aborted) | Same, after an error | Same harm; the client has not sent a rollback |
The distinction between idle and idle in transaction is the most
consequential thing on this page. An idle session is merely a
process. An idle in transaction session is holding a transaction
open, which means it is holding whatever locks it took, and — as
Part VII and Part VIII will show — it is preventing VACUUM from
reclaiming dead rows anywhere in the cluster, because a row version
that transaction might still need cannot be removed.
A single connection left idle in transaction overnight is a routine
cause of a table growing without bound while its row count stays flat.
# The query to run before you conclude anything about bloat or vacuum.
psql -U postgres -c \
"SELECT pid, usename, application_name, state,
now() - xact_start AS xact_age, left(query, 80) AS last_query
FROM pg_stat_activity
WHERE state LIKE 'idle in transaction%'
ORDER BY xact_start"
wait_event_type and wait_event turn active from one state into
many. A backend that is active might be computing, waiting on I/O,
waiting for a lock, or waiting on a client that has stopped reading.
Those are four different incidents. Part XVI covers wait events
systematically; the shape to remember now is that active alone tells
you almost nothing.
pg_stat_io: where is the I/O going
Introduced in PostgreSQL 16 and extended in 18, pg_stat_io breaks I/O
down by which kind of process did it, what it was operating on, and
why.
$ psql -U postgres -c 'SELECT backend_type, object, context, reads, writes, extends, hits FROM pg_stat_io WHERE reads > 0 OR writes > 0 ORDER BY backend_type' backend_type | object | context | reads | writes | extends | hits
---------------------+----------+----------+-------+--------+---------+-------
autovacuum launcher | relation | normal | 1 | 0 | | 0
autovacuum worker | wal | normal | | 11 | |
autovacuum worker | relation | normal | 137 | 0 | 9 | 11248
autovacuum worker | relation | vacuum | 40 | 0 | 0 | 126
checkpointer | relation | normal | | 389 | |
checkpointer | wal | normal | 0 | 2 | |
client backend | relation | normal | 334 | 0 | 0 | 15381
client backend | relation | bulkread | 215 | 0 | | 345
client backend | wal | normal | 0 | 38 | |
standalone backend | relation | normal | 479 | 1038 | 598 | 92643
(10 rows)The question this view answers that no host-level tool can: which
part of PostgreSQL is generating this I/O, and for what purpose.
iostat shows a busy device. This shows that the writes are the
checkpointer rather than the application, or that the reads are an
autovacuum worker rather than a query.
context is the column that repays study:
normal— ordinary buffered access through the shared pool.vacuum— work done by vacuum, using its own restricted buffer ring.bulkread— large sequential reads, also using a restricted ring so they cannot evict the working set.bulkwrite— bulk loading operations such asCOPY.
A low hit rate in bulkread or vacuum context is the ring buffer
working correctly, not a cache problem. Reading a pooled hit ratio
without this breakdown is how the wrong conclusion gets reached, which
was the subject of the buffer-pool lesson.
In PostgreSQL 18 this view gained read_bytes, write_bytes and
extend_bytes, replacing the single op_bytes column that 16 and 17
carried. A monitoring query written against 16 or 17 that selects
op_bytes will fail on 18 — the same class of post-upgrade breakage as
the pg_stat_bgwriter column moves, and worth checking at the same
time.
A first-five-minutes sequence
The value of a fixed sequence is that each step rules something out, so you are narrowing rather than wandering.
# 1. Is the server up and accepting connections at all?
pg_isready
# 2. Is it in recovery? A standby, or still replaying after a crash.
psql -U postgres -tAc 'SELECT pg_is_in_recovery()'
# 3. How many sessions, and in what states?
psql -U postgres -c \
"SELECT state, count(*) FROM pg_stat_activity
WHERE backend_type = 'client backend' GROUP BY state ORDER BY count(*) DESC"
# 4. Is anything blocked, and how long has the oldest transaction been open?
psql -U postgres -c \
"SELECT pid, state, wait_event_type, wait_event,
now() - xact_start AS xact_age, left(query, 60) AS query
FROM pg_stat_activity
WHERE backend_type = 'client backend' AND xact_start IS NOT NULL
ORDER BY xact_start LIMIT 10"
# 5. Where is the disk going?
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"
Step 2 is the one people omit and regret. pg_is_in_recovery()
returning true means this server is a standby or is still replaying,
and a great many symptoms — writes being refused, data appearing stale,
queries being cancelled — have that single explanation. Establishing it
in one query at the start saves a long detour.
Production discipline
- Check
pending_restartafter every configuration change. It is the one column that distinguishes an intended change from an applied one. - Filter
pg_stat_activitytoclient backendbefore counting anything. Background processes are permanently in anActivitywait and will distort any count that includes them. - Treat
idle in transactionas a finding, never as noise. It holds locks and blocks cleanup cluster-wide, and it explains symptoms that appear entirely unrelated. - Capture
application_name,client_addrand transaction age before terminating anything. Terminating removes the evidence of which code path is responsible. - Read statistics as two samples, not one number. They are
cumulative since
stats_reset, and a crash resets them to zero. - Run
pg_is_in_recovery()early. It explains a large family of symptoms in a single query.
Cross-course references
- Observability for Production Sysadmins — Part LIX (Database observability) covers turning these views into exported metrics, and Part XII (PromQL foundations) covers computing rates from cumulative counters, which is precisely what these views require.
- Linux for Production Sysadmins — Part XLI (Disk performance)
covers the host-level I/O picture that
pg_stat_ioattributes to a cause. - Git, CI/CD & GitOps — Part CVI (Change management) covers
recording a configuration change so that
pending_restarthas something to be reconciled against.
Quiz
Knowledge check · 6 questions
Q1. An operator raised shared_buffers in postgresql.conf four hours ago and reloaded the configuration. Which single query establishes whether the new value is actually in effect?
Q2. A table has grown steadily for a week while its row count has stayed flat, and autovacuum is enabled and running. Which pg_stat_activity finding would best explain this?
Q3. Which of these are true about the PostgreSQL cumulative statistics views? Select all that apply.
Q4. A low buffer hit ratio reported under the bulkread context in pg_stat_io is expected behaviour rather than a sign that shared_buffers is too small.
Q5. Name two filters that should be applied to pg_stat_activity before counting sessions, and explain what each one removes.
Q6. Run the triage and state what the evidence supports.
At 03:00 an application reports database timeouts. The database process is running and pg_isready reports the server is accepting connections. pg_stat_activity grouped by state shows 4 active, 11 idle, and 87 idle in transaction. The oldest transaction started at 22:14 the previous evening. pg_stat_io shows autovacuum worker rows with substantial reads in vacuum context. Host CPU is 20% and disk utilisation is moderate. The application team reports that a deployment went out at 22:10.
Passing score: 75%. Answers are checked in this browser.