Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

PostgreSQL in Depth  Module 1 – How PostgreSQL works and how to run it

PostgreSQL architecture: processes, memory, files

How a PostgreSQL 18 server is built: the postmaster and its processes, shared buffers against per-query memory, and the files that hold every table.

  • Beginner
  • 10 minutes
  • Examples run with PostgreSQL 18.6 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Describe what the postmaster, the client backends and each background process of a PostgreSQL 18 server do
  • Explain how shared buffers, WAL buffers and per-operation memory such as work_mem differ
  • Find a table's files in the data directory and name each of its forks
  • Identify the processes of a running server, a primary or a standby, from pg_stat_activity
What you’ll learn

What you will learn

  • Describe what the postmaster, the client backends and each background process of a PostgreSQL 18 server do
  • Explain how shared buffers, WAL buffers and per-operation memory such as work_mem differ
  • Find a table's files in the data directory and name each of its forks
  • Identify the processes of a running server, a primary or a standby, from pg_stat_activity
On this page

With three psql sessions open, the PostgreSQL 18 server of this lesson shows 12 processes in its activity view, and a 13th, the postmaster, started all of them: one client backend for each connection and nine background processes. They share one block of memory, most of it a 128 MB cache of table pages, each backend takes more for its own sorts, and every table is a set of files in one data directory. Knowing which part does what turns a stuck connection, gigabytes of temporary files or an out-of-memory crash into something you can find.

You will follow one connection from its first packet to its last, name every background process of a primary and a standby, size the shared memory and the memory each query takes for itself, and find the files of a table on disk, fork by fork.

Every connection gets a process of its own

PostgreSQL’s manual calls its design a “process per user” client/server model. One process, the postmaster: The supervisor process of a PostgreSQL server. It starts the background processes, listens for connections and starts a client backend for each one; it runs no queries itself., listens for connections. For each one it starts a new process, a client backend: The server process that PostgreSQL starts for one client connection. It authenticates the client and runs every query of that session, and it exits when the client disconnects., and from then on the client talks only to that backend, while the postmaster goes back to listening. Step through one psql session on the server of this lesson:

Interactive · message by message One connection, from the first packet to the last Step through one psql session on the lesson's server and watch which process does each part of the work.
One connection, from the first packet to the last, step 7 of 7. Messages so far: psql to postmaster: open a connection; Client backend to psql: prove the password; psql to Client backend: SCRAM proof; psql to Client backend: a SELECT query; Client backend to psql: 8 rows; psql to Client backend: terminate.psqlthe clientpostmasterlistens on one TCP portClient backendexitedopen a connectionprove the passwordSCRAM proofa SELECT query8 rowsterminate

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The client knocks. psql opens a connection to the server's port on 127.0.0.1, 5432 unless a server sets another; the lesson's server takes an unused one. The only process listening there is the postmaster, the supervisor that started every other process of the server.
  2. A process for this client. The postmaster starts a new process, a client backend, and hands it the connection. From now on psql talks only to that backend, and the postmaster goes back to waiting for the next client.
  3. Who are you?. The backend reads pg_hba.conf to choose how this client must prove who it is. On the lesson's server that is a scram-sha-256 password exchange, so the password itself never crosses the connection.
  4. Ready for queries. psql answers the challenge correctly. The backend loads the settings for this role and database and reports that it is ready for a query.
  5. A query arrives. psql sends a SELECT on pg_stat_activity. The backend parses and plans it, then runs it, reading the pages it needs through the shared buffers that every process of the server shares.
  6. Rows go back. The backend sends the result rows to psql. Its sorting and hashing memory, work_mem, belongs to this backend alone and is given back when the operation ends.
  7. The client leaves. psql disconnects. The backend process exits and its private memory goes with it, while the postmaster keeps running and listening, ready to start a backend for the next client.

The run below starts a server, opens two psql connections that sit idle, and asks the server’s activity view, pg_stat_activity, which kinds of process it runs. It then attaches a standby and asks the same question on both servers, which the next section reads:

The processes of a primary, then of a primary and its standby

processes_session.py

"""The processes of a PostgreSQL 18.6 server: alone with three connections, then with a streaming standby."""

from pg_session import Server

with Server(settings={"summarize_wal": "on"}) as pg:
    pg.up()
    with pg.sessions(2):
        pg.psql("processes.sql", note="the primary, with two more psql connections open")
    standby_port = pg.standby()
    pg.psql("processes.sql", note="the primary again, now with a streaming standby attached")
    pg.psql("processes.sql", port=standby_port, note="the standby")

Output

-- the primary, with two more psql connections open
$ psql -X -a -f processes.sql
-- Which kinds of process does this server run, and how many of each?
SELECT current_setting('server_version') AS version, pg_is_in_recovery() AS standby;
 version | standby 
---------+---------
 18.6    | f
(1 row)

SELECT backend_type, count(*) FROM pg_stat_activity GROUP BY backend_type ORDER BY backend_type;
         backend_type         | count 
------------------------------+-------
 autovacuum launcher          |     1
 background writer            |     1
 checkpointer                 |     1
 client backend               |     3
 io worker                    |     3
 logical replication launcher |     1
 walsummarizer                |     1
 walwriter                    |     1
(8 rows)

-- the primary again, now with a streaming standby attached
$ psql -X -a -f processes.sql
-- Which kinds of process does this server run, and how many of each?
SELECT current_setting('server_version') AS version, pg_is_in_recovery() AS standby;
 version | standby 
---------+---------
 18.6    | f
(1 row)

SELECT backend_type, count(*) FROM pg_stat_activity GROUP BY backend_type ORDER BY backend_type;
         backend_type         | count 
------------------------------+-------
 autovacuum launcher          |     1
 background writer            |     1
 checkpointer                 |     1
 client backend               |     1
 io worker                    |     3
 logical replication launcher |     1
 walsender                    |     1
 walsummarizer                |     1
 walwriter                    |     1
(9 rows)

-- the standby
$ psql -X -a -f processes.sql
-- Which kinds of process does this server run, and how many of each?
SELECT current_setting('server_version') AS version, pg_is_in_recovery() AS standby;
 version | standby 
---------+---------
 18.6    | t
(1 row)

SELECT backend_type, count(*) FROM pg_stat_activity GROUP BY backend_type ORDER BY backend_type;
   backend_type    | count 
-------------------+-------
 background writer |     1
 checkpointer      |     1
 client backend    |     1
 io worker         |     3
 startup           |     1
 walreceiver       |     1
 walsummarizer     |     1
(7 rows)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 processes_session.py

processes.sql

-- Which kinds of process does this server run, and how many of each?
SELECT current_setting('server_version') AS version, pg_is_in_recovery() AS standby;
SELECT backend_type, count(*) FROM pg_stat_activity GROUP BY backend_type ORDER BY backend_type;

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)

The first answer counts three client backends: the two idle sessions and the psql that asked. A connection that does nothing still holds a process, with its own memory, for as long as it stays open. That is why a server allows a fixed number of them, max_connections, 100 by default, and why applications with thousands of short requests put a connection pooler in front of PostgreSQL instead of opening a connection per request.

Killing a backend to drop one session

A client backend stopped with kill -9 dies without cleaning up, and the postmaster treats that as a crash: with restart_after_crash on, the default, it ends every other session and reinitialises the server. End one session from SQL instead, SELECT pg_terminate_backend(pid) with the pid that pg_stat_activity shows.

Checkpoint

Check: One backend per connection

  1. Question 1 of 2 Five application connections sit idle on a server while you run one query on pg_stat_activity from psql. How many rows does the query count with backend_type 'client backend'?

    Type a number.

    A hint

    The connection that asks is a connection too.

    Show the answer: Question 1 of 2

    Answer: 6 client backends

    Every connection has a client backend of its own, busy or idle, and the psql that runs the query is one more connection. In the recorded run, two idle sessions and the asking psql gave 3; here five and one give 6.

  2. Question 2 of 2 The postmaster has just started a client backend for a new psql connection. What does the postmaster do next?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It goes back to listening for the next connection, and psql talks only to the new backend

    The manual's "process per user" model has one supervisor, the postmaster, that starts a backend for every connection. From then on the client and its backend communicate without the postmaster, which goes back to waiting, as the stepped connection showed.

Background processes do the work no client asks for

The other rows of the first answer are processes that no client started. Each has one job, which the manual describes in the chapter of its feature:

Process (backend_type) Its job
background writer Writes changed (“dirty”) pages from the shared buffers to the data files ahead of time, so that backends rarely have to
checkpointer Runs checkpoints: writes every dirty page and marks a point from which crash recovery starts, by default every 5 minutes or before 1 GB of WAL builds up
walwriter Flushes the write-ahead log from its buffers to the files in pg_wal
autovacuum launcher Starts autovacuum workers that clean up old row versions and update statistics
io worker (three) Performs reads and writes for other processes: PostgreSQL 18’s asynchronous I/O, with io_method = worker and io_workers = 3
logical replication launcher Starts the workers that apply logical replication
walsummarizer Summarises WAL for incremental backups; it runs only with summarize_wal = on, which this session set

The second answer, after a standby connected, has one more row: a walsender: A process on a PostgreSQL primary that streams write-ahead log to one standby or logical replication subscriber; the primary runs one for each., the process that streams WAL to that standby, one per standby. The standby’s own list is shorter and different. It runs a startup process, which replays the WAL it receives, and a walreceiver, which receives it, and it has no walwriter, autovacuum launcher or logical replication launcher, because a standby accepts no writes and generates no WAL of its own. An archiver appears only when archive_mode is on.

New in PostgreSQL 18, changed in 19

The three io worker processes are new in PostgreSQL 18, which added asynchronous I/O. The release notes of PostgreSQL 19, still in preparation, let the server start and stop I/O workers between new settings io_min_workers and io_max_workers, so their count will vary on a version 19 server.

Checkpoint

Check: The background processes

  1. Question 1 of 2 Which of these process types did the recorded standby run that its primary did not?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • startup
    • walreceiver

    The standby's list had startup and walreceiver and no walwriter, autovacuum launcher or logical replication launcher. The primary gained a walsender when the standby connected: one walsender per standby.

  2. Question 2 of 2 Which process writes dirty pages from the shared buffers to the data files ahead of time, so that backends rarely have to?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The background writer

    The background writer issues writes of dirty shared buffers when clean buffers run short. The checkpointer writes every dirty page at each checkpoint, by default every 5 minutes or before 1 GB of WAL builds up.

Shared memory is sized at start, query memory per operation

Two kinds of memory serve those processes. Shared memory is set aside once, when the postmaster starts, and every process uses it. Most of it is the shared buffers: PostgreSQL's cache of table and index pages in shared memory, used by every process of the server. Its size, shared_buffers (128MB by default), changes only with a restart., PostgreSQL’s cache of table and index pages, and a smaller part holds the WAL buffers, where changes wait before the walwriter or a committing backend writes them out. Each backend also takes private memory for what it does itself, above all work_mem for each sort or hash. The run reads the settings and then sorts 200,000 rows twice:

Shared memory, and one sort with two work_mem settings

memory_session.py

"""Shared and per-query memory on a PostgreSQL 18.6 server with its default settings."""

from pg_session import Server

with Server() as pg:
    pg.up()
    pg.psql("memory.sql")

Output

$ psql -X -a -f memory.sql
-- Memory every process shares, sized when the server starts, and memory each query takes for itself.
SELECT name, setting, unit, context FROM pg_settings
WHERE name IN ('shared_buffers', 'wal_buffers', 'work_mem', 'maintenance_work_mem', 'temp_buffers',
               'effective_cache_size', 'max_connections')
ORDER BY name;
         name         | setting | unit |  context   
----------------------+---------+------+------------
 effective_cache_size | 524288  | 8kB  | user
 maintenance_work_mem | 65536   | kB   | user
 max_connections      | 100     |      | postmaster
 shared_buffers       | 16384   | 8kB  | postmaster
 temp_buffers         | 1024    | 8kB  | user
 wal_buffers          | 512     | 8kB  | postmaster
 work_mem             | 4096    | kB   | user
(7 rows)

-- shared_buffers is counted in pages of block_size bytes: 16384 pages of 8192 bytes.
SELECT current_setting('block_size') AS block_size,
       pg_size_pretty(16384 * current_setting('block_size')::bigint) AS shared_buffers;
 block_size | shared_buffers 
------------+----------------
 8192       | 128 MB
(1 row)

SHOW shared_memory_size;
 shared_memory_size 
--------------------
 150MB
(1 row)

SELECT name, pg_size_pretty(allocated_size) AS size FROM pg_shmem_allocations
WHERE name IN ('Buffer Blocks', 'XLOG Ctl') ORDER BY allocated_size DESC;
     name      |  size   
---------------+---------
 Buffer Blocks | 128 MB
 XLOG Ctl      | 4110 kB
(2 rows)

-- One sort of 200,000 rows with the default work_mem, then with 64MB.
CREATE TABLE readings AS SELECT n AS id, md5(n::text) AS tag FROM generate_series(1, 200000) AS n;
SELECT 200000
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM readings ORDER BY tag;
                         QUERY PLAN                         
------------------------------------------------------------
 Sort (actual rows=200000.00 loops=1)
   Sort Key: tag
   Sort Method: external merge  Disk: 9208kB
   ->  Seq Scan on readings (actual rows=200000.00 loops=1)
(4 rows)

SET work_mem = '64MB';
SET
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM readings ORDER BY tag;
                         QUERY PLAN                         
------------------------------------------------------------
 Sort (actual rows=200000.00 loops=1)
   Sort Key: tag
   Sort Method: quicksort  Memory: 17082kB
   ->  Seq Scan on readings (actual rows=200000.00 loops=1)
(4 rows)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 memory_session.py

memory.sql

-- Memory every process shares, sized when the server starts, and memory each query takes for itself.
SELECT name, setting, unit, context FROM pg_settings
WHERE name IN ('shared_buffers', 'wal_buffers', 'work_mem', 'maintenance_work_mem', 'temp_buffers',
               'effective_cache_size', 'max_connections')
ORDER BY name;
-- shared_buffers is counted in pages of block_size bytes: 16384 pages of 8192 bytes.
SELECT current_setting('block_size') AS block_size,
       pg_size_pretty(16384 * current_setting('block_size')::bigint) AS shared_buffers;
SHOW shared_memory_size;
SELECT name, pg_size_pretty(allocated_size) AS size FROM pg_shmem_allocations
WHERE name IN ('Buffer Blocks', 'XLOG Ctl') ORDER BY allocated_size DESC;
-- One sort of 200,000 rows with the default work_mem, then with 64MB.
CREATE TABLE readings AS SELECT n AS id, md5(n::text) AS tag FROM generate_series(1, 200000) AS n;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM readings ORDER BY tag;
SET work_mem = '64MB';
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM readings ORDER BY tag;

shared_buffers is counted in pages: 16,384 pages of 8,192 bytes are 128 MB, and the context postmaster means it changes only with a restart. wal_buffers was left at its automatic size, 1/32 of the shared buffers, 512 pages or 4 MB, part of the 4,110 kB block called XLOG Ctl. With the rest of what the server keeps in shared memory, the total shared_memory_size is 150MB.

Under the shared buffers the operating system keeps its own cache of recently read file pages, which PostgreSQL also relies on. The manual suggests 25 % of a dedicated server’s memory as a first value for shared_buffers and, because of that second cache, rarely more than 40 %. effective_cache_size, 4GB by default, is only the planner’s assumption about how much of both caches a query can count on. It allocates nothing.

The two plans show what work_mem is. At the default 4MB the sort of 200,000 rows wrote 9,208 kB of temporary files and merged them (external merge). At 64MB it sorted in memory, using 17,082 kB. Follow that one sort through the memory and the files:

Interactive · step by step One sort of 200,000 rows, at 4MB and at 64MB of work_mem Step through the sort of the lesson's readings table and see where its rows wait when work_mem is too small for them.
One sort of 200,000 rows, at 4MB and at 64MB of work_mem, step 6 of 6. Showing Client backend, Shared buffers, work_mem (17082kB in memory), Table file, Temporary files (not used). In focus: Shared buffers, Client backend.Client backendruns the ORDER BYShared buffers128MB for every processwork_mem17082kB in memoryTable filereadingsTemporary filesnot used

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Rows come in. The backend scans readings. Each page of the table comes through the shared buffers, which hold pages for every process of the server; a page that is not there yet is read from the table's file.
  2. The sort fills work_mem. The rows go into the sort's own memory. work_mem, 4MB by default, is the most this one sort may use before it writes to disk.
  3. Too big for 4MB. The 200,000 rows need about 17MB in memory, four times the limit. So the sort sorts what fits, writes that run to a temporary file, and starts the next run.
  4. An external merge. When every row has been written in sorted runs, the sort merges the runs from the files. EXPLAIN ANALYZE reports it as external merge, Disk 9208kB.
  5. With 64MB instead. SET work_mem = '64MB' in the same session gives this sort room for all its rows. It sorts in memory with quicksort and EXPLAIN reports Memory 17082kB; no temporary file is written.
  6. Only this sort. The shared buffers did not change size. work_mem is taken by each sort or hash operation of each query, so many sessions with a large work_mem can use many times its value at once.

work_mem is a limit for each sort or hash operation, not for a query or a session. The manual warns that a complex query may run several such operations at once, in each of many sessions, so the total “could be many times” the value. Raise it for the one report that needs it, never for the whole server first.

Key idea

Shared buffers are one cache of pages that every process shares, sized when the server starts; work_mem is taken again by every sort and hash of every session, so its real total is the value times the operations running at once.

Checkpoint

Check: Shared memory and work_mem

  1. Question 1 of 2 pg_settings shows shared_buffers with setting 65536 and unit 8kB, on a server whose block_size is 8192. How many megabytes are the shared buffers?

    Type a number.

    A hint

    The setting counts pages, and each page is 8 kB.

    Show the answer: Question 1 of 2

    Answer: 512 MB

    65,536 pages × 8 kB = 524,288 kB = 512 MB. The lesson's server had 16,384 pages, which is 128 MB.

  2. Question 2 of 2 A team sets work_mem to 256MB for the whole server, which allows 100 connections. One report runs three sorts and a hash join at once. What is the honest worst case for that setting?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Many gigabytes, because every sort and hash of every session may take up to 256MB, and hashes even twice that

    The manual warns that the total "could be many times" work_mem: several operations per query, in many sessions. One report with four operations can take about 1.25 GB on its own (three sorts of 256MB and a hash of 512MB). Raise work_mem for the one report with SET LOCAL instead.

A table is a file named after its relfilenode

Everything the server stores lives in one folder, the data directory: The folder that initdb creates and a PostgreSQL server keeps all its data in: one folder per database under base, the write-ahead log in pg_wal and, by default, the configuration files., which initdb creates. Each database has a folder under base, named after the database’s OID, and each table and index is a file in it, named after the table’s relfilenode: The number that names a table's or index's files on disk (pg_class.relfilenode). It often equals the relation's OID at first, but TRUNCATE, REINDEX and table rewrites give a relation a new one.. The run creates a table of 10,000 orders and finds its file; the next section reads the rest of its output:

The files of one table, and of the data directory

files_session.py

"""Where one table's rows live on disk, and the forks that appear beside its main file."""

import os

from pg_session import Server

# Autovacuum is off on this server so that only the session's own VACUUM runs.
with Server(settings={"autovacuum": "off"}) as pg:
    pg.up()
    pg.psql("files.sql")
    folder, name = os.path.split(pg.query("SELECT pg_relation_filepath('orders')"))
    pg.listing(f"data/{folder}", prefix=name)
    pg.psql("vacuum.sql")
    pg.listing(f"data/{folder}", prefix=name)
    pg.psql("truncate.sql")
    pg.listing("data")
    pg.listing("data/pg_wal")

Output

$ psql -X -a -f files.sql
-- A table of 10,000 orders, and the file that holds it.
CREATE TABLE orders (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, customer text NOT NULL,
                     total numeric(10,2) NOT NULL);
CREATE TABLE
INSERT INTO orders (customer, total)
SELECT 'customer ' || n, (n % 500) + 0.5 FROM generate_series(1, 10000) AS n;
INSERT 0 10000
SELECT oid, datname FROM pg_database ORDER BY oid;
 oid |  datname  
-----+-----------
   1 | template1
   4 | template0
   5 | postgres
(3 rows)

SELECT oid, relfilenode, pg_relation_filepath('orders') AS path FROM pg_class WHERE relname = 'orders';
  oid  | relfilenode |     path     
-------+-------------+--------------
 16385 |       16385 | base/5/16385
(1 row)

SELECT pg_relation_size('orders', 'main') AS main, pg_relation_size('orders', 'fsm') AS fsm,
       pg_relation_size('orders', 'vm') AS vm;
  main  |  fsm  | vm 
--------+-------+----
 606208 | 24576 |  0
(1 row)

-- in data/base/5, the names that start with 16385:
   16385  16385_fsm

$ psql -X -a -f vacuum.sql
-- VACUUM records which pages hold only rows every transaction can see: that needs the visibility map.
VACUUM orders;
VACUUM
SELECT pg_relation_size('orders', 'main') AS main, pg_relation_size('orders', 'fsm') AS fsm,
       pg_relation_size('orders', 'vm') AS vm;
  main  |  fsm  |  vm  
--------+-------+------
 606208 | 24576 | 8192
(1 row)

-- in data/base/5, the names that start with 16385:
   16385  16385_fsm  16385_vm

$ psql -X -a -f truncate.sql
-- TRUNCATE gives the table a new, empty file: the OID stays, the file name (relfilenode) changes.
TRUNCATE orders;
TRUNCATE TABLE
SELECT oid, relfilenode, pg_relation_filepath('orders') AS path FROM pg_class WHERE relname = 'orders';
  oid  | relfilenode |     path     
-------+-------------+--------------
 16385 |       16395 | base/5/16395
(1 row)

-- in data:
   PG_VERSION  base/  global/  pg_commit_ts/  pg_dynshmem/  pg_hba.conf
   pg_ident.conf  pg_logical/  pg_multixact/  pg_notify/  pg_replslot/
   pg_serial/  pg_snapshots/  pg_stat/  pg_stat_tmp/  pg_subtrans/  pg_tblspc/
   pg_twophase/  pg_wal/  pg_xact/  postgresql.auto.conf  postgresql.conf
   postmaster.opts  postmaster.pid

-- in data/pg_wal:
   000000010000000000000001  archive_status/  summaries/

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 files_session.py

files.sql

-- A table of 10,000 orders, and the file that holds it.
CREATE TABLE orders (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, customer text NOT NULL,
                     total numeric(10,2) NOT NULL);
INSERT INTO orders (customer, total)
SELECT 'customer ' || n, (n % 500) + 0.5 FROM generate_series(1, 10000) AS n;
SELECT oid, datname FROM pg_database ORDER BY oid;
SELECT oid, relfilenode, pg_relation_filepath('orders') AS path FROM pg_class WHERE relname = 'orders';
SELECT pg_relation_size('orders', 'main') AS main, pg_relation_size('orders', 'fsm') AS fsm,
       pg_relation_size('orders', 'vm') AS vm;

vacuum.sql

-- VACUUM records which pages hold only rows every transaction can see: that needs the visibility map.
VACUUM orders;
SELECT pg_relation_size('orders', 'main') AS main, pg_relation_size('orders', 'fsm') AS fsm,
       pg_relation_size('orders', 'vm') AS vm;

truncate.sql

-- TRUNCATE gives the table a new, empty file: the OID stays, the file name (relfilenode) changes.
TRUNCATE orders;
SELECT oid, relfilenode, pg_relation_filepath('orders') AS path FROM pg_class WHERE relname = 'orders';

The database postgres has OID 5, so its tables live in base/5, and orders is the file base/5/16385, as pg_relation_filepath reports. The file holds 606,208 bytes, 74 pages of 8 kB. A file never grows past 1 GB, the default segment size: a larger table continues in 16385.1, 16385.2 and so on. The last listings show the top of the data directory, whose parts you will meet in this track:

Name What it holds
base/ One folder per database, named by its OID, with its tables and indexes
global/ Tables shared by the whole cluster, such as pg_database
pg_wal/ The write-ahead log, in segment files of 16 MB, here 000000010000000000000001
pg_xact/ Whether each transaction committed or aborted
postgresql.conf, pg_hba.conf The settings, and the rules for who may connect and how
postgresql.auto.conf Settings written by ALTER SYSTEM, which override postgresql.conf
postmaster.pid The postmaster’s process ID and port while the server runs

The configuration files sit in the data directory by default, but a server can be started with config_file and hba_file pointing elsewhere, and some packaged installations do that. SHOW config_file and SHOW hba_file tell you where your server reads them from.

Never edit or delete files in the data directory by hand

The server knows things about these files that the files do not say: which pages are dirty in shared memory, which WAL is still needed. Deleting “old” files in pg_wal to free disk space can leave a cluster that cannot start. Change data and settings through the server.

Checkpoint

Check: Tables as files in the data directory

  1. Question 1 of 2 In a database whose OID is 16384, a table's relfilenode is 24601. Which file in the data directory holds its first gigabyte of rows?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: base/16384/24601

    The recorded table was base/5/16385: database postgres has OID 5, and orders had relfilenode 16385. pg_relation_filepath returns this path for any relation.

  2. Question 2 of 2 A table with relfilenode 16385 has grown to 2.5 GB. Which files hold its main fork?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 16385, 16385.1 and 16385.2

    A relation that outgrows 1 GB carries on in further files of 1 GB each: the plain filenode first, then filenode.1, filenode.2 and so on. pg_relation_filepath names only the first segment of the main fork.

Forks appear beside the main file when they are needed

A table’s rows are its main fork: One of the files that store a table or index: the main fork holds its rows or entries, and beside it may be a free space map (_fsm), a visibility map (_vm) and, for unlogged relations, an initialization fork (_init)., the file 16385. Beside it the server keeps other forks with the same name and a suffix. The free space map: A fork of a table or index (its file ends in _fsm) that records how much room each page has left, so that new rows can be placed without reading the whole relation., _fsm, records how much room each page has left, so an insert can find space without reading the whole table. The visibility map: A fork of a table (its file ends in _vm) with two bits per page: whether every row on the page is visible to all transactions, and whether every row is frozen. VACUUM and index-only scans use it., _vm, keeps two bits per page: whether every row on the page is visible to every transaction, and whether every row is frozen. Index-only scans and VACUUM use it to skip pages.

The recorded run shows them appear. After the insert, orders had a 24,576-byte free space map (three pages) but a visibility map of 0 bytes: no file at all, as the first listing shows. The session’s VACUUM checked every page and recorded that all of them are all-visible, which needs the visibility map, and the file 16385_vm of 8,192 bytes appeared in the second listing. Unlogged tables and indexes also have an initialization fork, _init.

The last step shows why the file name and the table’s OID are two numbers. TRUNCATE gave orders a new, empty file: the OID stayed 16385, and the relfilenode became 16395. The manual warns that TRUNCATE, REINDEX, CLUSTER and some forms of ALTER TABLE do this, so never assume the two are equal; pg_relation_filepath always gives the current path of the first segment of the main fork. Sorts and hashes that spill, like the 9,208 kB sort above, write their temporary files in base/pgsql_tmp.

Checkpoint

Check: Forks and relfilenodes

  1. Question 1 of 2 Right after 10,000 rows were inserted into a new table, pg_relation_size(t, 'vm') returned 0. Why, and what changed it in the recorded run?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No visibility map existed yet; the first VACUUM recorded all-visible pages and created the 8 kB fork

    Forks appear when they are needed. The free space map existed after the insert, the visibility map only after VACUUM had all-visible pages to record: 0 bytes before, 8,192 after.

  2. Question 2 of 2 After TRUNCATE orders, what happened to the table's OID and its relfilenode in the recorded run?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The OID stayed 16385 and the relfilenode changed to 16395

    A table's OID and its file name often start equal and then drift apart: TRUNCATE, REINDEX, CLUSTER and some forms of ALTER TABLE give a relation new files. Ask pg_relation_filepath instead of assuming the OID.

Practise: read a server from its processes and files

Every question below comes from the recorded runs above: the process lists, the memory settings and the files of orders. The session begins with the idea your answers so far have mastered least:

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.

  1. Question 1 of 11 How does PostgreSQL serve 40 open connections?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: With 40 client backends, one process for each connection

    The process-per-connection model is why max_connections is a fixed limit and why idle connections still cost a process each.

  2. Question 2 of 11 An application opens a new connection for every web request and keeps 900 of them idle in a pool of its own, on a server with max_connections = 1000. What is the main risk?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: 900 backend processes, each with its own memory, and new requests refused once the limit is reached

    Every connection is a process. A pooler in front of PostgreSQL lets many clients share a few backends; a later lesson of this track sets one up.

  3. Question 3 of 11 A primary streams WAL to three standbys. How many walsender processes does its pg_stat_activity show?

    Type a number.

    Show the answer: Question 3 of 11

    Answer: 3 walsenders

    One walsender serves each standby that streams from the primary. With one standby the recorded list had one; with three it has three.

  4. Question 4 of 11 What are the three io worker processes on a PostgreSQL 18 server?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: Processes that carry out asynchronous reads and writes for other processes, because io_method is worker and io_workers is 3

    io_method defaults to worker and io_workers to 3, so a version 18 server shows three io worker rows.

  5. Question 5 of 11 What does the context postmaster of shared_buffers in pg_settings tell you?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: A new value takes effect only when the server restarts

    shared_buffers, wal_buffers and max_connections all had context postmaster in the recorded run: they size shared memory or the process table, which exist from the postmaster's start.

  6. Question 6 of 11 EXPLAIN ANALYZE of a sort reports "Sort Method: external merge Disk: 9208kB". What does it mean, and what would change it?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: The sort did not fit in work_mem and wrote 9,208 kB of temporary files; a larger work_mem for this query lets it sort in memory

    A sort that needs more than work_mem writes sorted runs to temporary files in base/pgsql_tmp and merges them. The fix belongs to the query that needs it: SET LOCAL work_mem for that transaction.

  7. Question 7 of 11 A colleague raises effective_cache_size from 4GB to 48GB on a server with 64 GB of RAM and expects PostgreSQL to use more memory. What happens?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: No memory is allocated; the planner only assumes more data is cached and may choose index scans more often

    effective_cache_size feeds the cost of index scans in the planner. Shared memory comes from shared_buffers and private memory from settings such as work_mem.

  8. Question 8 of 11 What names the folders under base in the data directory?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: The OID of each database, such as 5 for postgres in the recorded run

    The recorded pg_database listed template1 (1), template0 (4) and postgres (5), and the table lived in base/5.

  9. Question 9 of 11 The disk of a database server is nearly full and pg_wal holds many files. What is the right first move?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: Leave pg_wal alone and work out why WAL is kept, for example a stuck archive or an unused replication slot

    Files in the data directory are changed through the server only. Why WAL is retained, and how to fix it, is taught in this track's WAL and replication lessons.

  10. Question 10 of 11 Which of these files can belong to the table whose relfilenode is 16385?

    Choose every answer that is right.

    Show the answer: Question 10 of 11

    Answer:

    • 16385_fsm
    • 16385_vm
    • 16385.1

    Beside the main fork, a table has a free space map and a visibility map; unlogged relations also have _init. Any fork can be split into numbered 1 GB segments.

  11. Question 11 of 11 A script finds a table's file by assuming the file is named after the table's OID. It works for months, then fails after a weekend's maintenance. What most likely happened?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: A TRUNCATE, REINDEX, CLUSTER or table rewrite gave the table a new relfilenode, so the OID no longer matches the file

    In the recorded run TRUNCATE moved orders from base/5/16385 to base/5/16395 while its OID stayed 16385. The manual tells you to avoid assuming the two are equal.

On your own server

Run SELECT backend_type, count(*) FROM pg_stat_activity GROUP BY 1; on a server you look after, then SELECT pg_relation_filepath('your_table'); and compare pg_relation_size('your_table', 'vm') before and after a VACUUM of a newly loaded table.

Interview questions

Warm-up (fresher): What happens inside PostgreSQL when a client connects, and why does it matter for the number of connections?

The postmaster, the server’s supervisor process, accepts the connection and starts a new process for it, a client backend. The backend authenticates the client using pg_hba.conf, loads the settings for its role and database and then runs every query of that session; the postmaster goes back to listening. Each connection therefore costs a whole process with its own private memory, even when it is idle, which is why max_connections (100 by default) is a fixed limit and why applications with many short requests use a connection pooler. When the client disconnects, its backend exits and its memory is released, while the postmaster and the background processes keep running.

Two questions go further: memory that a server runs out of although its shared buffers are small, and a file in the data directory that nobody can name. Each has a first step and follow-ups on this module’s question page:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A server with 32 GB of RAM runs PostgreSQL with shared_buffers = 8GB, max_connections = 500 and work_mem = 64MB set for everyone. During a burst of reports the kernel's out-of-memory killer ends a postgres process and every session drops. How can that happen when shared memory is only 8 GB, and what do you change?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A disk report shows that base/16384/2609183.3 and its neighbours take 40 GB, and nobody knows which table that is. How do you find out, safely, and what do the folder name, the number and the .3 tell you?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. In PostgreSQL's "process per user" model, what happens when a client connects?

    The postmaster listens and starts one client backend process for the connection; from then on the client talks only to that backend while the postmaster returns to listening.

  2. Why does every open connection cost memory even when idle?

    Each connection holds its own backend process with private memory for as long as it stays open. That is why max_connections (100 by default) is fixed and why apps with many short requests use a connection pooler.

  3. How should you end a single PostgreSQL session, and why not kill -9?

    Use SELECT pg_terminate_backend(pid). kill -9 on a backend looks like a crash to the postmaster, which with restart_after_crash on (the default) ends every session and reinitialises the server.

  4. How do shared_buffers and work_mem differ?

    shared_buffers is one page cache (128 MB by default) shared by every process, sized at startup and changed only by restart. work_mem is a per-sort/per-hash limit taken again by every operation, so its real total is the value times the operations running at once.

  5. Where is a table's file stored and what is it named after?

    In base/<database OID>/, named after the table's relfilenode and split into 1 GB segments. pg_relation_filepath gives the current path; TRUNCATE, REINDEX and CLUSTER change the relfilenode while the OID stays.

  6. What do the free space map (_fsm) and visibility map (_vm) forks hold?

    The free space map records how much room each page has left so inserts find space without scanning. The visibility map keeps two bits per page (all-visible, all-frozen), appears once VACUUM records in it, and lets index-only scans and VACUUM skip pages.

Key takeaways

  • The postmaster starts one client backend per connection, so every open connection, idle or busy, costs a process and its private memory.
  • Background processes do the server’s own work: the background writer and the checkpointer write pages, the walwriter writes WAL, three io workers serve asynchronous I/O in PostgreSQL 18, and a standby runs startup and walreceiver instead.
  • Shared buffers, 128 MB by default, are one cache of pages that every process shares and that changes only with a restart, while work_mem is a limit for each sort or hash of each session.
  • A table is a file in base/<database OID>/ named after its relfilenode, split into 1 GB segments, and pg_relation_filepath gives its current path.
  • The free space map and the visibility map are forks beside the main file; the visibility map appears once VACUUM has something to record in it, and operations such as TRUNCATE give a table a new relfilenode while its OID stays.

Terms in this lesson

client backend
The server process that PostgreSQL starts for one client connection. It authenticates the client and runs every query of that session, and it exits when the client disconnects.
data directory
The folder that initdb creates and a PostgreSQL server keeps all its data in: one folder per database under base, the write-ahead log in pg_wal and, by default, the configuration files.
fork
One of the files that store a table or index: the main fork holds its rows or entries, and beside it may be a free space map (_fsm), a visibility map (_vm) and, for unlogged relations, an initialization fork (_init).
free space map
A fork of a table or index (its file ends in _fsm) that records how much room each page has left, so that new rows can be placed without reading the whole relation.
postmaster
The supervisor process of a PostgreSQL server. It starts the background processes, listens for connections and starts a client backend for each one; it runs no queries itself.
relfilenode
The number that names a table's or index's files on disk (pg_class.relfilenode). It often equals the relation's OID at first, but TRUNCATE, REINDEX and table rewrites give a relation a new one.
shared buffers
PostgreSQL's cache of table and index pages in shared memory, used by every process of the server. Its size, shared_buffers (128MB by default), changes only with a restart.
visibility map
A fork of a table (its file ends in _vm) with two bits per page: whether every row on the page is visible to all transactions, and whether every row is frozen. VACUUM and index-only scans use it.
walsender
A process on a PostgreSQL primary that streams write-ahead log to one standby or logical replication subscriber; the primary runs one for each.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.