PostgreSQL in Depth Module 1 – How PostgreSQL works and how to run it
Creating a cluster: initdb, settings, ALTER SYSTEM
Create a PostgreSQL 18 cluster with initdb, start and reload it with pg_ctl, and change settings at server, database, role and session level.
What you will learn
- Create a PostgreSQL 18 cluster with initdb, choosing the encoding, locale provider and checksums deliberately
- Start, stop and reload a server with pg_ctl, and tell which settings need a restart
- Change a setting for the server, a database, a role, a session or one transaction
- Trace where a setting's current value came from with pg_settings and pg_file_settings
Before you start
What you’ll learn · Before you start
What you will learn
- Create a PostgreSQL 18 cluster with initdb, choosing the encoding, locale provider and checksums deliberately
- Start, stop and reload a server with pg_ctl, and tell which settings need a restart
- Change a setting for the server, a database, a role, a session or one transaction
- Trace where a setting's current value came from with pg_settings and pg_file_settings
Before you start
On this page
A report needs work_mem at 256MB and a migration needs lock_timeout at 2 seconds: put either value in
postgresql.conf and every one of the server’s 100 connections gets it. initdb creates a cluster with its
defaults, ALTER SYSTEM changes a value for the whole server, and narrower levels change it for one database, one
role, one session or one transaction; choosing the right level is most of configuring PostgreSQL well.
You will create a cluster with initdb and make its choices on purpose, start, reload and restart it with pg_ctl,
set values with ALTER SYSTEM, per database, per role and per transaction, and trace where any value on a running
server came from.
initdb makes choices that are hard to change later
A database cluster: One PostgreSQL data directory and the server that runs it, holding any number of databases that share its settings, roles and write-ahead log. initdb creates it. is one data directory and the one server that runs it, holding any number of
databases. initdb creates it: the folders, the catalogs shared by the whole cluster, and three databases:
postgres for everyday use, and template1 and template0, the templates a later CREATE DATABASE copies
(template1 unless it names another). Here it runs twice, once with the defaults and once with the builtin locale
provider:
initdb_session.py
"""Creating a PostgreSQL 18.6 cluster with initdb, starting it with pg_ctl, and a second cluster that takes its
collation from PostgreSQL's builtin locale provider instead of the operating system."""
from pg_session import Server
with Server() as pg:
pg.initdb(show=True)
pg.start(show=True)
pg.psql("checks.sql")
with Server(initdb=["--locale-provider=builtin", "--builtin-locale=C.UTF-8"]) as builtin:
builtin.initdb(show=True) Output
$ initdb -D data -U postgres --auth=scram-sha-256 --pwfile=pwfile
The files belonging to this database system will be owned by user "learner".
This user must also own the server process.
The database cluster will be initialized with locale "en_US.UTF-8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are enabled.
creating directory data ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default "max_connections" ... 100
selecting default "shared_buffers" ... 128MB
selecting default time zone ... UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
Success. You can now start the database server using:
pg_ctl -D data -l logfile start
$ pg_ctl -D data -l data.log -w -t 60 start
waiting for server to start... done
server started
$ psql -X -a -f checks.sql
-- What did initdb choose? Checksums, and the locale of the databases it created.
SHOW data_checksums;
data_checksums
----------------
on
(1 row)
SELECT datname, datlocprovider, datcollate, pg_encoding_to_char(encoding) AS encoding
FROM pg_database ORDER BY datname;
datname | datlocprovider | datcollate | encoding
-----------+----------------+-------------+----------
postgres | c | en_US.UTF-8 | UTF8
template0 | c | en_US.UTF-8 | UTF8
template1 | c | en_US.UTF-8 | UTF8
(3 rows)
$ initdb -D data -U postgres --auth=scram-sha-256 --pwfile=pwfile --locale-provider=builtin --builtin-locale=C.UTF-8
The files belonging to this database system will be owned by user "learner".
This user must also own the server process.
The database cluster will be initialized with this locale configuration:
locale provider: builtin
default collation: C.UTF-8
LC_COLLATE: en_US.UTF-8
LC_CTYPE: en_US.UTF-8
LC_MESSAGES: en_US.UTF-8
LC_MONETARY: en_US.UTF-8
LC_NUMERIC: en_US.UTF-8
LC_TIME: en_US.UTF-8
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are enabled.
creating directory data ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default "max_connections" ... 100
selecting default "shared_buffers" ... 128MB
selecting default time zone ... UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
Success. You can now start the database server using:
pg_ctl -D data -l logfile start
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 initdb_session.py
checks.sql
-- What did initdb choose? Checksums, and the locale of the databases it created.
SHOW data_checksums;
SELECT datname, datlocprovider, datcollate, pg_encoding_to_char(encoding) AS encoding
FROM pg_database ORDER BY datname; pg_session.py
"""Throwaway PostgreSQL 18.6 servers for the examples of this lesson.
Every session creates a cluster of its own with initdb in a new folder inside the examples folder, starts it, runs
psql scripts and server programs on it, prints what they printed, and stops and deletes it again, also when something
fails. The server is the pinned PostgreSQL 18.6 from the mise installations next to the Python that runs this file.
It listens only on 127.0.0.1, an address no other computer can reach, on a port the operating system reports as
unused, with no Unix socket, and it asks for a password (scram-sha-256) made up for the run and never printed.
learn:verify's run sandbox cannot read the home folder, where mise keeps PostgreSQL, so these sessions are recorded
with LEARN_VERIFY_NETWORK=allow; they read only this folder and the toolchain and connect to nothing but 127.0.0.1.
The output is exactly what the programs printed, with four substitutions so that a record never shows the computer
it ran on and is the same on every run: the run's folder is shown as ".", the folder of PostgreSQL's programs is left
out of their paths, the operating-system user's name is shown as "learner", and the dots pg_ctl prints while it waits
(one about every tenth of a second) as three. The sessions never print the port. A session stops with status 1 when
a command fails that should have worked, or reports a different number of errors than the session expects, so a run
that went wrong is never recorded as if it had worked.
"""
import os
import pwd
import re
import secrets
import shlex
import shutil
import socket
import subprocess
import sys
import tempfile
import textwrap
import time
from pathlib import Path
PG_VERSION = "18.6"
# .../installs/python/3.14.8/bin/python3 -> .../installs
INSTALLS = Path(sys.executable).resolve().parents[3]
BIN = INSTALLS / "postgres" / PG_VERSION / "bin"
OS_USER = pwd.getpwuid(os.geteuid()).pw_name
def free_port():
"""A TCP port on 127.0.0.1 that nothing listens on now."""
with socket.socket() as probe:
probe.bind(("127.0.0.1", 0))
return probe.getsockname()[1]
class Server:
"""One throwaway cluster: initdb, settings, start; stopped and deleted when the with block ends."""
def __init__(self, settings=None, initdb=(), name="data"):
self.settings = dict(settings or {})
self.initdb_args = list(initdb)
self.name = name
self.running = []
def __enter__(self):
if not (BIN / "postgres").exists():
sys.exit(f"PostgreSQL {PG_VERSION} was not found next to this Python")
self.base = Path(tempfile.mkdtemp(prefix="pg", dir=".")).resolve()
self.port = free_port()
self.password = secrets.token_urlsafe(24)
(self.base / "pwfile").write_text(self.password + "\n")
self.env = {**os.environ, "PGHOST": "127.0.0.1", "PGPORT": str(self.port), "PGUSER": "postgres",
"PGPASSWORD": self.password, "PGDATABASE": "postgres", "PGCONNECT_TIMEOUT": "10"}
for key in ("PGSERVICE", "PGSERVICEFILE", "PGOPTIONS", "PSQLRC"):
self.env.pop(key, None)
for script in Path(".").glob("*.sql"):
shutil.copy(script, self.base / script.name)
for data in Path(".").glob("*.csv"):
shutil.copy(data, self.base / data.name)
return self
def __exit__(self, *exc):
for folder, _port in reversed(self.running):
self._call(["pg_ctl", "-D", folder, "-m", "fast", "-w", "-t", "60", "stop"])
self.running = []
shutil.rmtree(self.base, ignore_errors=True)
# ---- the cluster ---------------------------------------------------------------------------------------------
def initdb(self, show=False):
args = ["initdb", "-D", self.name, "-U", "postgres", "--auth=scram-sha-256", "--pwfile=pwfile",
*self.initdb_args]
if not show:
args.append("--no-instructions")
self._expect(args, show=show)
with open(self.base / self.name / "postgresql.conf", "a") as conf:
conf.write("\n# Added by the session: 127.0.0.1 only, no Unix socket.\n")
conf.write(f"listen_addresses = '127.0.0.1'\nport = {self.port}\nunix_socket_directories = ''\n")
for key, value in self.settings.items():
conf.write(f"{key} = {value}\n")
def start(self, show=False, folder=None, port=None):
folder = folder or self.name
self._expect(["pg_ctl", "-D", folder, "-l", f"{folder}.log", "-w", "-t", "60", "start"], show=show)
self.running.append((folder, port or self.port))
def up(self):
"""initdb and start, quietly."""
self.initdb()
self.start()
return self
def standby(self, folder="standby"):
"""A streaming standby of this server, made with pg_basebackup -R, started on a port of its own."""
self._expect(["pg_basebackup", "-D", folder, "-R", "-X", "stream", "-c", "fast"])
port = free_port()
with open(self.base / folder / "postgresql.conf", "a") as conf:
conf.write(f"port = {port}\n")
self.start(folder=folder, port=port)
for _ in range(300):
if self.query("SELECT count(*) FROM pg_stat_replication WHERE state = 'streaming'") == "1":
return port
time.sleep(0.1)
sys.exit("the standby did not start streaming")
# ---- running things ------------------------------------------------------------------------------------------
def _call(self, args, env=None):
args = [str(a) for a in args]
done = subprocess.run([str(BIN / args[0]), *args[1:]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, cwd=self.base, env=env or self.env)
return done.returncode, self.clean(done.stdout)
def _expect(self, args, show=False, status=0):
code, out = self._call(args)
if show:
_show(args, out)
if code != status:
if not show:
print(out)
sys.exit(f"{args[0]} ended with status {code}, not {status}")
return out
def clean(self, text):
"""The run's folder as ".", the user as learner, pg_ctl's waiting dots as three (see the docstring)."""
text = text.replace(str(BIN) + "/", "").replace(str(self.base) + "/", "").replace(str(self.base), ".")
text = re.sub(r"(waiting for server to (?:start|shut down))\.+", r"\1...", text)
return text.replace(f'"{OS_USER}"', '"learner"')
def wait_for(self, sql, value):
"""Waits (unprinted) until a query returns the value, for example after a reload."""
for _ in range(300):
if self.query(sql) == value:
return
time.sleep(0.1)
sys.exit(f"{sql} never returned {value}")
def show_file(self, path):
"""Prints a text file of the cluster, as `cat` would."""
print(f"$ cat {path}")
print((self.base / path).read_text().rstrip("\n"))
print()
def query(self, sql, port=None):
"""One value from one query, unprinted (the session uses it to wait or to decide)."""
env = {**self.env, "PGPORT": str(port or self.port)}
code, out = self._call(["psql", "-X", "-At", "-c", sql], env=env)
if code != 0:
sys.exit(f"the query failed: {out}")
return out.strip()
def silent(self, sql):
"""Runs a statement without printing it (used only to give a role the run's password)."""
self.query(sql)
def psql(self, script, errors=0, status=None, user=None, db=None, port=None, flags=("-a",), note=None):
"""Runs a psql script with -X (no ~/.psqlrc) and the given flags, and prints the command and its output.
`errors` is how many ERROR lines the script must print; `status` the exit status it must end with."""
env = {**self.env, "PGPORT": str(port or self.port)}
if user:
env["PGUSER"] = user
if db:
env["PGDATABASE"] = db
args = ["psql", "-X", *flags, "-f", script]
code, out = self._call(args, env=env)
shown = ["psql", "-X", *flags, "-f", script]
if user:
shown[1:1] = ["-U", user]
if db:
shown[1:1] = ["-d", db]
if note:
print(f"-- {note}")
_show(shown, out)
found = sum(1 for line in out.splitlines() if "ERROR:" in line or ": error:" in line)
expected_status = 0 if status is None else status
if found != errors or code != expected_status:
sys.exit(f"{script}: {found} errors and status {code}; the session expects {errors} and {expected_status}")
def sessions(self, count):
"""Opens `count` idle psql connections (each one a client backend) until the with block ends."""
return _Idle(self, count)
def shell(self, *args, status=0):
"""Runs one of PostgreSQL's programs and prints the command and its output."""
return self._expect(list(args), show=True, status=status)
def listing(self, folder, prefix=""):
"""Prints the names in a folder of the cluster (sorted, folders with a slash), as `ls -p` would list them."""
path = self.base / folder
names = sorted(n + ("/" if (path / n).is_dir() else "") for n in os.listdir(path) if n.startswith(prefix))
print(f"-- in {folder}" + (f", the names that start with {prefix}" if prefix else "") + ":")
print(textwrap.fill(" ".join(names), width=78, initial_indent=" ", subsequent_indent=" ",
break_on_hyphens=False))
print()
return names
def _arg(a):
"""A command-line argument as a reader would type it: double quotes around text with single quotes in it."""
a = str(a)
if re.fullmatch(r"[\w@%+=:,./-]+", a):
return a
if not re.search(r"[\"$`\\!]", a):
return f'"{a}"'
return shlex.quote(a)
def _show(args, out):
print("$ " + " ".join(_arg(a) for a in args))
if out.strip():
print(out.rstrip("\n"))
print()
class _Idle:
"""Idle psql connections, closed again when the with block ends."""
def __init__(self, server, count):
self.server, self.count, self.procs = server, count, []
def __enter__(self):
before = int(self.server.query("SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend'"))
for _ in range(self.count):
self.procs.append(subprocess.Popen([str(BIN / "psql"), "-X", "-q"], stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
env=self.server.env, cwd=self.server.base))
sql = "SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend'"
for _ in range(300):
# the query's own connection is one more client backend
if int(self.server.query(sql)) == before + self.count:
return self
time.sleep(0.1)
sys.exit("the idle connections did not open")
def __exit__(self, *exc):
for p in self.procs:
p.stdin.close()
p.wait(timeout=30) Read the first output from the top. The superuser is named postgres because of -U postgres; without it initdb uses
the name of the operating-system user. Its password came from a file, --pwfile, so it never appears on a command
line, and --auth=scram-sha-256 makes pg_hba.conf ask every client for it. The manual warns that trust, the default
when no method is given, should be used only if you trust every local user of the machine.
Data page checksums are enabled. That line is new in PostgreSQL 18, which turned checksums on by default; a
checksum on every page lets the server detect corruption from the storage, and --no-data-checksums turns them off.
pg_upgrade needs the same choice on the old and the new cluster, which is why the option exists.
The locale came from the environment, en_US.UTF-8, and so did the encoding, UTF8. With the default provider,
libc, sorting and case rules come from the operating system’s C library, so they follow whatever that library
does on each machine. The second run chose --locale-provider=builtin --builtin-locale=C.UTF-8: PostgreSQL’s own
Unicode rules, which compare code points and do not depend on the operating system.
Some choices can never change afterwards. --wal-segsize, 16 MB by default, is fixed when the cluster is created, and
an existing database keeps its encoding and locale: a database with others is created from template0 and the data
is moved into it.
Checkpoint
Check: What initdb decides
Every setting has a context that says when a change applies
A running server has about 400 settings, and the view pg_settings lists every one with its current value, its
unit, where the value came from (source) and its context, which says how a change can take effect. The run reads
six settings, counts the contexts and changes work_mem twice:
settings_session.py
"""Reading pg_settings, and setting a value for one transaction and for one session."""
from pg_session import Server
with Server() as pg:
pg.up()
pg.psql("settings.sql", errors=1) Output
$ psql -X -a -f settings.sql
-- Where does each value come from, and when can it change? context answers the second question.
SELECT name, setting, unit, context, source FROM pg_settings
WHERE name IN ('shared_buffers', 'max_connections', 'listen_addresses', 'log_min_duration_statement',
'work_mem', 'lock_timeout')
ORDER BY context, name;
name | setting | unit | context | source
----------------------------+-----------+------+------------+--------------------
listen_addresses | 127.0.0.1 | | postmaster | configuration file
max_connections | 100 | | postmaster | configuration file
shared_buffers | 16384 | 8kB | postmaster | configuration file
log_min_duration_statement | -1 | ms | superuser | default
lock_timeout | 0 | ms | user | default
work_mem | 4096 | kB | user | default
(6 rows)
SELECT context, count(*) FROM pg_settings GROUP BY context ORDER BY count(*) DESC, context;
context | count
-------------------+-------
user | 151
sighup | 104
postmaster | 69
superuser | 49
internal | 20
superuser-backend | 4
backend | 2
(7 rows)
-- SET LOCAL lasts until the transaction ends; SET until the session ends or RESET.
BEGIN;
BEGIN
SET LOCAL work_mem = '64MB';
SET
SHOW work_mem;
work_mem
----------
64MB
(1 row)
COMMIT;
COMMIT
SHOW work_mem;
work_mem
----------
4MB
(1 row)
SET work_mem = '16MB';
SET
SELECT setting, unit, source FROM pg_settings WHERE name = 'work_mem';
setting | unit | source
---------+------+---------
16384 | kB | session
(1 row)
RESET work_mem;
RESET
SHOW work_mem;
work_mem
----------
4MB
(1 row)
-- A postmaster setting cannot change inside a running server.
SET shared_buffers = '256MB';
psql:settings.sql:18: ERROR: parameter "shared_buffers" cannot be changed without restarting the server
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 settings_session.py
settings.sql
-- Where does each value come from, and when can it change? context answers the second question.
SELECT name, setting, unit, context, source FROM pg_settings
WHERE name IN ('shared_buffers', 'max_connections', 'listen_addresses', 'log_min_duration_statement',
'work_mem', 'lock_timeout')
ORDER BY context, name;
SELECT context, count(*) FROM pg_settings GROUP BY context ORDER BY count(*) DESC, context;
-- SET LOCAL lasts until the transaction ends; SET until the session ends or RESET.
BEGIN;
SET LOCAL work_mem = '64MB';
SHOW work_mem;
COMMIT;
SHOW work_mem;
SET work_mem = '16MB';
SELECT setting, unit, source FROM pg_settings WHERE name = 'work_mem';
RESET work_mem;
SHOW work_mem;
-- A postmaster setting cannot change inside a running server.
SET shared_buffers = '256MB'; | Context | How a change takes effect | Examples |
|---|---|---|
postmaster |
Only when the server restarts (69 settings here) | shared_buffers, max_connections, listen_addresses |
sighup |
When the server reloads its configuration files (104) | log_line_prefix, autovacuum |
superuser |
In a session, by a superuser or a role granted SET on it (49) | log_min_duration_statement |
user |
In any session, by any user (151) | work_mem, lock_timeout |
internal |
Never: fixed when PostgreSQL was built or the cluster created (20) | block_size, data_checksums |
Two rarer contexts, backend and superuser-backend (6 settings here), take their value when a session starts and
never change inside it.
SET LOCAL work_mem = '64MB' lasted until the COMMIT, and SHOW work_mem then gave 4MB again. That is the safe way
to give one report more memory: inside its transaction and nowhere else. A plain SET lasted until the RESET, and
pg_settings reported its source as session. The last statement tried to SET shared_buffers and failed with
“cannot be changed without restarting the server”, which is what context postmaster means.
Checkpoint
Check: Setting contexts
ALTER SYSTEM writes a file that a reload or a restart applies
ALTER SYSTEM changes a setting for the whole server from SQL. It writes the value into
postgresql.auto.conf: The configuration file in the data directory that ALTER SYSTEM writes. The server reads it after postgresql.conf, so a value set there overrides the same setting in postgresql.conf. in the data directory, which the server reads after
postgresql.conf, so its values win. Nothing changes until the configuration is reloaded, or, for a postmaster
setting, until a restart. The run changes work_mem with a reload and shared_buffers with a restart:
"""ALTER SYSTEM and postgresql.auto.conf: a reload for work_mem, a restart for shared_buffers."""
from pg_session import Server
with Server() as pg:
pg.up()
# psql sends one -c string as one request, so its two statements run in one transaction block.
pg.shell("psql", "-X", "-c", "ALTER SYSTEM SET work_mem = '8MB'; SELECT pg_reload_conf();", status=1)
pg.shell("psql", "-X", "-c", "ALTER SYSTEM SET work_mem = '8MB'", "-c", "SELECT pg_reload_conf()")
pg.wait_for("SHOW work_mem", "8MB")
pg.show_file("data/postgresql.auto.conf")
pg.psql("file_settings.sql")
pg.shell("psql", "-X", "-c", "ALTER SYSTEM SET shared_buffers = '256MB'", "-c", "SELECT pg_reload_conf()")
pg.wait_for("SELECT pending_restart FROM pg_settings WHERE name = 'shared_buffers'", "t")
pg.psql("pending.sql")
pg.shell("pg_ctl", "-D", "data", "-l", "data.log", "restart")
pg.shell("psql", "-X", "-c", "SHOW shared_buffers") Output
$ psql -X -c "ALTER SYSTEM SET work_mem = '8MB'; SELECT pg_reload_conf();"
ERROR: ALTER SYSTEM cannot run inside a transaction block
$ psql -X -c "ALTER SYSTEM SET work_mem = '8MB'" -c "SELECT pg_reload_conf()"
ALTER SYSTEM
pg_reload_conf
----------------
t
(1 row)
$ cat data/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
work_mem = '8MB'
$ psql -X -a -f file_settings.sql
-- Which file and line set work_mem, and does every entry in the files apply?
SELECT name, setting, unit, source, pending_restart FROM pg_settings WHERE name = 'work_mem';
name | setting | unit | source | pending_restart
----------+---------+------+--------------------+-----------------
work_mem | 8192 | kB | configuration file | f
(1 row)
SELECT regexp_replace(sourcefile, '^.*/', '') AS file, sourceline, name, setting, applied, error
FROM pg_file_settings WHERE name IN ('max_connections', 'shared_buffers', 'work_mem') ORDER BY seqno;
file | sourceline | name | setting | applied | error
----------------------+------------+-----------------+---------+---------+-------
postgresql.conf | 65 | max_connections | 100 | t |
postgresql.conf | 132 | shared_buffers | 128MB | t |
postgresql.auto.conf | 3 | work_mem | 8MB | t |
(3 rows)
$ psql -X -c "ALTER SYSTEM SET shared_buffers = '256MB'" -c "SELECT pg_reload_conf()"
ALTER SYSTEM
pg_reload_conf
----------------
t
(1 row)
$ psql -X -a -f pending.sql
-- shared_buffers has context postmaster: a reload reads the new value but cannot apply it.
SELECT name, setting, unit, pending_restart FROM pg_settings WHERE name = 'shared_buffers';
name | setting | unit | pending_restart
----------------+---------+------+-----------------
shared_buffers | 16384 | 8kB | t
(1 row)
SELECT regexp_replace(sourcefile, '^.*/', '') AS file, name, setting, applied, error
FROM pg_file_settings WHERE name = 'shared_buffers' ORDER BY seqno;
file | name | setting | applied | error
----------------------+----------------+---------+---------+------------------------------
postgresql.conf | shared_buffers | 128MB | f |
postgresql.auto.conf | shared_buffers | 256MB | f | setting could not be applied
(2 rows)
$ pg_ctl -D data -l data.log restart
waiting for server to shut down... done
server stopped
waiting for server to start... done
server started
$ psql -X -c "SHOW shared_buffers"
shared_buffers
----------------
256MB
(1 row)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 alter_system_session.py
The first command failed: “ALTER SYSTEM cannot run inside a transaction block”. psql sends a -c string with two
statements as one request, which runs as one transaction, and ALTER SYSTEM writes a file that no ROLLBACK could
take back, so the manual forbids it in a transaction. Two -c options run as two requests, and the second try worked.
pg_file_settings reads the configuration files again and reports every entry, with the file, the line and whether
it can be applied: work_mem = 8MB came from line 3 of postgresql.auto.conf. After the shared_buffers change and a
reload, pg_settings still showed 16384 pages but pending_restart was true, and pg_file_settings reported the new
entry as “setting could not be applied”. Only pg_ctl restart made SHOW shared_buffers say 256MB.
pg_ctl is the program that starts, stops, restarts and reloads a server:
| Command | What it does |
|---|---|
pg_ctl -D data -l data.log start |
Starts the postmaster in the background, logging to data.log, and waits until it accepts connections |
pg_ctl -D data stop -m fast |
Fast, the default mode: rolls back open transactions, disconnects clients and shuts down cleanly |
pg_ctl -D data stop -m smart |
Refuses new connections and waits for every client to leave |
pg_ctl -D data stop -m immediate |
Ends every process without a clean shutdown; the next start runs crash recovery |
pg_ctl -D data reload |
Sends the postmaster SIGHUP: the configuration files and pg_hba.conf are read again |
pg_ctl -D data restart |
A stop followed by a start, needed for postmaster settings |
SELECT pg_reload_conf() does the same as pg_ctl reload from SQL.
Editing postgresql.conf while ALTER SYSTEM has set the same value
A value in postgresql.auto.conf wins over postgresql.conf, so changing work_mem in postgresql.conf and reloading
changes nothing while ALTER SYSTEM has set it. ALTER SYSTEM RESET work_mem removes the entry, and
pg_file_settings shows both entries and which one is applied.
Checkpoint
Check: ALTER SYSTEM, reload and restart
Databases and roles carry their own defaults
ALTER DATABASE … SET and ALTER ROLE … SET store a value that a session receives when it starts, kept in the
catalog pg_db_role_setting. ALTER ROLE … IN DATABASE … SET stores one for one role in one database. The run gives
a reporting role 64MB of work_mem, the shop database a 2-second lock_timeout, and the reporting role 32MB when it
works in shop, then logs in as reporting twice:
levels_session.py
"""Defaults stored for a role, for a database and for the pair, and which one a login receives."""
from pg_session import Server
with Server() as pg:
pg.up()
pg.psql("levels.sql")
pg.silent(f"ALTER ROLE reporting PASSWORD '{pg.password}'")
pg.psql("whoami.sql", user="reporting", db="shop")
pg.psql("whoami.sql", user="reporting", db="postgres")
pg.psql("set_role.sql", db="shop") Output
$ psql -X -a -f levels.sql
-- A role and a database, each with stored defaults, and one default for the pair.
CREATE ROLE reporting LOGIN;
CREATE ROLE
CREATE DATABASE shop;
CREATE DATABASE
ALTER ROLE reporting SET work_mem = '64MB';
ALTER ROLE
ALTER DATABASE shop SET lock_timeout = '2s';
ALTER DATABASE
ALTER ROLE reporting IN DATABASE shop SET work_mem = '32MB';
ALTER ROLE
SELECT coalesce(r.rolname, '(every role)') AS role, coalesce(d.datname, '(every database)') AS database,
s.setconfig
FROM pg_db_role_setting AS s
LEFT JOIN pg_roles AS r ON r.oid = s.setrole
LEFT JOIN pg_database AS d ON d.oid = s.setdatabase
ORDER BY 1, 2;
role | database | setconfig
--------------+------------------+-------------------
(every role) | shop | {lock_timeout=2s}
reporting | (every database) | {work_mem=64MB}
reporting | shop | {work_mem=32MB}
(3 rows)
-- The session now gives reporting a password made up for this run, and never prints it.
$ psql -d shop -U reporting -X -a -f whoami.sql
-- Which defaults did this login receive, and from which level?
SELECT current_user, current_database(), current_setting('work_mem') AS work_mem,
current_setting('lock_timeout') AS lock_timeout;
current_user | current_database | work_mem | lock_timeout
--------------+------------------+----------+--------------
reporting | shop | 32MB | 2s
(1 row)
SELECT name, setting, unit, source FROM pg_settings WHERE name IN ('lock_timeout', 'work_mem') ORDER BY name;
name | setting | unit | source
--------------+---------+------+---------------
lock_timeout | 2000 | ms | database
work_mem | 32768 | kB | database user
(2 rows)
$ psql -d postgres -U reporting -X -a -f whoami.sql
-- Which defaults did this login receive, and from which level?
SELECT current_user, current_database(), current_setting('work_mem') AS work_mem,
current_setting('lock_timeout') AS lock_timeout;
current_user | current_database | work_mem | lock_timeout
--------------+------------------+----------+--------------
reporting | postgres | 64MB | 0
(1 row)
SELECT name, setting, unit, source FROM pg_settings WHERE name IN ('lock_timeout', 'work_mem') ORDER BY name;
name | setting | unit | source
--------------+---------+------+---------
lock_timeout | 0 | ms | default
work_mem | 65536 | kB | user
(2 rows)
$ psql -d shop -X -a -f set_role.sql
-- SET ROLE changes the role's privileges, not its stored defaults: those apply only at login.
SET ROLE reporting;
SET
SELECT current_user, current_database(), current_setting('work_mem') AS work_mem;
current_user | current_database | work_mem
--------------+------------------+----------
reporting | shop | 4MB
(1 row)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 levels_session.py
levels.sql
-- A role and a database, each with stored defaults, and one default for the pair.
CREATE ROLE reporting LOGIN;
CREATE DATABASE shop;
ALTER ROLE reporting SET work_mem = '64MB';
ALTER DATABASE shop SET lock_timeout = '2s';
ALTER ROLE reporting IN DATABASE shop SET work_mem = '32MB';
SELECT coalesce(r.rolname, '(every role)') AS role, coalesce(d.datname, '(every database)') AS database,
s.setconfig
FROM pg_db_role_setting AS s
LEFT JOIN pg_roles AS r ON r.oid = s.setrole
LEFT JOIN pg_database AS d ON d.oid = s.setdatabase
ORDER BY 1, 2;
-- The session now gives reporting a password made up for this run, and never prints it. whoami.sql
-- Which defaults did this login receive, and from which level?
SELECT current_user, current_database(), current_setting('work_mem') AS work_mem,
current_setting('lock_timeout') AS lock_timeout;
SELECT name, setting, unit, source FROM pg_settings WHERE name IN ('lock_timeout', 'work_mem') ORDER BY name; set_role.sql
-- SET ROLE changes the role's privileges, not its stored defaults: those apply only at login.
SET ROLE reporting;
SELECT current_user, current_database(), current_setting('work_mem') AS work_mem; In shop, reporting received work_mem 32MB (source database user) and lock_timeout 2s (source database); in
postgres it received 64MB (source user) and no lock timeout. The manual gives the order: a setting for the role in
the database overrides one for the role, which overrides one for the database, and all of them override the
configuration files. Step through the login to shop:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Built into the server. Every setting starts from the value compiled into PostgreSQL 18. For work_mem that is 4MB and for lock_timeout 0, which means no limit.
- The configuration files. The server reads postgresql.conf, then postgresql.auto.conf, where ALTER SYSTEM writes. A value in the second file wins over the first. On the lesson's server neither file sets work_mem or lock_timeout.
- The command line. Values given on the postgres command line with -c override both files, so they cannot be changed without a restart. This server was started without any.
- The database. The reporting role logs in to shop. ALTER DATABASE shop SET lock_timeout = '2s' applies now, at the start of the session, so lock_timeout becomes 2s.
- The role. ALTER ROLE reporting SET work_mem = '64MB' applies next and replaces the built-in 4MB. A setting for a role overrides one for a database.
- The role in this database. A setting for this role in this database is the most specific default, so work_mem becomes 32MB. pg_settings reports its source as database user, and lock_timeout's as database.
- The session itself. The session starts with work_mem 32MB and lock_timeout 2s, as the recorded run printed. A SET in the session replaces a value until the session ends, and SET LOCAL until the transaction ends.
The last run shows a trap. A superuser in shop ran SET ROLE reporting and still had work_mem 4MB: stored defaults
apply only at login, and SET ROLE changes the privileges of a session, not its settings.
Which level fits depends on who needs the value and for how long. Answer two questions about a change you need to make:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Where | Reaches only what needs it | Applies without a restart | Outlives the session |
|---|---|---|---|
| The whole server Statement: ALTER SYSTEM SET | Poor every session of every database and role | Mixed a reload for most settings, a restart for postmaster ones such as shared_buffers | Good kept in postgresql.auto.conf until RESET |
| One database Statement: ALTER DATABASE … SET | Mixed every role that logs in to that database | Good new sessions of the database, at login | Good kept in pg_db_role_setting |
| One role Statement: ALTER ROLE … SET | Mixed that role in every database | Good new logins of the role; SET ROLE does not apply it | Good kept in pg_db_role_setting |
| One role in one database Statement: ALTER ROLE … IN DATABASE … SET | Good one role in one database: the narrowest stored default | Good new logins of the role to that database | Good kept in pg_db_role_setting |
| This session Statement: SET | Good this session only | Good at once, for user and superuser settings | Poor gone when the session ends or on RESET |
| This transaction Statement: SET LOCAL | Good this transaction only | Good at once | Poor gone at COMMIT or ROLLBACK, as the recorded 64MB was |
How the answers decide
- “Who needs the new value?” Every session of the server: rules out One database, One role, One role in one database, This session and This transaction. Those levels reach only some sessions; a value for the whole server belongs in the configuration files.
- “Who needs the new value?” Every role in one database: rules out The whole server, One role, This session and This transaction and adds a caveat to One role in one database. The database's own default reaches every role there and nothing else; a role in that database covers one role only.
- “Who needs the new value?” One role, wherever it logs in: rules out The whole server, One database, This session and This transaction and adds a caveat to One role in one database. The role's default follows it into every database; tie it to one database only if the need is limited to it.
- “Who needs the new value?” One job that I run now: rules out The whole server, One database, One role and One role in one database. A stored default would change every later session too; SET or SET LOCAL changes the job and nothing else.
- “Must the value stay after the session that sets it ends?” Yes, for every new session: rules out This session and This transaction. A SET value ends with its session, and a SET LOCAL value with its transaction.
- “Must the value stay after the session that sets it ends?” No, only while this job runs: rules out The whole server, One database, One role and One role in one database. A stored default stays until someone resets it, long after the job that needed it.
In short: a value for everyone goes in the configuration files, a value for one database or one role in its stored
default, and a value for one job in SET LOCAL inside that job’s transaction.
Checkpoint
Check: Database and role defaults
Trace any value back to the place that set it
When a value surprises you, the source column of pg_settings names the level that set it, and two more views
take you to the exact place:
source says |
Where to look next |
|---|---|
default |
Nothing set it: the built-in value applies |
configuration file |
sourcefile and sourceline in pg_settings, or every entry in pg_file_settings |
command line |
The options the postmaster was started with, recorded in postmaster.opts |
database, user, database user |
pg_db_role_setting, joined to pg_database and pg_roles as levels.sql does |
session |
A SET in this session; RESET brings back the value the session started with |
Two checks belong after every configuration change: SELECT name, setting, pending_restart FROM pg_settings WHERE pending_restart lists the values waiting for a restart, and SELECT * FROM pg_file_settings WHERE error IS NOT NULL
lists the entries that failed, such as a misspelt name or a value out of range. A reload skips an invalid value and
only logs it, and a misspelt name or a syntax error makes it apply no setting from the file at all, so without that
second check the change may never have happened.
Key idea
Ask the server where a value came from before you change it: pg_settings names the level and pending_restart, pg_file_settings the file and line, and pg_db_role_setting the database and role defaults.
Checkpoint
Check: Tracing a value to its source
Practise: put each change at its level
Five requests reach a database team, and each belongs at a different level. Decide where each goes before you start
the questions: the reporting role needs work_mem 256MB everywhere; one migration needs lock_timeout 2s; every
session of the analytics database should time out statements after 30 seconds; the server needs shared_buffers
raised to 4GB; and one script must log every statement it runs. The self-check then asks about the same decisions:
Practice that adapts to you
Practise this lesson
11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What is the difference between reloading and restarting PostgreSQL, and how do you know which one a change needs?
A reload, pg_ctl reload or SELECT pg_reload_conf(), sends the postmaster a SIGHUP signal: it reads
postgresql.conf, postgresql.auto.conf and pg_hba.conf again and forwards the signal to its child processes, and every
connection stays open. A restart stops the server, ending every session, and starts it again. The context column of
pg_settings tells you which a setting needs: postmaster settings such as shared_buffers and max_connections
change only with a restart, sighup settings with a reload, and user or superuser settings can also be changed
inside a session with SET. After a reload, pending_restart in pg_settings marks the changed values that are still
waiting for a restart.
Two scenarios go further: a team that wants to put every change in ALTER SYSTEM, and a value in postgresql.conf
that the running server does not show. Open them below for what a strong answer covers first and what an interviewer asks next:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A team wants one rule: every configuration change is made with ALTER SYSTEM and a reload. This week they need work_mem 256MB for the reporting role, lock_timeout 2s for one migration and statement_timeout 30s in the analytics database. What goes wrong with their rule, and what do you do instead?
-
Mid to senior
After an incident you find shared_buffers = 32GB in postgresql.conf, but SHOW shared_buffers on the running server says 8GB. Explain the possible causes and how you prove which one it is without guessing.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Which choices does initdb fix that are hard to change later?
Data page checksums (on by default since PostgreSQL 18), the encoding, the default locale and its provider, and the WAL segment size. A database also keeps its encoding and locale for life.
-
What does a setting's context in pg_settings tell you?
How a change takes effect: postmaster needs a restart, sighup needs a reload, user and superuser can be changed with SET in a session, and internal is fixed at build or cluster creation.
-
What does ALTER SYSTEM do, and what are its limits?
It writes the value into postgresql.auto.conf, which the server reads after postgresql.conf, so its values win. The manual forbids it in a transaction, since no ROLLBACK could take a written file back, and the new value applies only after a reload, or a restart for postmaster settings.
-
Where does a value for one report or migration belong, and what is the precedence order?
In SET LOCAL inside that job's transaction. Precedence: role in a database overrides role, which overrides database, which overrides the config files. Stored defaults apply only at login, never on SET ROLE.
-
What is the difference between reloading and restarting PostgreSQL?
A reload (pg_ctl reload or pg_reload_conf) sends SIGHUP to re-read config files with connections kept open. A restart ends every session. pending_restart in pg_settings marks changed postmaster settings still waiting.
-
How do you trace a surprising setting back to where it was set?
pg_settings names the source level and pending_restart; pg_file_settings gives the file, line and any errors; pg_db_role_setting holds the database and role defaults.
Key takeaways
initdbcreates the cluster and fixes choices that are hard to change later: data checksums (on by default since PostgreSQL 18), the encoding, the default locale and its provider, and the WAL segment size.- The
contextof a setting inpg_settingssays how a change takes effect: a restart forpostmaster, a reload forsighup, and aSETin a session foruserandsuperusersettings. ALTER SYSTEMwrites postgresql.auto.conf, which overrides postgresql.conf; it cannot run in a transaction block, and nothing changes until a reload or, forpostmastersettings, a restart.- A setting for a role in a database overrides one for the role, which overrides one for the database; they apply
only when a session starts, never on
SET ROLE. SET LOCALchanges a value for one transaction, the right place for the needs of a single report or migration.pg_settingsnames the source of every value,pg_file_settingsthe file, line and errors of every entry, andpending_restartthe changes still waiting for a restart.
Terms in this lesson
- database cluster
- One PostgreSQL data directory and the server that runs it, holding any number of databases that share its settings, roles and write-ahead log. initdb creates it.
- postgresql.auto.conf
- The configuration file in the data directory that ALTER SYSTEM writes. The server reads it after postgresql.conf, so a value set there overrides the same setting in postgresql.conf.
References
- PostgreSQL 18 documentation: initdb (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 18.2 Creating a Database Cluster (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: pg_ctl (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 18.3 Starting the Database Server (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 19.1 Setting Parameters (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: ALTER SYSTEM (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: ALTER ROLE (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 53.25 pg_settings (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 53.8 pg_file_settings (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: 23.1 Locale Support (The PostgreSQL Global Development Group)
- PostgreSQL 18 release notes (The PostgreSQL Global Development Group)
Report a problem with this lesson
Kept only in this browser. Your Learn progress