Skip to content

Basic PostgreSQL Administration

Installation, server architecture, basic configuration and day-to-day PostgreSQL administration.

This course keeps the official topic order and exposes the English handout as readable, native Markdown for review and future editorial work.

Source status: 官方 PDF 已提取为原生 Markdown

Translation baseline: English baseline. The current English archive is the working baseline; the Russian edition is retained for chapter-level cross-checking.

Course scope

Installation, server architecture, basic configuration and day-to-day PostgreSQL administration.

Source status

LanguageCurrent packageVersionTopicsCourse page
EnglishDBA1-handouts-16-en.zip1617HTTP 200
РусскийDBA1-handouts-16.zip1617HTTP 200

Contents

Supplementary materials

  • catalogs.pdf
  • dba1_new_features.pdf
  • dba1_student_guide.pdf
  • unix_commands.pdf

Editorial status

The available English handout is extracted from the official PDF. Where no current English archive was found, the route remains ready for a later source edition.

00 Lesson 00: PostgreSQL Administration / Basic Course

Native Markdown source page: PostgreSQL Administration / Basic Course
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_00_introduction.html Archive: DBA1-handouts-16-en.zip PDF pages: 9

Page 1: PostgreSQL Administration / Basic Course

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is”, and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: For Administrators

Source slide page 2: For Administrators{#source-figure-002 caption=“Source slide page 2: For Administrators”}

  • DBA2, 4 days

Speaker notes

Prior knowledge of the basics of the SQL language is required to complete all courses. There is no course on the SQL language in our line of courses, but there are many books and other educational resources that can help you master SQL.

For administrators, we offer the following courses.

The DBA1 basic course provides general information about the PostgreSQL architecture, installation process, basic server configuration and management. It also considers basic administration tasks and provides an overview of access control, backup and replication mechanisms.

The DBA2 course focuses on determining appropriate configuration parameters based on an understanding of the internals of the server. It also talks about monitoring the server and using feedback for iterative parameter tweaking.

The DBA3 course is devoted to backup, as well as physical and logical replication configuration and scenarios for its use. It also provides an overview of the methods and difficulties of building highly accessible, scalable clusters.

The QPT course examines in detail the mechanisms for planning and executing queries, discusses the configuration of instance parameters related to performance, and examines the options for finding problematic queries and optimizing them.

The PGPRO course discusses additional features provided by the Postgres Pro Enterprise DBMS.

Page 3: For Backend Developers

Source slide page 3: For Backend Developers{#source-figure-003 caption=“Source slide page 3: For Backend Developers”}

Speaker notes

For backend developers, we offer the following courses.

The DEV1 course is the basic course. It provides general information about the architecture of PostgreSQL and the use of basic database objects; server-side programming in SQL and PL/pgSQL languages using basic data types (including the composite type and arrays), as well as client-side interactions.

The DEV2 course examines the features of the server’s internal structure that affect the development of application code, and comprehensively discusses extensibility: the ability to extend server mechanisms with your own code, which allows you to use PostgreSQL to solve a wide variety of tasks.

The QPT and PGPRO courses are intended for both developers and administrators.

Developers may also be interested in administration courses if there is a need to study the internal structure of PostgreSQL in more detail, as well as in the case when there is no dedicated administrator role on the project.

https://postgrespro.com/community/courses

Page 4: About the DBA1 Course

Duration: 3 days

  • Prior knowledge requirements
    • Basic knowledge of databases and SQL
  • Unix fundamentals What competences will be acquired
    • general information about the PostgreSQL architecture
    • installation, basic configuration, server management
    • data organization at logical and physical levels
    • basic administration tasks
    • access control information
    • understanding backup and replication

Page 5: Materials Used

Pre-configured virtual machine

  • Xubuntu 22.04 OS
  • PostgreSQL 16
  • documentation in English
  • Course handouts
    • presentations, demonstrations, practice tasks and practice keys
    • (in html and pdf)
    • reference materials: Unix commands used in the course,
    • chart of the main tables of the system catalog with psql commands

Speaker notes

If you are taking the course on your own, start with the Student’s Guide. Among other things, it shows where to download and how to use the virtual machine and other materials. All course materials, including the Student’s Guide, are available at: https://postgrespro.com/community/courses/DBA1

Completing practice tasks is very important to gain the basic skills in working with PostgreSQL. Try to complete the practice tasks yourself first, and then review the provided keys, even if the practice task did not raise questions. Practice keys may contain additional information that is not mentioned in presentations and demonstrations.

Course handouts (presentations, demonstrations, practice tasks and keys) are available in two formats. Html is good for browsing and copying code snippets. Pdf is split in pages and is more convenient for printing.

Additional reference materials will help you quickly find the information you need.

Page 6: Course Outline and Schedule

Each training day is approximately 6 hours + lunch

  • Each lesson usually consists of
    • presentation and demonstration: ~20-60 minutes
    • practice: ~20-30 minutes, including a break

Page 7: Day 1

Basic Tools

    1. Server Installation and Management
    1. Using psql
    1. Server Configuration
  • Architecture
      1. PostgreSQL Fundamentals
      1. Isolation and MVCC
      1. Vacuuming

Page 8: Day 2

Architecture (Continued)

    1. Buffer Сache and WAL
  • Data Organization
      1. Databases and Schemas
      1. System Catalog
      1. Tablespaces
      1. Low Level
  • Administration
      1. Monitoring

Page 9: Day 3

Access Control

    1. Overview
  • Backup
      1. Overview
  • Replication
      1. Overview of Physical Replication
      1. Overview of Logical Replication

01 Lesson 01: Basic Tools / Server Installation and Management

Native Markdown source page: Basic Tools / Server Installation and Management
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_01_tools_install.html Archive: DBA1-handouts-16-en.zip PDF pages: 23

Page 1: Basic Tools / Server Installation and Management

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is”, and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Basic Сoncepts
  • Installation from Source Code
  • Server Management
  • Package Installation
  • Server Management in Ubuntu
  • Cloud Solutions

Page 3: DB Cluster and Server

Source slide page 3: DB Cluster and Server{#source-figure-003 caption=“Source slide page 3: DB Cluster and Server”}

Speaker notes

Before talking about installation, let’s look at some basic concepts.

PostgreSQL is a program that belongs to the class of database management systems.

When this program is running, we call it a PostgreSQL server or a server instance. So far, the server seems to be a “black box” for us, but gradually we will get acquainted with how it works.

The data managed by PostgreSQL is stored in databases. One instance of PostgreSQL simultaneously manages several databases. This set of databases is called a database cluster. We will talk more about databases in the lesson Data Organization. Databases and Schemas.

So, a database cluster is data in files. A server or a server instance is a program that manages a database cluster.

Page 4: Source Code

Installation from source code

  • stable server version
  • you can use non-standard parameters
  • or build the server for a non-standard architecture
  • Installation from git
    • current server version
    • used primarily by kernel developers; requires a broader set of tools

Speaker notes

You should at least have an idea of how to install the program from source code, so that you could build PostgreSQL with non-standard parameters or on a non-standard architecture, if necessary.

You can find the source code in 2 formats: gzip and bzip2 at https://www.postgresql.org/ftp/source, as well as in the VM in the student user’s home directory.

You can also get the source code straight from the project’s git repository: git://git.postgresql.org/git/postgresql.git (there are other mirrors, including github).

This way you can build not just the stable version, but also a version at any specific commit, including the most recent one.

When installing from the git repository, a wider set of tools is required. For example, the lexer and parser are made using Flex and Bison, and git stores the source codes for these tools. They generate C files when built, which are then compiled. The source code archives include the pre-built C files already.

Page 5: Required Software

Hard requirements

  • tar, gzip/bzip2, GNU make, C compiler (C89)
  • Used but can be disabled
    • GNU Readline, zlib, lz4, and zstd libraries
  • Extras
    • Perl, Python, and Tcl programming languages
    • for using PL/Perl, PL/Python, PL/Tcl
    • Kerberos, OpenSSL, OpenLDAP, PAM for authentication and encryption
    • ICU library for cross-platform UNICODE support
  • Separate tools when building from the git repository

Speaker notes

The configuration and installation process is traditionally based on GNU Make and Autoconf:

https://postgrespro.com/docs/postgresql/16/install-make

This course covers the traditional build method.

Starting with version 16, PostgreSQL also supports the Meson build system as an alternative.

https://postgrespro.com/docs/postgresql/16/install-meson

A number of programs and utilities are required when building from source.

The readline library allows you to edit the command line, use the command history and auto-completion. In a server installation, it may not be necessary if you are never going to run psql via the console.

The libraries zlib, lz4, and zstd are used for compression.

Page 6: Unpacking the Archive

The source code package is located in the student user’s home directory. Let’s unpack and open it:

student$ cd
student$ tar xzf /home/student/postgresql-16.0.tar.gz
student$ ls -l /home/student/postgresql-16.0
total 876
-rw-r--r--  1 student student    365 Sep 11  2023 aclocal.m4
drwxrwxr-x  2 student student   4096 Sep 11  2023 config
-rwxr-xr-x  1 student student 584200 Sep 11  2023 configure
-rw-r--r--  1 student student  87156 Sep 11  2023 configure.ac
drwxrwxr-x 61 student student   4096 Sep 11  2023 contrib
-rw-r--r--  1 student student   1192 Sep 11  2023 COPYRIGHT
drwxrwxr-x  3 student student   4096 Sep 11  2023 doc
-rw-r--r--  1 student student   4288 Sep 11  2023 GNUmakefile.in
-rw-r--r--  1 student student    277 Sep 11  2023 HISTORY
-rw-r--r--  1 student student  64592 Sep 11  2023 INSTALL
-rw-r--r--  1 student student   1875 Sep 11  2023 Makefile
-rw-r--r--  1 student student 101920 Sep 11  2023 meson.build
-rw-r--r--  1 student student   6266 Sep 11  2023 meson_options.txt
-rw-r--r--  1 student student   1213 Sep 11  2023 README
drwxrwxr-x 16 student student   4096 Sep 11  2023 src

Configuration

We’ll set up the configuration process in the directory containing the unpacked PostgreSQL source code using the configure script.

student$ cd /home/student/postgresql-16.0
Note

Important Note! If rebuilding (e.g., with different configuration parameters), you must first remove files from previous configuration to avoid conflicts:

student$ make distclean

The configure command accepts a number of parameters. Example: –prefix — installation directory, default/usr/local/pgsql –enable-debug — to enable debug information See the documentation for the full list of parameters. The command also makes use of environment variables. For example, CC and CFLAGS configure the C compiler. By default, the configure script prepares compilation and installation, placing all executable files, libraries, and other PostgreSQL files in the directory: /usr/local/pgsql. Typically, the installation directory is accessible for writing only to the OS superuser. For demonstration purposes, we use the –prefix option to specify the server software installation directory in the pgsql16 subdirectory of the student user’s home directory. We are also using port 5555 instead of the default 5432:

student$ ./configure --prefix=/home/student/pgsql16 --with-pgport=5555

The output of this command is extremely verbose, so we are skipping it entirely.

PostgreSQL Compilation and Installation

Options: make — build only the server make world — build the server, all extensions and documentation Let’s compile the server. Depending on your computer performance, this may take minutes or even tens of minutes.

student$ make

Now, install the server.

student$ make install

To compile the extensions, repeat the make and make install commands from within the contrib directory.

student$ cd contrib

Page 7: Page 7

student$ make
student$ make install

If you build the server together with the extensions and docs (make world), you can install everything with just the make install-world command.

Page 8: Creating a Сluster

Source slide page 8: Creating a Сluster{#source-figure-008 caption=“Source slide page 8: Creating a Сluster”}

Tool

  • initdb
  • Features
    • a new database cluster cannot belong to the OS superuser
    • PGDATA is a convenient variable that represents the cluster directory
    • configuration files are created in the PGDATA directory
    • enabling checksum calculation for data pages is a good idea

Speaker notes

After installing the server, you need to create a database cluster. The initdb tool does that for you.

For security reasons, the directory in which the cluster is initialized cannot belong to the OS superuser. Usually, the cluster ownership is assigned to the postgres user.

The cluster owner can define the PGDATA environment variable pointing to the cluster directory. This variable is used by some server tools when they need to find out the location of the cluster. Such tools include initdb, as well as pg_ctl, the main server management tool, which we will discuss in a bit.

During cluster initialization, initdb creates configuration files in the PGDATA directory. We will talk more about configuration files in another lesson.

Initdb has many keys that affect its operation. One important key is -k or –data-checksums. It enables or disables calculating checksums for data pages. The checksum check is performed when accessing any data page in the cluster. This slightly reduces performance, but allows you to quickly detect data corruption.

For more details, see https://postgrespro.com/docs/postgresql/16/app-initdb

Page 9: Creating a Cluster

Now, let’s create a data directory. In production, it’s a good practice to have a separate OS user as the owner of the data directory. Here, we will just use the student user and the /home/student/pgsql16 directory.

student$ mkdir /home/student/pgsql16/data

This directory is often referred to as PGDATA. PGDATA is the environment variable pointing to it, and it’s a convenient shortcut.

student$ export PGDATA=/home/student/pgsql16/data

All server tools and utilities are located in the bin directory. We recommend you to add it into the PATH variable:

student$ export PATH=/home/student/pgsql16/bin:$PATH

initdb is a tool used to initialize a database cluster. In the -U option, you can specify the name of the DBMS superuser. If you do not specify it, then the OS username is used, in our case student. Usually this user is postgres, so let’s set this name explicitly. The -k option includes page checksum calculation, which allows timely detection of data corruption. If PGDATA is not set, specify a directory for the data in the -D option. In our case, we do not actually need to specify this option.

student$ initdb -U postgres -k -D /home/student/pgsql16/data

Page 10: Server Management

Tool

  • pg_ctl
  • Main operations
    • starting, stopping and checking the status of the server
    • reloading configuration parameters
    • switching to a standby

Speaker notes

The main server management operations include starting and stopping the server, getting the current status of the server, reloading the configuration, and some others.

pg_ctl, a stock PostgreSQL tool, is designed to perform these actions.

pg_ctl should be run as the owner of the database cluster.

For more details about server management for database administrators, see:

https://postgrespro.com/docs/postgresql/16/app-pg-ctl

https://postgrespro.com/docs/postgresql/16/runtime

Page 11: Server Management

We can start the server now. In the -l option, specify the server message log file. The -D option is omitted because the PGDATA variable is set.

student$ pg_ctl start -l /home/student/logfile

Let’s verify that the server is running: use the psql tool to connect to the server and return current time:

student$ psql -U postgres -p 5555 -c 'SELECT now();'
             now
------------------------------
 2025-11-27 13:45:51.89913+03
(1 row)

The following command stops the server:

student$ pg_ctl stop

Page 12: Package Installation

Source slide page 12: Package Installation{#source-figure-012 caption=“Source slide page 12: Package Installation”}

Package installation is usually preferable

  • Linux ( , Debian, Red Hat, SUSE and others)
    • included in the OS distribution
    • repository (yum, apt) or RPM/DEB packages
  • FreeBSD, OpenBSD
    • packages from the Ports and Packages Collection
  • Mac OS X
  • Windows

Speaker notes

The preferred option is to use ready-made packages, since in this case a clear, supported and easily updated installation is obtained.

Packages exist for most popular systems. Each of them may have its own features that you should get to know before installing.

Package repositories of some systems already include PostgreSQL, but usually not the latest version. The latest version is always available in the PostgreSQL Global Development Group (PGDG) repository:

https://www.postgresql.org/download/

We will be working with the Ubuntu PostgreSQL package.

Page 13: Creating a Cluster

Source slide page 13: Creating a Cluster{#source-figure-013 caption=“Source slide page 13: Creating a Cluster”}

Tool

  • pg_createcluster
  • initdb
  • Features
    • initialization is performed when installing the package, creates
    • a cluster named “main”
    • checksum calculation is not enabled by default
    • defines the location of server configuration and log files
    • sets up automatic server startup at OS startup

Speaker notes

pg_createcluster is a wrapper for the initdb tool designed to initialize a cluster in Ubuntu.

The help for the pg_createcluster command can be obtained using man: $ man pg_createcluster

pg_createcluster is executed automatically when the package is installed and creates a database cluster named “main”.

Note that cluster initialization is done with checksum calculation for data pages disabled.

Executables, configuration files and the server log are placed in accordance with the Ubuntu practices.

In addition, the installation sets up the PostgreSQL server to start and stop when Ubuntu starts and stops.

To delete a cluster, the pg_dropcluster tool is used.

Both pg_createcluster and pg_dropcluster are specific to Ubuntu. In other systems, you need to explicitly initialize a cluster using initdb and use the appropriate operating system tools.

Page 14: Creating a Cluster in Ubuntu

In the provided VM, PostgreSQL is installed from the distribution package:

student$ sudo apt install -y postgresql-16

PostgreSQL directory:

student$ sudo ls -l /usr/lib/postgresql/16
total 8
drwxr-xr-x 2 root root 4096 Nov 27 12:16 bin
drwxr-xr-x 4 root root 4096 Nov 27 12:16 lib

The owner of the server software is the root user. pg_config shows the parameters the server was compiled with:

student$ sudo /usr/lib/postgresql/16/bin/pg_config --configure
 '--build=x86_64-linux-gnu' '--prefix=/usr' '--includedir=${prefix}/include'
'--mandir=${prefix}/share/man' '--infodir=${prefix}/share/info' '--sysconfdir=/etc'
'--localstatedir=/var' '--disable-option-checking' '--disable-silent-rules'
'--libdir=${prefix}/lib/x86_64-linux-gnu' '--runstatedir=/run'
'--disable-maintainer-mode' '--disable-dependency-tracking' '--with-tcl' '--with-perl'
'--with-python' '--with-pam' '--with-openssl' '--with-libxml' '--with-libxslt'
'--mandir=/usr/share/postgresql/16/man' '--docdir=/usr/share/doc/postgresql-doc-16'
'--sysconfdir=/etc/postgresql-common' '--datarootdir=/usr/share/'
'--datadir=/usr/share/postgresql/16' '--bindir=/usr/lib/postgresql/16/bin'
'--libdir=/usr/lib/x86_64-linux-gnu/' '--libexecdir=/usr/lib/postgresql/'
'--includedir=/usr/include/postgresql/' '--with-extra-version= (Ubuntu
16.11-1.pgdg24.04+1)' '--enable-nls' '--enable-thread-safety' '--enable-debug'
'--disable-rpath' '--with-uuid=e2fs' '--with-gnu-ld' '--with-gssapi' '--with-ldap'
'--with-pgport=5432' '--with-system-tzdata=/usr/share/zoneinfo' 'AWK=mawk'
'MKDIR_P=/bin/mkdir -p' 'PROVE=/usr/bin/prove' 'PYTHON=/usr/bin/python3' 'TAR=/bin/tar'
'XSLTPROC=xsltproc --nonet' 'CFLAGS=-g -O2 -fno-omit-frame-pointer
-mno-omit-leaf-frame-pointer -flto=auto -ffat-lto-objects -fstack-protector-strong
-fstack-clash-protection -Wformat -Werror=format-security -fcf-protection
-fno-omit-frame-pointer' 'LDFLAGS=-Wl,-Bsymbolic-functions -flto=auto -ffat-lto-objects
-Wl,-z,relro -Wl,-z,now' '--enable-tap-tests' '--with-icu' '--with-llvm'
'LLVM_CONFIG=/usr/bin/llvm-config-19' 'CLANG=/usr/bin/clang-19' '--with-lz4'
'--with-zstd' '--with-systemd' '--with-selinux' '--enable-dtrace'
'build_alias=x86_64-linux-gnu' 'CPPFLAGS=-Wdate-time -D_FORTIFY_SOURCE=3' 'CXXFLAGS=-g
-O2 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -flto=auto -ffat-lto-objects
-fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security
-fcf-protection'

A database cluster called main is already initialized and is located in /var/lib/postgresql/16/main. The owner of the directory is postgres user. Here’s what the directory contains:

student$ sudo ls -l /var/lib/postgresql/16/main
total 92
drwx------ 1 postgres postgres 4096 Nov 27 13:08 base
drwx------ 1 postgres postgres 4096 Nov 27 13:45 global
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_commit_ts
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_dynshmem
drwx------ 4 postgres postgres 4096 Nov 27 13:08 pg_logical
drwx------ 4 postgres postgres 4096 Nov 27 13:08 pg_multixact
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_notify
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_replslot
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_serial
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_snapshots
drwx------ 1 postgres postgres 4096 Nov 27 13:45 pg_stat
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_stat_tmp
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_subtrans
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_tblspc
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_twophase
-rw------- 1 postgres postgres    3 Nov 27 13:08 PG_VERSION
drwx------ 3 postgres postgres 4096 Nov 27 13:08 pg_wal
drwx------ 2 postgres postgres 4096 Nov 27 13:08 pg_xact
-rw------- 1 postgres postgres   88 Nov 27 13:08 postgresql.auto.conf
-rw------- 1 postgres postgres  130 Nov 27 13:45 postmaster.opts
-rw------- 1 postgres postgres  108 Nov 27 13:45 postmaster.pid

Page 15: Page 15

Page 16: Server Management

Tool

  • pg_ctlcluster
  • pg_ctl
  • Main operations
    • starting, stopping and checking the status of the server
    • reloading configuration parameters
    • switching to a standby

Speaker notes

In the Ubuntu PostgreSQL package, pg_ctl is not run directly, but through a special wrapper pg_ctlcluster. The help for pg_ctlcluster can be obtained using man: $ man pg_ctlcluster

In other systems, pg_ctl can be used directly.

Page 17: Server Management in Ubuntu

With the package installation, PostgreSQL is automatically added to the OS startup list. So, there is no need to start it manually. You can explicitly control the server using the pg_ctlcluster tool, which must be run either as the OS postgres user or via sudo. The pg_ctlcluster tool can manage multiple servers of different versions. It has the following parameters: 16 — server version number main — server name Command Stop:

student$ sudo pg_ctlcluster 16 main stop
student$ pg_lsclusters 16 main
Ver Cluster Port Status Owner    Data directory              Log file
16  main    5432 down   postgres /var/lib/postgresql/16/main
/var/log/postgresql/postgresql-16-main.log

Start:

student$ sudo pg_ctlcluster 16 main start

Restart:

student$ sudo pg_ctlcluster 16 main restart

Get server status:

student$ sudo pg_ctlcluster 16 main status
pg_ctl: server is running (PID: 3539)
/usr/lib/postgresql/16/bin/postgres "-D" "/var/lib/postgresql/16/main" "-c"
"config_file=/etc/postgresql/16/main/postgresql.conf"

Reload configuration:

student$ sudo pg_ctlcluster 16 main reload

For a package installation, the server log file is located at:

student$ ls -l /var/log/postgresql/postgresql-16-main.log
-rw-r----- 1 postgres adm 7377 Nov 27 13:46 /var/log/postgresql/postgresql-16-main.log

Let’s look at the last messages:

student$ tail -n 10 /var/log/postgresql/postgresql-16-main.log
2025-11-27 13:45:57.933 MSK [3485] LOG:  shutting down
2025-11-27 13:45:57.948 MSK [3485] LOG:  checkpoint starting: shutdown immediate
2025-11-27 13:45:58.058 MSK [3485] LOG:  checkpoint complete: wrote 3 buffers (0.0%); 0
WAL file(s) added, 0 removed, 0 recycled; write=0.030 s, sync=0.021 s, total=0.125 s;
sync files=2, longest=0.013 s, average=0.011 s; distance=0 kB, estimate=0 kB;
lsn=0/1921C98, redo lsn=0/1921C98
2025-11-27 13:45:58.067 MSK [3484] LOG:  database system is shut down
2025-11-27 13:45:58.317 MSK [3539] LOG:  starting PostgreSQL 16.11 (Ubuntu
16.11-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu
13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
2025-11-27 13:45:58.317 MSK [3539] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2025-11-27 13:45:58.332 MSK [3539] LOG:  listening on Unix socket
"/var/run/postgresql/.s.PGSQL.5432"
2025-11-27 13:45:58.369 MSK [3542] LOG:  database system was shut down at 2025-11-27
13:45:58 MSK
2025-11-27 13:45:58.390 MSK [3539] LOG:  database system is ready to accept connections
2025-11-27 13:46:00.978 MSK [3539] LOG:  received SIGHUP, reloading configuration files

Page 18: Cloud Solutions

PostgreSQL in a virtual environment

  • virtualization overhead
  • the administrator has full access to the instance
  • PostgreSQL as a Service
    • offered by many cloud providers
    • the provider takes over the administration routine
    • limited management, backup and monitoring tools

Speaker notes

Virtualization solutions can be used to run databases, including PostgreSQL. The trade off for the convenience of this approach is considerable overhead costs. For high-load systems, any additional intermediate layers between the DBMS and the hardware are undesirable.

In addition, many leading cloud providers offer PostgreSQL as a service (Database as a Service, managed database).

In this case, the provider takes over most of the administration tasks. This limits your control over the instance. Moreover, management capabilities for tasks such as performance monitoring or backup and recovery are limited by the tools offered by the service provider.

We will not consider the features of individual cloud solutions within the course. You can study the documentation offered by different service providers to learn more.

Page 19: Takeaways

Two installation options — package or source code

  • Cloud solutions available
  • The server is run by a dedicated OS user
  • Initialize a database cluster before using the server
  • Specific commands for server management

Page 20: Practice

Enabling checksum calculation for a cluster.

    1. Stop the server.
    1. Check whether checksums are calculated for the cluster.
    1. Enable checksum calculation.
    1. Start the server.

Speaker notes

2, 3. Use the pg_checksums tool. To run it, enter the full path: /usr/lib/postgresql/16/bin/pg_checksums

https://postgrespro.com/docs/postgresql/16/app-pgchecksums

Page 21: 1. Stopping the Server

student$ sudo pg_ctlcluster 16 main stop

2. Verification

To see if page checksum calculation is enabled, run the pg_checksums utility with the –check option:

student$ sudo /usr/lib/postgresql/16/bin/pg_checksums --check -D /var/lib/postgresql/16/main
pg_checksums: error: data checksums are not enabled in cluster

3. Enabling Checksum Calculation

Run pg_checksums with the –enable option:

student$ sudo /usr/lib/postgresql/16/bin/pg_checksums --enable -D /var/lib/postgresql/16/main
Checksum operation completed
Files scanned:   1244
Blocks scanned:  3749
Files written:  1026
Blocks written: 3749
pg_checksums: syncing data directory
pg_checksums: updating control file
Checksums enabled in cluster

Checksum calculation is now enabled.

4. Starting the Server

student$ sudo pg_ctlcluster 16 main start

Page 22: Practice+

  1. Install PostgreSQL from source code as shown in the demo.
  • Create a database cluster, start the server.
  • Make sure the server is running.
  • Stop the server.

Page 23: 1. Compiling PostgreSQL from Source Code

All necessary commands were shown during the demo, but were not executed. Repeat the process by yourself within the provided virtual machine.

Note

Note Note that compiling of the source code may take a while (several to tens of minutes, depending on your computer resources).

02 Lesson 02: Basic Tools / Using psql

Native Markdown source page: Basic Tools / Using psql
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_02_tools_psql.html Archive: DBA1-handouts-16-en.zip PDF pages: 17

Page 1: Basic Tools / Using psql

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss ofincome, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Launching psql and Connecting to the Database
  • Getting Help
  • Working with psql
  • Configuring psql

Page 3: Purpose of psql

Terminal client for working with PostgreSQL

  • Comes with the DBMS
  • Used by administrators and developers for interactive work and script execution

Speaker notes

There are other third-party tools available, but they are not considered in the scope of the course.

The psql terminal client will be used throughout the course. Those who are used to working with GUI tools may find it uncomfortable at first. Nevertheless, it is very powerful if you get used to it.

This is the only client supplied with the DBMS. The knowledge of psql will be useful to both developers and DB administrators, regardless of which tool they choose to work with at the end of the day.

https://postgrespro.com/docs/postgresql/16/app-psql

Page 4: Connection

Launch

$ psql -d database -U user -h host -p port
  • New connection in psql
=> \c[onnect] database user node port
  • Information about the current connection
=> \conninfo

Speaker notes

The required connection parameters include: database name, user name, server name, port number. If these parameters are not specified, psql will try to connect using the default values:

● database — matches the user name;

● user — matches the OS user name;

● node — connection via Unix socket;

● port — usually 5432.

To make a new connection without leaving psql, run the \connect command. It uses the current connection’s parameters as defaults.

The \conninfo command provides information about the current connection.

Additional information about connection configuration options:

https://postgrespro.com/docs/postgresql/16/libpq-envars

https://postgrespro.com/docs/postgresql/16/libpq-pgservice

https://postgrespro.com/docs/postgresql/16/libpq-pgpass

Page 5: Getting Help

In the OS command line

$ psql --help
$ man psql
  • In psql
    • => \?
    • list of psql commands
    • => \? variables
    • psql variables
    • => \h[elp]
    • list of SQL commands
    • => \h command
    • syntax of the SQL command
    • => \q
    • quit

Speaker notes

Reference information on psql can be obtained not only from the documentation, but also from within the system directly.

psql with the –help option displays a startup help message. If the documentation package is installed with the system, you can view the manual for psql using the man psql command.

psql can execute SQL commands as well as its own commands. All psql commands start with a backslash and, as a rule, can be abbreviated to their first letter.

Inside psql, you can get a list and a brief description of all psql commands: \?.

The \help command provides a list of SQL commands that the server supports, as well as the syntax of an SQL command (if specified).

Another command that is useful to know, although it has nothing to do with the help, is \q — exit psql. Alternatively, you can use the exit and quit commands to quit.

Page 6: Executing SQL Commands and Formatting the Output

Run psql:

student$ psql

Check the connection:

=> \conninfo
You are connected to database "student" as user "student" via socket in
"/var/run/postgresql" at port "5432".

Using the default parameters, we have connected to the student database as the student user. You will learn more about databases and users in later modules. The \c[onnect] command creates a new connection without leaving psql. SQL commands, unlike psql ones, may span multiple rows. To send an SQL command for execution, end it with a semicolon:

=> SELECT schemaname, tablename, tableowner
FROM pg_tables
LIMIT 5;
 schemaname |       tablename       | tableowner
------------+-----------------------+------------
 pg_catalog | pg_statistic          | postgres
 pg_catalog | pg_type               | postgres
 pg_catalog | pg_foreign_table      | postgres
 pg_catalog | pg_authid             | postgres
 pg_catalog | pg_statistic_ext_data | postgres
(5 rows)

psql can give output in different formats. Here are some of them: Aligned format Unaligned format Extended format The aligned format is the default. It sets each column’s width based on its contents. There is also the header and the total row. psql commands to switch display modes: \a — switches aligned format and unaligned format mode \t — switches the header and footer display Let’s switch to non-aligned, turn the header and the total row off, and use a whitespace as the separator:

=> \t \a
Tuples only is on.
Output format is unaligned.
=> \pset fieldsep ' '
Field separator is " ".
=> SELECT schemaname, tablename, tableowner FROM pg_tables LIMIT 5;
pg_catalog pg_statistic postgres
pg_catalog pg_type postgres
pg_catalog pg_foreign_table postgres
pg_catalog pg_authid postgres
pg_catalog pg_statistic_ext_data postgres
=> \t \a
Tuples only is off.
Output format is aligned.

The extended format is convenient for displaying multiple columns for one or several records:

=> \x
Expanded display is on.
=> SELECT * FROM pg_tables WHERE tablename = 'pg_class';
-[ RECORD 1 ]-----------
schemaname  | pg_catalog
tablename   | pg_class
tableowner  | postgres
tablespace  |
hasindexes  | t
hasrules    | f
hastriggers | f
rowsecurity | f
=> \x
Expanded display is off.

The extended mode can be set for a single query. To do that, add \gx at the end instead of a semicolon:

=> SELECT * FROM pg_tables WHERE tablename = 'pg_proc' \gx

Page 7: Page 7

-[ RECORD 1 ]-----------
schemaname  | pg_catalog
tablename   | pg_proc
tableowner  | postgres
tablespace  |
hasindexes  | t
hasrules    | f
hastriggers | f
rowsecurity | f

You can see all the formatting options by using the \pset command. If used with no parameters, it will display current settings:

=> \pset
border                   1
columns                  0
csv_fieldsep             ','
expanded                 off
fieldsep                 ' '
fieldsep_zero            off
footer                   on
format                   aligned
linestyle                ascii
null                     ''
numericlocale            off
pager                    1
pager_min_lines          0
recordsep                '\n'
recordsep_zero           off
tableattr
title
tuples_only              off
unicode_border_linestyle single
unicode_column_linestyle single
unicode_header_linestyle single
xheader_width            full

Interacting with the OS

psql can run shell commands:

=> \! pwd
/home/student

You can set OS environment variables:

=> \setenv HELLO Hello
=> \! echo $HELLO
Hello

It can write output into a file with the \o[ut] command:

=> \o tmp/dba1_log
=> SELECT schemaname, tablename, tableowner FROM pg_tables LIMIT 5;

There is nothing on the screen! Let’s check the file:

=> \! cat tmp/dba1_log
 schemaname |       tablename       | tableowner
------------+-----------------------+------------
 pg_catalog | pg_statistic          | postgres
 pg_catalog | pg_type               | postgres
 pg_catalog | pg_foreign_table      | postgres
 pg_catalog | pg_authid             | postgres
 pg_catalog | pg_statistic_ext_data | postgres
(5 rows)

Let’s get the output back to the screen:

=> \o

Executing Scripts

Another way to run a query is with the \g command. You can specify parameters for this particular query in the brackets. Query output can be redirected to OS command by appending the command after a vertical bar. For example, display query output with line numbers:

=> SELECT format('SELECT count(*) FROM %I;', tablename)
FROM pg_tables
LIMIT 3
\g (tuples_only=on format=unaligned) | cat -n
     1 SELECT count(*) FROM pg_statistic;
     2 SELECT count(*) FROM pg_type;
     3 SELECT count(*) FROM pg_foreign_table;

The \g command can specify a filename to redirect output:

=> SELECT format('SELECT count(*) FROM %I;', tablename)
FROM pg_tables
LIMIT 3
\g (tuples_only=on format=unaligned) tmp/dba1_log

Page 8: Page 8

This is what we get:

=> \! cat tmp/dba1_log
SELECT count(*) FROM pg_statistic;
SELECT count(*) FROM pg_type;
SELECT count(*) FROM pg_foreign_table;

We can execute this file as a script using \i[nclude]:

=> \i tmp/dba1_log
 count
-------
   409
(1 row)
 count
-------
   613
(1 row)
 count
-------
     0
(1 row)

Other ways to run commands from a file: psql < filename psql -f filename You can skip creating a file in the last example if you end the query with \gexec:

=> SELECT format('SELECT count(*) FROM %I;', tablename)
FROM pg_tables
LIMIT 3
\gexec
 count
-------
   409
(1 row)
 count
-------
   613
(1 row)
 count
-------
     0
(1 row)

gexec considers the contents of each column of each row an SQL-operator, and tries to execute them one by one.

psql Variables and Control Structures

Not unlike shell, psql has its own variables, including some pre-defined ones (with special meaning for psql). Store the value of the OS USER environment variable in a psql variable:

=> \getenv User USER

Set a variable Test:

=> \set Test Hi

To substitute a variable’s value, use a colon prefix before the variable name:

=> \echo :Test :User!
Hi student!

To reset a variable, use:

=> \unset Test
=> \echo :Test
:Test

Query results can be saved to variables. For this purpose use the \gset command termination:

=> SELECT now() AS curr_time \gset
=> \echo :curr_time
2025-11-27 13:46:21.349396+03

The query must return only one record. If used with no parameters, \set displays all currently set variables and their values:

=> \set

Page 9: Page 9

AUTOCOMMIT = 'on'
COMP_KEYWORD_CASE = 'preserve-upper'
DBNAME = 'student'
ECHO = 'none'
ECHO_HIDDEN = 'off'
ENCODING = 'UTF8'
ERROR = 'false'
FETCH_COUNT = '0'
HIDE_TABLEAM = 'off'
HIDE_TOAST_COMPRESSION = 'off'
HISTCONTROL = 'none'
HISTFILE = 'hist'
HISTSIZE = '500'
HOST = '/var/run/postgresql'
IGNOREEOF = '0'
LAST_ERROR_MESSAGE = ''
LAST_ERROR_SQLSTATE = '00000'
ON_ERROR_ROLLBACK = 'off'
ON_ERROR_STOP = 'off'
PORT = '5432'
PROMPT1 = '%/%R%x%# '
PROMPT2 = '%/%R%x%# '
PROMPT3 = '>> '
QUIET = 'off'
ROW_COUNT = '1'
SERVER_VERSION_NAME = '16.11 (Ubuntu 16.11-1.pgdg24.04+1)'
SERVER_VERSION_NUM = '160011'
SHELL_ERROR = 'false'
SHELL_EXIT_CODE = '0'
SHOW_ALL_RESULTS = 'on'
SHOW_CONTEXT = 'errors'
SINGLELINE = 'off'
SINGLESTEP = 'off'
SQLSTATE = '00000'
USER = 'student'
User = 'student'
VERBOSITY = 'default'
VERSION = 'PostgreSQL 16.11 (Ubuntu 16.11-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled
by gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit'
VERSION_NAME = '16.11 (Ubuntu 16.11-1.pgdg24.04+1)'
VERSION_NUM = '160011'
curr_time = '2025-11-27 13:46:21.349396+03'

In scripts, you can use conditional operators. For example, let’s check if working_dir has a value, and if it does not, set it as the current directory. The following command checks if the value is set and returns a Boolean value:

=> \echo :{?working_dir}
FALSE

This conditional psql operator checks if the variable exists and sets the default value if needed:

=> \if :{?working_dir}
   -- variable is defined
\else
   -- set the value as the output of the OS command
   \set working_dir `pwd`
\endif

Now we can be sure that the working_dir variable is defined:

=> \echo :working_dir
/home/student

System Catalog Commands

There is a set of commands (mostly starting with \d) used to quickly and conveniently get information about database objects. Example:

=> \d pg_tables
              View "pg_catalog.pg_tables"
   Column    |  Type   | Collation | Nullable | Default
-------------+---------+-----------+----------+---------
 schemaname  | name    |           |          |
 tablename   | name    |           |          |
 tableowner  | name    |           |          |
 tablespace  | name    |           |          |
 hasindexes  | boolean |           |          |
 hasrules    | boolean |           |          |
 hastriggers | boolean |           |          |
 rowsecurity | boolean |           |          |

We will see more of these commands later.

Configuring psql

On startup, psql runs two scripts (if they exist): First a common system psqlrc script Then a custom .psqlrc file

Page 10: Page 10

The user configuration file must be in the home directory. The system script’s location can be discovered with the following command:

student$ pg_config --sysconfdir
/etc/postgresql-common

Neither file exists by default. You can use the files to configure your session parameters, for example: psql prompt Program for page-by-page viewing of query results Variables for storing the text of frequently used commands For example, let’s store a query that returns 5 largest tables in a variable top5:

=> \set top5 'SELECT tablename, pg_total_relation_size(schemaname||''.''||tablename) AS bytes FROM pg_tables ORDER BY bytes DESC LIMIT 5;'

Now we can execute the query by just typing:

=> :top5
   tablename    |  bytes
----------------+---------
 pg_proc        | 1245184
 pg_rewrite     |  745472
 pg_attribute   |  720896
 pg_description |  630784
 pg_statistic   |  294912
(5 rows)

If you write the \set command into the ~/.psqlrc file, the top5 variable will be available immediately after psql startup. Thanks to readline support, psql can autocomplete keywords and object names, and it also stores the command history. The name and the size of the history file are set by HISTFILE and HISTSIZE variables.

Page 11: Takeaways

psql is a terminal client for working with PostgreSQL

  • Connection parameters are required at startup
  • Executes SQL and psql commands
  • Includes tools for interactive work, as well as for preparing and executing scripts

Page 12: Practice

  1. Run psql and check the current connection information. 2. Display a detailed list of databases. 3. By default, psql uses less command for page-by-page output. Replace it with less -XS and display the detailed database list again. 4. The default prompt shows only the name of the database. Configure the prompt to display in the following format: user@database=#. 5. Configure psql to display the execution time for all commands. Make sure that this setting is saved when you restart.

Speaker notes

  1. When starting psql, if you omit the connection parameters, the default values will apply.
2. Use the \l+ command.
  1. The pager program is configured using the PSQL_PAGER environment variable. The setting can be configured in the .psqlrc file using the \setenv command. This will set the value to ’less -XS’ specifically for psql sessions, while maintaining the OS default settings in all other cases. By default, less wraps long lines during viewing and clears its output upon exit. The -XS parameter disables this default behavior.

  2. Prompt customization is described in the documentation:

https://postgrespro.com/docs/postgresql/16/app-psql#APP-PSQL-PROMPTI NG

5.The psql command to output the duration of a query execution can be found in the PostgreSQL documentation or within psql itself with the \? command.

Page 13: 1. Running psql and Displaying Connection Information

student$ psql
=> \conninfo
You are connected to database "student" as user "student" via socket in
"/var/run/postgresql" at port "5432".

2. Paging Results Using less

When query output exceeds terminal dimensions, psql sends it to the less pager. You can navigate through query results using standard navigation keys. The h command displays the help file. The q command exits the display mode.

Note

Note Note that by default less will wrap long lines, which can make the results difficult to read. Besides, the output will be cleared after quitting less. For example, \l+ output becomes unreadable due to wrapping.

=> \l+
List of databases
   Name    |  Owner   | Encoding | Locale Provider |   Collate   |    Ctype    | ICU
Locale | ICU Rules |   Access privileges   |  Size   | Tablespace |
Description
-----------+----------+----------+-----------------+-------------+-------------+----------
--+-----------+-----------------------+---------+------------+----------------------------
----------------
 postgres  | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           |                       | 7361 kB | pg_default | default administrative
connection database
 student   | student  | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           |                       | 7516 kB | pg_default |
 template0 | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           | =c/postgres          +| 7361 kB | pg_default | unmodifiable empty database
           |          |          |                 |             |             |
  |           | postgres=CTc/postgres |         |            |
 template1 | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           | =c/postgres          +| 7516 kB | pg_default | default template for new
databases
           |          |          |                 |             |             |
  |           | postgres=CTc/postgres |         |            |
(4 rows)

3. Configuring Page View in .psqlrc

When using the less pager with -XS flags, long lines will not wrap and output remains visible after exiting the less pager. For such configuration, simply set the PSQL_PAGER environment variable using the \setenv command. We will save this setting in the ~/.psqlrc script:

student$ echo "\setenv PSQL_PAGER 'less -XS'" > ~/.psqlrc

4. Customizing Prompts

To include role information in your prompts, prepend %n@ to both PROMPT1 and PROMPT2 variables.

student$ echo "\set PROMPT1 '%n@%/%R%x%# '" >> ~/.psqlrc
student$ echo "\set PROMPT2 '%n@%/%R%x%# '" >> ~/.psqlrc

The PROMPT1 variable controls the primary prompt displayed for the first line of a user’s query input. For multi-line queries, PROMPT2 controls the prompt display from the second line onward. While both variables share identical default values, you can configure distinct prompts for initial and following lines. Note that PROMPT3 serves only for COPY command operations.

5. Output of SQL Execution Timing

student$ echo '\timing on' >> ~/.psqlrc

The complete contents of your .psqlrc file will be like this:

Page 14: Page 14

student$ cat ~/.psqlrc
\setenv PSQL_PAGER 'less -XS'
\set PROMPT1 '%n@%/%R%x%# '
\set PROMPT2 '%n@%/%R%x%# '
\timing on

For the changes to take effect, restart your psql session.

=> \q
student$ psql

After restarting, verify the new configuration: Prompt (must include role name) Displaying detailed information about databases Output of command execution time

Page 15: Practice+

  1. Open a transaction and execute a command that ends with any error. Make sure that no other commands can be executed inside this transaction.
    1. Set the ON_ERROR_ROLLBACK parameter to on and make sure that after the error, you can continue executing commands inside the transaction.

Speaker notes

  1. To open a transaction, run the command
BEGIN;
  1. Setting the ON_ERROR_ROLLBACK parameter to ON causes psql to create a SAVEPOINT before each SQL command inside an open transaction and, in case of an error, roll back to this savepoint.

https://postgrespro.com/docs/postgresql/16/sql-savepoint

Page 16: 1. psql and In-Transaction Errors

student$ psql

The psql tool autocommits transactions by default, so each SQL command is executed within a separate transaction. So each SQL command is executed within a separate transaction. To start a transaction explicitly, the BEGIN command is used:

student@student=# BEGIN;
BEGIN
Note

Note Note that the psql prompt has changed. The asterisk character shows that the transaction is currently open.

student@student=*# CREATE TABLE t (id int);
CREATE TABLE

Consider that we have made a typo in the following command:

student@student=*# INSERTINTO t VALUES(1);
ERROR:  syntax error at or near "INSERTINTO"
LINE 1: INSERTINTO t VALUES(1);
        ^

The asterisk will change to an exclamation mark, indicating an error. Now, rewrite the command:

student@student=!# INSERT INTO t VALUES(1);
ERROR:  current transaction is aborted, commands ignored until end of transaction block

But PostgreSQL cannot roll back just a single command, so it terminates and rolls back the whole transaction. To continue, we must send a command that says that the transaction is complete. It can be either COMMIT or ROLLBACK, since the transaction is already cancelled.

student@student=!# COMMIT;
ROLLBACK

Creating the table was cancelled, so there is no such table in the database:

student@student=# SELECT * FROM t;
ERROR:  relation "t" does not exist
LINE 1: SELECT * FROM t;
                      ^

The ON\_ERROR\_ROLLBACK Variable

We can change how psql behaves here.

student@student=# \set ON_ERROR_ROLLBACK on

Now, before every transaction command, there will be a savepoint created. In case of an error, it will roll back to the last savepoint. This way, transaction commands can continue executing.

student@student=# BEGIN;
BEGIN
student@student=*# CREATE TABLE t (id int);
CREATE TABLE
student@student=*# INSERTINTO t VALUES(1);
ERROR:  syntax error at or near "INSERTINTO"
LINE 1: INSERTINTO t VALUES(1);
        ^
student@student=*# INSERT INTO t VALUES(1);
INSERT 0 1
student@student=*# COMMIT;
COMMIT
student@student=# SELECT * FROM t;

Page 17: Page 17

 id
----
  1
(1 row)

The ON_ERROR_ROLLBACK variable can be set to interactive. This will make such behavior work only in the interactive mode, but not when executing scripts.

student@student=# DROP TABLE t;
DROP TABLE

03 Lesson 03: Basic Tools / Server Configuration

Native Markdown source page: Basic Tools / Server Configuration
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_03_tools_configuration.html Archive: DBA1-handouts-16-en.zip PDF pages: 22

Page 1: Basic Tools / Server Configuration

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Configuration Parameters
  • Configuration Files
  • Parameter Management at the Instance and Session Levels

Page 3: Configuration Parameters

Purpose

  • managing DBMS operation and behavior
  • Setting parameters
    • for an instance via configuration files
    • for a separate database or user
    • for the current session

Speaker notes

There are multiple parameters in PostgreSQL that control the DBMS behavior. These parameters affect resource management, server processes, and much more.

For example, the max_connections parameter limits the number of concurrent connections to the server.

The full list of configuration parameters and their descriptions is available in the documentation: https://postgrespro.com/docs/postgresql/16/runtime-config

In this topic, we will not cover any specific configuration parameters, but rather discuss how to set their values.

Configuration parameters are generally managed in configuration files. The parameter values set in the configuration files affect the whole DBMS instance, unless explicitly configured otherwise.

Some parameters can be set for a specific database or for a specific user’s sessions. Such settings will overrule those declared in configuration files. These types of settings will be discussed in further lessons of the course.

Lastly, many parameters can be changed at the session level, during client operaton.

Page 4: postgresql.conf

Main configuration file

  • loaded when the server starts
  • option to load additional configuration files
  • located in the data directory (PGDATA) by default
/etc/postgresql/16/main
  • After any changes to the parameters, the file has to be reloaded
$ pg_ctl reload
$ pg_ctlcluster 16 main reload
  • => SELECT pg_reload_conf();
  • changes to some parameters require a server restart to apply

Speaker notes

The main configuration file is postgresql.conf.

The default file’s location is defined during initial PostgreSQL compilation. The server executable accepts -c config_file as a command line argument to set the configuration file path.

By default, the file is located in the data catalog (PGDATA), but package distributions usually place it somewhere else, depending on the OS-specific conventions.

This is a well-documented plaintext file which stores parameters in a key-value format.

Additional configuration files can be included. By default, PostgreSQL on Ubuntu automatically includes all .conf files from /etc/postgresql/16/main/conf.d.

If the same parameter is defined in the configuration files multiple times, only the most recently read value will be used.

For any changes to parameters to apply, the file must be reloaded. Some parameters require a server restart to apply.

Page 5: The postgresql.conf File and the pg_file_settings View

The configuration file name is a value of the read-only config_file parameter. The configuration file can be specified using a command-line switch when starting postgres.

=> SHOW config_file;
               config_file
-----------------------------------------
 /etc/postgresql/16/main/postgresql.conf
(1 row)

Take a look at a part of a config file.

=> SELECT pg_read_file('/etc/postgresql/16/main/postgresql.conf', 1516, 861) \g (tuples_only=on format=unaligned)
#------------------------------------------------------------------------------
# FILE LOCATIONS
#------------------------------------------------------------------------------
# The default values of these variables are driven from the -D command-line
# option or PGDATA environment variable, represented here as ConfigDir.
data_directory = '/var/lib/postgresql/16/main'
# use data in another directory
# (change requires restart)
hba_file = '/etc/postgresql/16/main/pg_hba.conf'
# host-based authentication file
# (change requires restart)
ident_file = '/etc/postgresql/16/main/pg_ident.conf'
# ident configuration file
# (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
external_pid_file = '/var/run/postgresql/16-main.pid'
# write an extra
PID file
# (change requires restart)

The main configuration file postgresql.conf can include additional configuration files. Directives: include_dir is a directory with additional configuration files. include includes an additional configuration file. include_if_exists Includes an additional configuration file, if one exists. These directives are typically placed at the end of postgresql.conf:

student$ sudo grep -A3 ^include /etc/postgresql/16/main/postgresql.conf
include_dir = 'conf.d'
# include files ending in '.conf' from
# a directory, e.g., 'conf.d'
#include_if_exists = '...'
# include file only if it exists
#include = '...'
# include file

To see current settings in configuration files, query the pg_file_settings view:

=> SELECT sourceline, name, setting, applied, error FROM pg_file_settings;

Page 6: Page 6

 sourceline |            name            |                setting                 |
applied | error
------------+----------------------------+----------------------------------------+-------
--+-------
         42 | data_directory             | /var/lib/postgresql/16/main            | t
  |
         44 | hba_file                   | /etc/postgresql/16/main/pg_hba.conf    | t
  |
         46 | ident_file                 | /etc/postgresql/16/main/pg_ident.conf  | t
  |
         50 | external_pid_file          | /var/run/postgresql/16-main.pid        | t
  |
         64 | port                       | 5432                                   | t
  |
         65 | max_connections            | 100                                    | t
  |
         68 | unix_socket_directories    | /var/run/postgresql                    | t
  |
        108 | ssl                        | on                                     | t
  |
        110 | ssl_cert_file              | /etc/ssl/certs/ssl-cert-snakeoil.pem   | t
  |
        113 | ssl_key_file               | /etc/ssl/private/ssl-cert-snakeoil.key | t
  |
        130 | shared_buffers             | 128MB                                  | t
  |
        153 | dynamic_shared_memory_type | posix                                  | t
  |
        247 | max_wal_size               | 1GB                                    | t
  |
        248 | min_wal_size               | 80MB                                   | t
  |
        565 | log_line_prefix            | %m [%p] %q%u@%d                        | t
  |
        603 | log_timezone               | Europe/Moscow                          | t
  |
        607 | cluster_name               | 16/main                                | t
  |
        715 | datestyle                  | iso, mdy                               | t
  |
        717 | timezone                   | Europe/Moscow                          | t
  |
        731 | lc_messages                | en_US.UTF-8                            | t
  |
        733 | lc_monetary                | en_US.UTF-8                            | t
  |
        734 | lc_numeric                 | en_US.UTF-8                            | t
  |
        735 | lc_time                    | en_US.UTF-8                            | t
  |
        741 | default_text_search_config | pg_catalog.english                     | t
  |
(24 rows)

The view displays non-comment lines of configuration files. The applied column indicates whether the specified value will be applied upon reloading the configuration. Specifically, the column will show false if: The change requires a restart of the server. There is a line with the same parameter that will be read later. There is an error in one of the lines where the parameter is specified. The view also displays the configuration file name and line number, making it easy to locate errors.

The pg\_settings View

For example, let’s take the work_mem parameter. It defines how much memory is allocated for operations like sorting or hash join. The default value can be insufficient for some queries. To learn more about the work_mem parameter, check out our Query Performance Tuning course (QPT). Active configuration parameter values are shown in the pg_settings view. For example, here’s its output regarding the work_mem parameter:

=> SELECT name, unit, setting, boot_val, reset_val,
  source, sourcefile, sourceline, pending_restart, context
FROM pg_settings
WHERE name = 'work_mem' \gx

Page 7: Page 7

Source slide page 7: Page 7{#source-figure-007 caption=“Source slide page 7: Page 7”}

-[ RECORD 1 ]---+---------
name            | work_mem
unit            | kB
setting         | 4096
boot_val        | 4096
reset_val       | 4096
source          | default
sourcefile      |
sourceline      |
pending_restart | f
context         | user

Below are the main columns of the pg_settings view: name, unit — parameter name and unit setting — current value boot_val — default value reset_val — initial value for sessions source — source of the current parameter value sourcefile, sourceline — configuration file and line number, if the current value was specified in the file pending_restart — true if the value is changed in the configuration file, but server restart is required to be applied The context column shows what action has to be taken for the parameter change to take effect. Possible values include: internal — cannot be changed, the value is set during installation postmaster — server restart required sighup — requires a reload of the configuration files superuser — superuser can change for their session user — any user can change for their session

Order of Applying Settings

When reloading the configuration, the main file is read first, followed by additional files. If the same parameter is defined multiple times, the latest value is applied. For example, let’s specify the work_mem parameter twice in an additional configuration file:

student$ echo work_mem=12MB | sudo tee /etc/postgresql/16/main/conf.d/work_mem.conf
work_mem=12MB
student$ echo work_mem=8MB | sudo tee -a /etc/postgresql/16/main/conf.d/work_mem.conf
work_mem=8MB

Contents of the /etc/postgresql/16/main/conf.d/work_mem.conf file:

=> SELECT sourcefile, sourceline, name, setting, applied
FROM pg_file_settings WHERE sourcefile LIKE '%/work_mem.conf';
                  sourcefile                  | sourceline |   name   | setting | applied
----------------------------------------------+------------+----------+---------+---------
 /etc/postgresql/16/main/conf.d/work_mem.conf |          1 | work_mem | 12MB    | f
 /etc/postgresql/16/main/conf.d/work_mem.conf |          2 | work_mem | 8MB     | t
(2 rows)

The applied = f for the first line indicates that it will not be applied. The context field of the work_mem parameter says “user”. This means that the parameter can be changed by the user during their session. We will discuss how to do it in a bit. To change the value for all sessions, you just need to reload the configuration file:

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Verify that work_mem now has taken the value from the second line of the configuration file:

=> SELECT name, unit, setting, boot_val, reset_val,
  source, sourcefile, sourceline, pending_restart, context
FROM pg_settings
WHERE name = 'work_mem'\gx

Page 8: Page 8

-[ RECORD 1 ]---+---------------------------------------------
name            | work_mem
unit            | kB
setting         | 8192
boot_val        | 4096
reset_val       | 8192
source          | configuration file
sourcefile      | /etc/postgresql/16/main/conf.d/work_mem.conf
sourceline      | 2
pending_restart | f
context         | user

Page 9: postgresql.auto.conf

Configuration file managed by SQL commands

  • ALTER SYSTEM
  • adds or changes a line
  • SET parameter TO value;
  • ALTER SYSTEM RESET parameter;
  • deletes a line
  • ALTER SYSTEM RESET ALL;
  • deletes all lines
read after postgresql.conf
  • Location
    • always in the data directory (PGDATA)
  • Actions needed to apply
same as for postgresql.conf

Speaker notes

The file postgresql.auto.conf is always loaded last. This file is always located in the data catalog (PGDATA).

It should never be modified manually, but only with the ALTER SYSTEM command. ALTER SYSTEM is an SQL interface for managing configuration parameters.

For any changes made with ALTER SYSTEM to take place, the server must reload the configuration files, as it does with postgresql.conf.

The contents of both files (postgresql.conf and postgresql.auto.conf) can be checked using the pg_file_settings view, and the current parameter values are shown in the pg_settings view.

More about the ALTER SYSTEM command:

https://postgrespro.com/docs/postgresql/16/sql-altersystem

Page 10: The ALTER SYSTEM Command and the postgresql.auto.conf File

Let’s set a new value for the work_mem parameter:

=> ALTER SYSTEM SET work_mem TO '16mb';
ERROR:  invalid value for parameter "work_mem": "16mb"
HINT:  Valid units for this parameter are "B", "kB", "MB", "GB", and "TB".

What happened? ALTER SYSTEM checks if the provided value is valid.

=> ALTER SYSTEM SET work_mem TO '16MB';
ALTER SYSTEM

Now it works! The command saves the new value 16MB to a file named postgresql.auto.conf:

=> SELECT pg_read_file('postgresql.auto.conf')
\g (tuples_only=on format=unaligned)
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
work_mem = '16MB'

However, the value has not been applied yet:

=> SHOW work_mem;
 work_mem
----------
 8MB
(1 row)

For the new work_mem setting to apply, reload the configuration file first:

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)
=> SELECT name, unit, setting, boot_val, reset_val,
  source, sourcefile, sourceline, pending_restart, context
FROM pg_settings
WHERE name = 'work_mem'\gx
-[ RECORD 1 ]---+-------------------------------------------------
name            | work_mem
unit            | kB
setting         | 16384
boot_val        | 4096
reset_val       | 16384
source          | configuration file
sourcefile      | /var/lib/postgresql/16/main/postgresql.auto.conf
sourceline      | 3
pending_restart | f
context         | user

Lines are removed from postgresql.auto.conf with the ALTER SYSTEM RESET command:

=> ALTER SYSTEM RESET work_mem;
ALTER SYSTEM
=> SELECT pg_read_file('postgresql.auto.conf')
\g (tuples_only=on format=unaligned)

Page 11: Page 11

# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.

Reload the configuration file again. The old value from work_mem.conf is now applied:

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)
=> SELECT name, unit, setting, boot_val, reset_val,
  source, sourcefile, sourceline, pending_restart, context
FROM pg_settings
WHERE name = 'work_mem'\gx
-[ RECORD 1 ]---+---------------------------------------------
name            | work_mem
unit            | kB
setting         | 8192
boot_val        | 4096
reset_val       | 8192
source          | configuration file
sourcefile      | /etc/postgresql/16/main/conf.d/work_mem.conf
sourceline      | 2
pending_restart | f
context         | user

Page 12: Current Session

Source slide page 12: Current Session{#source-figure-012 caption=“Source slide page 12: Current Session”}

set until the end of the session or transaction

  • setting parameters is transactional
  • custom parameters are allowed

Speaker notes

Parameter values can be changed directly during the session with the SET command or the set_config function. The SHOW command or the current_setting function display the current parameter value.

By setting a new value, you can specify its expiration date: until the end of the session (by default) or until the end of the transaction (SET LOCAL).

In any case, setting parameters is transactional: if the current transaction is rolled back, any parameters modified within it will return to the state they were in at the start of the transaction.

In addition to the PostgreSQL system parameters, the same commands and functions can be used to create and get the values of custom parameters.

Page 13: Setting Parameters for the Current Session

The SET command is used to set parameter values for the duration of the current session:

=> SET work_mem TO '24MB';
SET

The set_config function also works:

=> SELECT set_config('work_mem', '32MB', false);
 set_config
------------
 32MB
(1 row)

The third parameter of the function defines if the parameter should be used for the duration of the current transaction (true) or until the end of the current session (false). This may be important when using a connection pool, when transactions by multiple users may be performed within the same session.

Checking Parameter Values During a Session

There are multiple ways to check the current parameter value:

=> SHOW work_mem;
 work_mem
----------
 32MB
(1 row)
=> \dconfig work_mem
List of configuration parameters
 Parameter | Value
-----------+-------
 work_mem  | 32MB
(1 row)
=> SELECT current_setting('work_mem');
 current_setting
-----------------
 32MB
(1 row)
=> SELECT name, setting, unit FROM pg_settings WHERE name = 'work_mem';
   name   | setting | unit
----------+---------+------
 work_mem | 32768   | kB
(1 row)

Reset the value to what was active at the start of the session:

=> RESET work_mem;
RESET

Setting Parameter Values within a Transaction

Open a transaction and set a new work_mem value:

=> BEGIN;
BEGIN
=> SET work_mem TO '64MB';
SET

Page 14: Page 14

=> SHOW work_mem;
 work_mem
----------
 64MB
(1 row)

If the transaction is rolled back, so is the new parameter value. If the transaction commits, however, the parameter remains changed.

=> ROLLBACK;
ROLLBACK
=> SHOW work_mem;
 work_mem
----------
 8MB
(1 row)

You can set a new parameter value just for the duration of the current transaction:

=> BEGIN;
BEGIN
=> SET LOCAL work_mem TO '64MB'; -- or set_config('work_mem','64MB',true);
SET
=> SHOW work_mem;
 work_mem
----------
 64MB
(1 row)
=> COMMIT;
COMMIT

When the transaction commits, the old value returns:

=> SHOW work_mem;
 work_mem
----------
 8MB
(1 row)

Custom Parameters

Custom parameters can be defined during session. You can also check if such a parameter is already defined. Custom parameter names must include the dot (.) in order to distinguish them from standard parameters.

=> SELECT CASE
  WHEN current_setting('myapp.currency_code', true) IS NULL
    THEN set_config('myapp.currency_code', 'RUB', false)
  ELSE
    current_setting('myapp.currency_code')
END;
 current_setting
-----------------
 RUB
(1 row)

Now myapp.currency_code can be used as a global variable during the session:

=> SELECT current_setting('myapp.currency_code');
 current_setting
-----------------
 RUB
(1 row)

Page 15: Page 15

Custom parameters can be defined in configuration files. This will make them initialize in all sessions.

Page 16: Takeaways

The main configuration file is postgresql.conf

  • ALTER SYSTEM is an SQL interface for managing configuration parameters stored in postgresql.auto.conf
  • When configuration files are modified, they have to be reloaded
  • Some parameters can be changed just for the current session
  • Changes to some parameters require a server restart to apply

Page 17: Practice

  1. Get a list of parameters that require a server restart to apply.
    1. In the additional included configuration file, make an error when changing the max_connections parameter.
    • Restart the server. Make sure that the server does not start and
    • check the message log.
    • Fix the error and start the server.

Speaker notes

  1. To get the location of the postgresql.conf file, check the value of the config_file parameter, as shown in the demonstration.

At the end of this file, you will find include_dir directive that loads additional configuration files.

Create a file named max_connections.conf in the directory specified by this directive. Edit this file either as the owner (the postgres user) or as a superuser.

To obtain the necessary privileges, use the sudo command.

Use your preferred text editor. For example: sudo nano /etc/postgresql/16/main/conf.d/max_connections.conf

To exit nano, use the Ctrl+X key combination.

Page 18: 1. Parameters that Require a Server Restart to Apply

=> SELECT name, setting, unit FROM pg_settings WHERE context = 'postmaster';
                name                 |                 setting                 | unit
-------------------------------------+-----------------------------------------+------
 archive_mode                        | off                                     |
 autovacuum_freeze_max_age           | 200000000                               |
 autovacuum_max_workers              | 3                                       |
 autovacuum_multixact_freeze_max_age | 400000000                               |
 bonjour                             | off                                     |
 bonjour_name                        |                                         |
 cluster_name                        | 16/main                                 |
 config_file                         | /etc/postgresql/16/main/postgresql.conf |
 data_directory                      | /var/lib/postgresql/16/main             |
 data_sync_retry                     | off                                     |
 debug_io_direct                     |                                         |
 dynamic_shared_memory_type          | posix                                   |
 event_source                        | PostgreSQL                              |
 external_pid_file                   | /var/run/postgresql/16-main.pid         |
 hba_file                            | /etc/postgresql/16/main/pg_hba.conf     |
 hot_standby                         | on                                      |
 huge_page_size                      | 0                                       | kB
 huge_pages                          | try                                     |
 ident_file                          | /etc/postgresql/16/main/pg_ident.conf   |
 ignore_invalid_pages                | off                                     |
 jit_provider                        | llvmjit                                 |
 listen_addresses                    | localhost                               |
 logging_collector                   | off                                     |
 max_connections                     | 100                                     |
 max_files_per_process               | 1000                                    |
 max_locks_per_transaction           | 64                                      |
 max_logical_replication_workers     | 4                                       |
 max_pred_locks_per_transaction      | 64                                      |
 max_prepared_transactions           | 0                                       |
 max_replication_slots               | 10                                      |
 max_wal_senders                     | 10                                      |
 max_worker_processes                | 8                                       |
 min_dynamic_shared_memory           | 0                                       | MB
 old_snapshot_threshold              | -1                                      | min
 port                                | 5432                                    |
 recovery_target                     |                                         |
 recovery_target_action              | pause                                   |
 recovery_target_inclusive           | on                                      |
 recovery_target_lsn                 |                                         |
 recovery_target_name                |                                         |
 recovery_target_time                |                                         |
 recovery_target_timeline            | latest                                  |
 recovery_target_xid                 |                                         |
 reserved_connections                | 0                                       |
 shared_buffers                      | 16384                                   | 8kB
 shared_memory_type                  | mmap                                    |
 shared_preload_libraries            |                                         |
 superuser_reserved_connections      | 3                                       |
 track_activity_query_size           | 1024                                    | B
 track_commit_timestamp              | off                                     |
 unix_socket_directories             | /var/run/postgresql                     |
 unix_socket_group                   |                                         |
 unix_socket_permissions             | 0777                                    |
 wal_buffers                         | 512                                     | 8kB
 wal_decode_buffer_size              | 524288                                  | B
 wal_level                           | replica                                 |
 wal_log_hints                       | off                                     |
(57 rows)

2. Setting the max\_connections Parameter

The current value of max_connections:

=> \dconfig max_conn*

Page 19: Page 19

List of configuration parameters
    Parameter    | Value
-----------------+-------
 max_connections | 100
(1 row)

Imagine we try to set it to 50, but mistype and enter the letter O instead of the zero:

student$ echo max_connections=5O | sudo tee /etc/postgresql/16/main/conf.d/max_connections.conf
max_connections=5O

The error can be discovered by looking at the pg_file_settings view:

=> SELECT * FROM pg_file_settings WHERE name = 'max_connections'\gx
-[ RECORD 1 ]---------------------------------------------------
sourcefile | /etc/postgresql/16/main/postgresql.conf
sourceline | 65
seqno      | 6
name       | max_connections
setting    | 100
applied    | f
error      |
-[ RECORD 2 ]---------------------------------------------------
sourcefile | /etc/postgresql/16/main/conf.d/max_connections.conf
sourceline | 1
seqno      | 25
name       | max_connections
setting    | 5O
applied    | f
error      | setting could not be applied

But here, we didn’t look at pg_file_settings and restarted the server right away:

=> \q
student$ sudo pg_ctlcluster 16 main restart
Job for postgresql@16-main.service failed because the service did not take the steps
required by its unit configuration.
See "systemctl status postgresql@16-main.service" and "journalctl -xeu
postgresql@16-main.service" for details.

The server does not start. The reason is recorded in the server log. Here is what it says:

student$ tail -n 5 /var/log/postgresql/postgresql-16-main.log
2025-11-27 13:56:17.124 MSK [31821] LOG:  database system is shut down
2025-11-27 13:56:17.380 MSK [32454] LOG:  invalid value for parameter "max_connections":
"5O"
2025-11-27 13:56:17.382 MSK [32454] FATAL:  configuration file
"/etc/postgresql/16/main/conf.d/max_connections.conf" contains errors
pg_ctl: could not start server
Examine the log output.

Let’s fix the error:

student$ echo max_connections=50 | sudo tee /etc/postgresql/16/main/conf.d/max_connections.conf
max_connections=50

The server is not running, so we’ll use an operating system command to verify.

student$ cat /etc/postgresql/16/main/conf.d/max_connections.conf
max_connections=50

Restart the server:

student$ sudo pg_ctlcluster 16 main start

The server starts. Check the max_connections value:

student$ psql
=> SHOW max_connections;
 max_connections
-----------------
 50
(1 row)

Page 20: Page 20

Page 21: Practice+

  1. Set the parameter work_mem = 32MB in the psql tool’s command line options. 2. In the Ubuntu package distribution, the postgresql.conf file is not located in the PGDATA directory. How does the server find this configuration file at startup?

Speaker notes

  1. Use either the options key in the connection string or the PGOPTIONS environment variable.

More on how connection strings are formed:

https://postgrespro.com/docs/postgresql/16/libpq-connect#LIBPQ-CONNST RING

  1. To see the location of the postgresql.conf file, check the config_file parameter.

To find where the parameter is set, run the ps command for the postgres main process. The process ID (PID) is the first line of the postmaster.pid file, which is located in the data directory (PGDATA).

Page 22: 1. Setting Parameters at Application Start

If the app uses the libpq library to connect to the server, there are two ways you can set parameters at application start. The first way is to add the options key to the connection string:

student$ psql "options='-c work_mem=32MB'" -c 'SHOW work_mem'
 work_mem
----------
 32MB
(1 row)

The second way is set the PGOPTIONS environment variable:

student$ export PGOPTIONS='-c work_mem=32MB'; psql -c 'SHOW work_mem'
 work_mem
----------
 32MB
(1 row)

2. The config\_file Variable

The file postgresql.conf isn’t located within the data catalog:

=> \dconfig (config_file|data_directory)
             List of configuration parameters
   Parameter    |                  Value
----------------+-----------------------------------------
 config_file    | /etc/postgresql/16/main/postgresql.conf
 data_directory | /var/lib/postgresql/16/main
(2 rows)

How does the server know where postgresql.conf is? In the Ubuntu package distribution, the config_file value is defined in the server startup command text. This allows the postgresql.conf file to be stored outside of PGDATA. To check the server’s startup command line, you can use the ps command. Find the PID of the main server process – it’s listed in the first line of the postmaster.pid file located in the PGDATA directory:

student$ sudo cat /var/lib/postgresql/16/main/postmaster.pid | head -n 1
32795

Now we can retrieve the postmaster process command line:

student$ ps -p 32795 -ho command
/usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/main -c
config_file=/etc/postgresql/16/main/postgresql.conf

04 Lesson 04: Architecture / PostgreSQL Fundamentals

Native Markdown source page: Architecture / PostgreSQL Fundamentals
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_04_arch_general.html Archive: DBA1-handouts-16-en.zip PDF pages: 20

Page 1: Architecture / PostgreSQL Fundamentals

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Igor Gnatyuk Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Client-Server Protocol
  • Transactionality and its Implementation
  • Query Processing and Execution
  • Processes and Memory Structures
  • Storing Data on Disk and Data Processing
  • System Extensibility

Page 3: Client and Server

Source slide page 3: Client and Server{#source-figure-003 caption=“Source slide page 3: Client and Server”}

  • connection
  • authentication
  • query generation
  • query execution
  • transaction management
  • transactionality support

Speaker notes

A client application, such as psql or any other program written in any programming language, connects to the server and “communicates” with it. In order for the client and the server to understand each other, they must use the same communication protocol. Usually, the client uses a driver that implements the protocol and provides a set of functions to use in the program. Internally, the driver can use the standard protocol implementation (the libpq library), or can implement the protocol itself.

The language the client is written in is unimportant, as the functionality behind the syntax is defined by the protocol. As an example, we will use the SQL language and the psql client. Of course, no one really would program a client in SQL, but we will use it here purely for educational purposes. It should not be that difficult to substitute any of the SQL commands provided below with corresponding statements in your programming language of choice.

Generally speaking, a connection protocol allows the client to connect to a database in a cluster. The server performs authentication: decides if the client should be allowed to connect, i.e. by demanding a password.

Then, the client sends the server queries in the SQL language, the server executes the queries and sends back the results. A powerful and convenient query language is one of the fundamentals of relational databases.

Another one is the ability to maintain transaction support.

https://postgrespro.com/docs/postgresql/16/protocol

Page 4: Transactions

Source slide page 4: Transactions{#source-figure-004 caption=“Source slide page 4: Transactions”}

  • atomicity — everything or nothing consistency — integrity constraints and user restrictions isolation — parallel processes impact
  • durability — no data loss after a failure

Speaker notes

A transaction is a sequence of operations that preserves the consistency of data, provided that the operations are performed completely and without interference from other transactions.

Transactions must satisfy four properties collectively known as ACID:

● Atomicity. A transaction is either executed in full or not executed at all. To that end, the beginning of a transaction is marked with the BEGIN command, and the end with either COMMIT (commit changes) or ROLLBACK (undo changes).

● Consistency. Transactions move the database from one consistent state to another consistent one (consistency here means that certain restrictions are fulfilled).

● Isolation. Transactions running simultaneously should not affect the specific one.

● Durability. Once data is committed, it should not be lost even after a server failure.

In PostgreSQL, the client application is the side usually responsible for transaction management (that is, for determining what commands make up a transaction, and for committing or canceling the transaction). Transactions can be managed on the server side by stored procedures.

https://postgrespro.com/docs/postgresql/16/sql-begin

https://postgrespro.com/docs/postgresql/16/sql-savepoint

https://postgrespro.com/docs/postgresql/16/transactions

Page 5: Transaction Management

By default, psql runs in autocommit mode:

=> \echo :AUTOCOMMIT
on

This means that any single command sent without explicitly opening the transaction is committed immediately. Check if this mode is enabled by default in the PostgreSQL driver for your favourite programming language. Create a table with one row:

=> CREATE TABLE t(
    id integer,
    s text
);
CREATE TABLE
=> INSERT INTO t(id, s) VALUES (1, 'foo');
INSERT 0 1

Will another transaction see this table and row?

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
(1 row)

Yes. Compare the result:

=> BEGIN; -- explicit start of transaction
BEGIN
=> INSERT INTO t(id, s) VALUES (2, 'bar');
INSERT 0 1

What will the other transaction see now?

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
(1 row)

The changes are not yet committed so the other transaction does not see them.

=> COMMIT;
COMMIT

What about now?

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
(2 rows)

When autocommit is off, a transaction is implicitly opened upon command input. The command then must be committed manually.

=> \set AUTOCOMMIT off
=> INSERT INTO t(id, s) VALUES (3, 'baz');
INSERT 0 1

What do we see now?

Page 6: Page 6

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
(2 rows)

The changes are not there, as the transaction was opened implicitly.

=> COMMIT;
COMMIT

Now, finally:

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
  3 | baz
(3 rows)

Turn the default autocommit mode back on.

=> \set AUTOCOMMIT on

Individual changes can be rolled back without interrupting the entire transaction (although this is rarely used).

=> BEGIN;
BEGIN
=> SAVEPOINT sp;
SAVEPOINT
=> INSERT INTO t(id, s) VALUES (4, 'qux');
INSERT 0 1
=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
  3 | baz
  4 | qux
(4 rows)
Note

Note Note how the transaction sees its own changes, even the uncommitted ones. Now, roll back to the save point. Rolling back to a save point does not imply a transfer of control (that is, it does not work as GOTO); only those changes to the database state that were made from the moment the point was set and up to the current moment are canceled.

=> ROLLBACK TO sp;
ROLLBACK

Check the table:

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
  3 | baz
(3 rows)

The changes have been rolled back, but the transaction is still open:

=> INSERT INTO t(id, s) VALUES (4, 'xyz');
INSERT 0 1

Page 7: Page 7

=> COMMIT;
COMMIT
=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
  3 | baz
  4 | xyz
(4 rows)

Page 8: Query Processing

Source slide page 8: Query Processing{#source-figure-008 caption=“Source slide page 8: Query Processing”}

parsing ← system catalog rewriting ← rules planning ← statistics execution ← data

Speaker notes

Query execution is complicated. First, a query is sent from a client to the server as text. The server parses the text, analyzing its syntax (whether letters are formed into words, and words into commands) and semantics (whether there are tables and other objects in the database that the query refers to by name). To do that, the server needs data on what is actually stored in the database. This meta-data is called the system catalog and is stored in special tables in the same database.

A query can be rewritten (transformed). For example, a view name can be substituted with the query text. Users can implement their own transformations using the rule system.

SQL is a declarative language: a query defines what data to get, but not how to get it. It is at this point when the query (already parsed and presented in the form of a tree) is passed on to the planner, which develops an execution plan. For example, the planner can decide whether or not to use indexes to find the data for the query. To plan the execution efficiently, the planner needs certain information about the tables it is going to work with, such as the size of the tables and the distribution of data within them. Together, this information is called statistics.

When a plan is selected, the query is executed in accordance with it, and the result is returned to the client in its entirety.

https://postgrespro.com/docs/postgresql/16/query-path

This is convenient and simple when we are talking about just a row or two, provided by the simple mode of the protocol.

Page 9: Prepared Statements

Source slide page 9: Prepared Statements{#source-figure-009 caption=“Source slide page 9: Prepared Statements”}

parsing rewriting

  • binding ← parameter values planning execution

Speaker notes

Each query goes through the steps listed above: parsing, rewriting, planning and execution. But if the same query (possibly with different parameters) is executed over and over again, there is no point in parsing it anew every time.

Therefore, in addition to the usual query execution process, the PostgreSQL protocol provides an extended mode that can control statement execution more precisely.

One of its features is the ability to prepare a statement. When a statement is prepared, it is parsed and rewritten as usual and its parse tree is saved.

When the statement is executed, specific parameter values are bound to it. If necessary, planning is redone (in some cases, PostgreSQL remembers the query plan and does not repeat this step). Then, the statement is executed.

Another advantage of prepared statements is that they are protected from possible SQL injections.

https://postgrespro.com/docs/postgresql/16/sql-prepare

https://postgrespro.com/docs/postgresql/16/sql-execute

Page 10: Prepared Statements

In SQL, you can prepare a statement by using the PREPARE command (it is a PostgreSQL extension not present in the SQL standard):

=> PREPARE q(integer) AS
  SELECT * FROM t WHERE id = $1;
PREPARE

The statement is parsed and rewritten, and the parse tree is saved. A prepared statement can be called by its name using arbitrary parameters:

=> EXECUTE q(1);
 id |  s
----+-----
  1 | foo
(1 row)

For non-parametric statements, an execution plan is saved as well. For queries with parameters, as in the example, the parameter values are taken into account at the planning stage. The planner may decide that the plan built without considering the parameters is good enough. In this case, it will not try to plan the query again. How do you prepare and execute a statement using your favourite programming language? Can you execute a statement WITHOUT preparing it? All prepared statements in the current session can be displayed with:

=> SELECT * FROM pg_prepared_statements \gx
-[ RECORD 1 ]---+---------------------------------
name            | q
statement       | PREPARE q(integer) AS           +
                |   SELECT * FROM t WHERE id = $1;
prepare_time    | 2025-11-27 13:47:05.19667+03
parameter_types | {integer}
result_types    | {integer,text}
from_sql        | t
generic_plans   | 0
custom_plans    | 1

Page 11: Cursors

Source slide page 11: Cursors{#source-figure-011 caption=“Source slide page 11: Cursors”}

parsing rewriting

  • binding ← parameter values planning execution
  • output

Speaker notes

The client may not want to get all the output at once. There can be too much data, and not all of it may be needed.

This issue is solved by cursors, another feature of the extended mode. The protocol can open a cursor for any operator, and then receive the output row by row.

A cursor can be imagined as a sliding window that shows only a part of the output at a time. When an output row is received, the window shifts down. In other words, cursors allow you to work with relational data (that comes in sets) iteratively, row by row.

An open cursor is represented on the server by a so-called portal. This term is mentioned in the documentation, but in general, the words “cursor” and “portal” can be considered synonyms.

A statement used within a cursor is implicitly prepared (that is, its parsing tree and possibly execution plan are saved).

https://postgrespro.com/docs/postgresql/16/sql-declare

https://postgrespro.com/docs/postgresql/16/sql-fetch

Page 12: Cursors

When the SELECT command is executed, the server sends and the client receives all the rows at once:

=> SELECT * FROM t ORDER BY id;
 id |  s
----+-----
  1 | foo
  2 | bar
  3 | baz
  4 | xyz
(4 rows)

Cursors let the client to retrieve data in batches of rows.

=> BEGIN;
BEGIN
=> DECLARE c CURSOR FOR
  SELECT * FROM t ORDER BY id;
DECLARE CURSOR
=> FETCH c;
 id |  s
----+-----
  1 | foo
(1 row)

You can set the size of the batch:

=> FETCH 2 c;
 id |  s
----+-----
  2 | bar
  3 | baz
(2 rows)

The batch size is paramount for huge outputs, when processing them one row at a time is inefficient. What if we reach the end of the table?

=> FETCH 2 c;
 id |  s
----+-----
  4 | xyz
(1 row)
=> FETCH 2 c;
 id | s
----+---
(0 rows)

FETCH will just stop returning rows. All regular programming languages have a way to check for this condition. How do you get data row by row using the cursor in your favorite programming language? Is it possible NOT to use the cursor and get all the rows at once? How is the cursor batch size set? You can close your cursor when done, freeing up some resources:

=> CLOSE c;
CLOSE CURSOR

However, cursors are automatically closed when the transaction ends, so explicit closing is optional (except WITH HOLD cursors.)

=> COMMIT;

Page 13: Page 13

COMMIT

Page 14: Processes and Memory

Source slide page 14: Processes and Memory{#source-figure-014 caption=“Source slide page 14: Processes and Memory”}

Speaker notes

Between processing queries from clients, the server must store technical information, such as parsed queries and their plans and the status of open cursors (portals). Where is it stored and how?

Under the hood, a PostgreSQL server consists of several interacting processes. First of all, when the server starts, a process traditionally called postmaster is started. It starts all other processes (using the fork system call in Unix). It also “babysits” them: if any of the processes crashes, postmaster will restart it (or restart the entire server if it considers that the failed process could have damaged any of the shared data).

Operations of the server are maintained by a number of background processes. The main ones will be discussed in the following lessons.

In order for the processes to exchange information between them, postmaster allocates shared memory that all the processes can access. In addition to shared memory, each process has its own local memory, accessible only to itself.

Postmaster also listens for incoming connections. For each connecting client, postmaster generates a designated backend process for the client to communicate with on the server side, and each client gets its own process. The backend process performs authentication, among other things.

The space required to execute a client’s query (parsed queries and their plans, cursor states, system catalog cache, a place to sort data, etc.) is allocated in the local memory of the backend process of this client.

Page 15: Multiple Clients

Source slide page 15: Multiple Clients{#source-figure-015 caption=“Source slide page 15: Multiple Clients”}

Speaker notes

When multiple clients connect to a server, each one gets a backend process created for it. As long as there are not too many clients, RAM is sufficient, and connections do not occur too often, sustaining many connections at once is not a problem in itself.

However, when multiple processes try to access the same database object, things must be done to ensure that one process will not change the data while another is in the process of reading it.

For objects in shared memory, this is ensured by short-term locks. PostgreSQL does this carefully enough so that the system scales well with an increase in the number of processors (cores).

Tables are more complicated. Locks will have to be held until the end of transactions (that is, potentially for a long time), so scalability may suffer. To avoid that, PostgreSQL uses a multiversion concurrency control mechanism (MVCC) and snapshot isolation: multiple versions of the same data can exist simultaneously, and each process sees only its own (but always consistent) data snapshot. Now, only those processes that are trying to change data that has already been changed, but not yet committed by other processes, will be locked.

MVCC is the main mechanism that enables the first three properties of transactions (atomicity, consistency, and isolation). We will talk about it more in its dedicated lesson.

Page 16: Connection Pool

Source slide page 16: Connection Pool{#source-figure-016 caption=“Source slide page 16: Connection Pool”}

Speaker notes

If there are too many clients, or connections are established and broken too often, using a connection pool may help. This functionality is usually provided by the application server or third-party pool managers (the most popular of which is pgBouncer).

With a connection pool in place, clients connect not to the PostgreSQL server directly, but to the pool manager. The manager keeps several connections to the database server open and uses free ones to fulfill client queries. From the server’s point of view, the number of clients remains constant regardless of how many clients access the pool manager.

The drawback is that multiple clients end up sharing the same backend process, which, as we remember, stores client-specific state in its local memory (such as parsed queries for prepared statements). Therefore, care should be taken when developing applications for such deployments.

One of pgBouncer’s features is the ability to temporarily pause client connections without disconnecting them. This pause can be used for server software updates or other operations that require a server restart.

The use of connection pools is discussed in greater detail in the DEV2 course.

Page 17: Data Storage

Source slide page 17: Data Storage{#source-figure-017 caption=“Source slide page 17: Data Storage”}

Speaker notes

Data is stored as regular OS files on disks. How exactly the data is distributed among the files is discussed in one of following lessons.

Logically, the files are divided into pages (sometimes the term block is used). A page is usually 8KB in size. It can be changed within some limits (16 KB or 32 KB), but only during server compilation. A cluster that has been compiled and started up this way can work with pages of only one size.

Each page has a certain internal structure. It contains a header and actual data. There may be free space between them if the page is not fully occupied.

Since disks work much slower than RAM (especially HDD, but SSD too), data heading to disk is cached first. A buffer cache, a certain amount of space in RAM, is allocated for recently read pages. The idea is that the system may want to read the same pages multiple times, and keeping them on hand may save time compared to repeated disk scans. Any recently changed data is also cached for some time before being written on disk.

Page 18: Data Storage

Source slide page 18: Data Storage{#source-figure-018 caption=“Source slide page 18: Data Storage”}

Speaker notes

The PostgreSQL buffer cache is located in shared memory so that all processes have access to it.

PostgreSQL does not directly access disks storing its data. Instead, it relies on the operating system. The operating system also has its own data cache. Therefore, if a page is not found in the buffer cache, there is a chance that it is in the OS cache and access to the disk will be avoided.

In case of a failure (for example, power supply dies), the contents of the RAM are lost, and the data changed but not yet written to disk will be lost. This is unacceptable and breaks the durability property of transactions. To avoid that, PostgreSQL keeps a log that allows it to redo lost operations and restore data to a consistent state. We will talk about the buffer cache and the write-ahead log in the dedicated lesson later on.

Page 19: Extensibility

Source slide page 19: Extensibility{#source-figure-019 caption=“Source slide page 19: Extensibility”}

Speaker notes

PostgreSQL is designed with extensibility in mind. It allows to create new data types based on existing ones, write stored procedures for data processing, and offers a convenient toolkit for administration, monitoring, and performance tuning.

You can always write an extension that adds the necessary functionality that you need. Most extensions can be installed “hot”, without stopping the server. Thanks to this architecture, there are plenty of existing extensions doing things such as:

● adding support for programming languages (in addition to standard SQL, PL/pgSQL, PL/Perl, PL/Python and PL/Tcl);

● introducing new data types and operators to work with them;

● creating new index types that work more efficiently with specific data types (in addition to standard B-trees, hash indexes, GiST, SP-GiST, GIN, BRIN);

● starting background workers to perform additional tasks;

● connecting to external data sources;

● collecting system load information, performing monitoring, and generating reports;

● exploring system data structures.

Extensibility is discussed in more detail in the DBA2 and DEV2 courses.

Page 20: Takeaways

A server manages a database cluster

  • The protocol allows clients to connect to the server, transmit queries and manage transactions
  • Each client is served by a dedicated backend process
  • Data is stored in files and accessed via the operating system
  • Data is cached both in local memory (system catalog, parsed queries) and in shared memory (buffer cache)

05 Lesson 05: Architecture / Isolation and MVCC

Native Markdown source page: Architecture / Isolation and MVCC
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_05_arch_mvcc_overview.html Archive: DBA1-handouts-16-en.zip PDF pages: 18

Page 1: Architecture / Isolation and MVCC

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Igor Gnatyuk Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is”, and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Multiversion Concurrency Control
  • Data Snapshot
  • Isolation Levels
  • Vacuuming and its Horizon
  • Locking
  • Transaction Status

Page 3: MVCC

Source slide page 3: MVCC{#source-figure-003 caption=“Source slide page 3: MVCC”}

Storing multiple versions of the same row

  • versions have different validity time
  • timestamp = transaction ID (IDs are given in ascending order)

Speaker notes

When multiple sessions are running at the same time, two transactions may access the same row at the same time. If both transactions are just reading the row, there is no problem. If both transactions try to write, no problem either (in this case, they line up and make the changes one after the other). The tricky part is when one transaction wants to read a row and another one wants to change it at the same time.

There are two simple ways about it. You can let such transactions block each other, but then performance suffers. Otherwise, you can let the reading transaction immediately see the changes made by the writing transaction, even if they are not committed (this is called a “dirty read”). This is dangerous, because the changes can be rolled back.

PostgreSQL goes the hard way and utilizes what is known as Multiversion concurrency control. In essence, the system stores multiple versions of each row. So, a writing transaction operates on its own version, while a reading transaction sees its own version.

To distinguish between the versions, PostgreSQL marks each one with two timestamps, which together specify a version’s “validity time”.

The timestamps are essentially just transaction IDs, which always come in ascending order. (In reality, the whole thing is a bit more complicated, but not worth getting into right now.) Upon creation, a row version is marked with the ID of the transaction that executed the INSERT command. When deleted, the version is marked with the ID of the transaction that did the DELETE command (but is not physically deleted). An UPDATE command is a DELETE and an INSERT executed back to back.

https://postgrespro.com/docs/postgresql/16/mvcc-intro

Page 4: Data Snapshot

Source slide page 4: Data Snapshot{#source-figure-004 caption=“Source slide page 4: Data Snapshot”}

Representation of database data in a consistent state at a specific point in time

  • transaction ID defines the point in time
  • list of active transactions helps the system
  • to exclude changes that have not been committed

Speaker notes

PostgreSQL uses snapshot-based transaction isolation.

A transaction accessing a table should see only one of the versions of each row (or none at all). To achieve this, PostgreSQL presents the transaction with a data snapshot created at a certain point in time. The snapshot includes the most recent versions of all committed rows but does not include any non-committed changes from active transactions. In other words, the snapshot takes the version of each row that corresponds to the moment when the snapshot was created.

A snapshot is not a physical copy of the data, but just a few numbers:

● the ID of the last committed transaction at the time of snapshot,

● list of active transactions at that point in time.

The list is needed in order to exclude from the snapshot any changes those transactions may have made but not yet committed.

With just these numbers, we can always tell which row version will be visible in the snapshot. Sometimes it will be the current (most recently committed) version, as with row 1 in the diagram. Sometimes not: row 2 has been deleted after the snapshot has been created (and the change has already been committed), but the transaction still continues to see it while working with the snapshot. This is the correct behavior, the snapshot gives a consistent representation of data at the selected point in time.

Some rows will not get into the snapshot at all: row 3 was deleted before the snapshot was made, so it is not included.

Page 5: Isolation Levels

Read Uncommitted

  • not supported by PostgreSQL: works as Read Committed
  • Read Committed (default)
    • the snapshot is created as of the time a statement starts
    • repeated query may return different data
  • Repeatable Read
    • the snapshot is created as of the time the first transaction statement starts
    • a transaction may fail with a serialization error
  • Serializable
    • total isolation, but additional overhead
    • a transaction may fail with a serialization error

Speaker notes

The SQL standard defines four isolation levels: the stricter the level, the less concurrent transactions affect each other. At the time when the standard was adopted, it was believed that the stricter the level, the more difficult it is to implement and the stronger its impact on performance (since then, these views have changed somewhat).

The most lax level of Read Uncommitted allows dirty reads. It is not supported by PostgreSQL, because it is of no practical value and does not give a performance gain.

The Read Committed level is the default isolation level in PostgreSQL. At this level, data snapshots are created at the beginning of each SQL statement execution. Thus, the statement works with an unchanged and consistent data snapshot, but two identical queries following one after the other may show different data.

At the Repeatable Read level, the snapshot is built at the beginning of a transaction (when executing the first statement). This makes all queries inside the same transaction see the same data. This level is convenient, for example, for generating reports from several queries.

The Serializable level guarantees total isolation. At this level, you can use any statements as if the transaction is running alone. The drawback is that some transactions will fail, and your application must be able to repeat such transactions.

https://postgrespro.com/docs/postgresql/16/transaction-iso

Page 6: Row Version Visibility

How do you verify that the same row can exist in multiple versions? Create a table:

=> CREATE TABLE t(s text);
CREATE TABLE

And insert one row. Remember that if you do not explicitly start a transaction with BEGIN, psql executes the command and immediately commits the result:

=> INSERT INTO t VALUES ('Version one');
INSERT 0 1

Start the transaction and get its ID:

=> BEGIN;
BEGIN
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                736
(1 row)

The transaction sees the first (and so far the only) version of the row:

=> SELECT *, xmin, xmax FROM t;
      s      | xmin | xmax
-------------+------+------
 Version one |  735 |    0
(1 row)

Here, the hidden columns show the transaction IDs that limit the visibility of the row version: xmin is the ID of the previous transaction which created the version, and xmax=0 means that this version is current. Now start another transaction in another session:

=> BEGIN;
BEGIN
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                737
(1 row)

The transaction sees the same and only version:

=> SELECT *, xmin, xmax FROM t;
      s      | xmin | xmax
-------------+------+------
 Version one |  735 |    0
(1 row)

Now change the row from within the second transaction.

=> UPDATE t SET s = 'Version two';
UPDATE 1

This is what we get:

=> SELECT *, xmin, xmax FROM t;

Page 7: Page 7

      s      | xmin | xmax
-------------+------+------
 Version two |  737 |    0
(1 row)

What will the first transaction see?

=> SELECT *, xmin, xmax FROM t;
      s      | xmin | xmax
-------------+------+------
 Version one |  735 |  737
(1 row)

Since the change is not yet committed, the first transaction continues to see the first version of the row.

Note

Note the xmax value: it indicates that another transaction is currently changing the row. Generally speaking, such “peeping” violates isolation, so the xmin and xmax fields are hidden and should not be used in production. Now commit the changes.

=> COMMIT;
COMMIT

What will the first transaction see now?

=> SELECT *, xmin, xmax FROM t;
      s      | xmin | xmax
-------------+------+------
 Version two |  737 |    0
(1 row)

The first transaction also sees the second version of the string. After commit, the first version of the row is no longer visible in any transaction.

=> COMMIT;
COMMIT

Page 8: Vacuuming and its Horizon

Source slide page 8: Vacuuming and its Horizon{#source-figure-008 caption=“Source slide page 8: Vacuuming and its Horizon”}

There is a single horizon for each database

  • Long-running transactions can hold back the horizon
    • thereby blocking the vacuuming of outdated row versions

Speaker notes

MVCC makes it possible to effectively implement snapshot-based isolation, but as a result, old (“dead”) row versions (“dead tuples”) accumulate in table pages.

Historical versions are needed for some time so that transactions can work with their data snapshots and, in case of rollback, get back to old values. For each database, there is a transaction ID (xid), so that all historical versions deleted by transactions with lower xids are no longer visible in any snapshot. This xid is called vacuum horizon.

Vacuuming is performed by special background processes; but can also be executed manually using the VACUUM command.

Long-running transactions and queries can hold back the horizon, which prevents the removal of accumulated row versions. If you do not vacuum historical data in a timely manner, tables and indexes will bloat, consuming excessive disk space, and the search for current row versions will slow down.

https://postgrespro.com/docs/postgresql/16/routine-vacuuming

Page 9: Locking

Row locks

  • reading never locks rows
  • changing rows locks them for changes, but not for reads
  • Table locks
    • prohibit changing or deleting a table while it is being worked on
    • prohibit reading the table when rebuilding or moving
    • etc.
  • Lock lifetime
    • set as needed or manually
    • released automatically upon transaction completion

Speaker notes

So what does MVCC provide? It allows the system to have only the most necessary minimum of locks, thereby increasing performance.

The main locks are set at the row level. Reading never blocks either reading or writing transactions. Changing a row does not lock it for reading. The only case when a transaction will wait for a lock to be released is if it tries to change a row that has already been changed by another transaction that has not been committed yet.

Locks can also be set at a higher level, particularly on tables. They are needed so that no one can delete the table while other transactions are reading data from it, or to prohibit access to the table being rebuilt. Such locks generally do not cause problems, since deleting or rebuilding tables is only done once in a while. However, some changes of table structure trigger implicit table rebuilding, completely blocking access to the table and its indexes. Locks are explained in detail in the Locking module of the DBA2 course.

All necessary locks are set automatically and automatically removed at the end of the transaction. However, if a lock was acquired after the savepoint was set, it will be released immediately when rolling back to that savepoint.

You can also set additional custom locks, but this is rarely necessary.

https://postgrespro.com/docs/postgresql/16/explicit-locking

Page 10: Locking

Repeat the experiment, but now let both transactions try to change the same row.

=> BEGIN;
BEGIN
=> UPDATE t SET s = 'Version three' RETURNING *;
       s
---------------
 Version three
(1 row)
UPDATE 1

And in the second transaction:

=> BEGIN;
BEGIN
=> UPDATE t SET s = 'Version four' RETURNING *;

The second transaction “hung up”: it cannot change the row until the first transaction releases the lock.

=> COMMIT;
COMMIT

Now the second transaction can continue:

      s
--------------
 Version four
(1 row)
UPDATE 1
=> COMMIT;
COMMIT

Both transactions have committed their changes. The first session reads the table again and sees the current row: this is the result committed by the second transaction:

=> SELECT * FROM t;
      s
--------------
 Version four
(1 row)

Page 11: Transaction Status

Transaction status (clog)

  • service information; two bits per transaction
  • stored in files on disk
  • cached in shared memory
  • Commit
    • the “transaction committed” bit is set
  • Termination
    • the “transaction aborted” bit is set
    • performed as fast as commit (no data rollback needed)

Speaker notes

For multiversion concurrency control to work, the server needs to understand the status of transactions. A transaction can be active or finished. A transaction can end either in a commit or in an abort. Therefore, two bits are required to represent the state of each transaction.

Transaction statuses (commit log, clog) are stored in special service files in the PGDATA/pg_xact directory and worked upon in the server’s shared memory, as to avoid constantly accessing the disk.

At any transaction completion (either successful or not), it is enough to set the appropriate status bits. Both transaction commit and abort occur equally quickly.

If an aborted transaction managed to create new row versions, these versions are not destroyed (there is no “physical” rollback of data). Thanks to the status information, other transactions will see that the transaction that created or deleted the row versions was actually aborted, and will not take changes made by it into account.

Page 12: Takeaways

Multiple versions of each row can be stored in data files

  • Transactions work with data snapshots, representations of database data in a consistent state at a specific point in time
  • Isolation levels have different snapshot creation times
  • Dead row versions beyond the vacuum horizon must be periodically vacuumed
  • Writers do not block readers, readers do not block anyone

Page 13: Practice

  1. Create a table with one row. Begin a transaction at the Read Committed isolation level and query the table. In another session, delete the row and commit the changes. How many rows will the first transaction see after executing the same query again? Try and see. Complete the first transaction. 2. Repeat the same thing, but now let the transaction work at the isolation level Repeatable Read:
  • BEGIN ISOLATION LEVEL REPEATABLE READ;
  • Explain the differences.

Page 14: 1. Read Committed Isolation Level

Create a table:

=> CREATE TABLE t(n integer);
CREATE TABLE
=> INSERT INTO t VALUES (42);
INSERT 0 1

Query from the first transaction (default isolation level is Read Committed):

=> BEGIN;
BEGIN
=> SELECT * FROM t;
 n
----
 42
(1 row)

Delete the row in the second transaction and commit the changes:

=> DELETE FROM t;
DELETE 1

Repeat the query:

=> SELECT * FROM t;
 n
---
(0 rows)

The first transaction sees the change: the row has been deleted.

=> COMMIT;
COMMIT

2. Repeatable Read Isolation Level

Return the row:

=> INSERT INTO t VALUES (42);
INSERT 0 1

Query from the first transaction:

=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
=> SELECT * FROM t;
 n
----
 42
(1 row)

Delete the row in the second transaction and commit the changes:

=> DELETE FROM t;
DELETE 1

Repeat the query:

=> SELECT * FROM t;

Page 15: Page 15

 n
----
 42
(1 row)

At this isolation level, the first transaction does not see any changes: the row still exists for it.

=> COMMIT;
COMMIT

Page 16: Practice+

  1. Begin a transaction and create a new table with one row. Without completing the transaction, open a second session and query the table in it. Check what the transaction will return in the second session.
  • Commit the transaction in the first session and repeat the query
  • to the table in the second session.
    1. Repeat task 1, but roll back rather than commit the transaction in the first session. What has changed?
    1. In the first session, start a transaction and make a query to the previously created table. Will it be possible to delete this table in the second session before the transaction is completed? Try and see.

Page 17: 1. Transactions and DDL Commands: Commit

Start a transaction and create a new table:

=> BEGIN;
BEGIN
=> CREATE TABLE t1(n integer);
CREATE TABLE
=> INSERT INTO t1 VALUES (42);
INSERT 0 1

In the second session, query the table:

=> SELECT * FROM t1;
ERROR:  relation "t1" does not exist
LINE 1: SELECT * FROM t1;
                      ^

Until the transaction that created the table is completed, all other transactions do not see the table. The table will be visible only after the completion of the transaction that created it:

=> COMMIT;
COMMIT
=> SELECT * FROM t1;
 n
----
 42
(1 row)

2. Transactions and DDL Commands: Rollback

Start a transaction and create a new table:

=> BEGIN;
BEGIN
=> CREATE TABLE t2(n integer);
CREATE TABLE
=> INSERT INTO t2 VALUES (42);
INSERT 0 1

The query in the second session sees no changes, as expected:

=> SELECT * FROM t2;
ERROR:  relation "t2" does not exist
LINE 1: SELECT * FROM t2;
                      ^

When the first transaction is rolled back, the table creation command is also rolled back:

=> ROLLBACK;
ROLLBACK
=> SELECT * FROM t2;
ERROR:  relation "t2" does not exist
LINE 1: SELECT * FROM t2;
                      ^

In PostgreSQL, DDL commands are transactional.

3. Table Locks

Page 18: Page 18

Start a transaction, query the table:

=> BEGIN;
BEGIN
=> SELECT * FROM t1;
 n
----
 42
(1 row)

The second transaction, which is trying to remove the table is locked: you cannot remove a table that is being used.

=> DROP TABLE t1;

The table will be removed only after the first transaction is completed:

=> COMMIT;
COMMIT
DROP TABLE

06 Lesson 06: Architecture / Vacuuming

Native Markdown source page: Architecture / Vacuuming
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_06_arch_vacuum_overview.html Archive: DBA1-handouts-16-en.zip PDF pages: 21

Page 1: Architecture / Vacuuming

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Routine Tasks
  • Autovacuum
  • Vacuum and Analysis
  • Table and Index Bloating
  • Full Vacuum and Rebuilding of Indexes

Page 3: Routine Tasks

Cleaning pages to remove MVCC historical data

  • dead row versions are vacuumed out of tables
    • index entries referencing dead versions are vacuumed from indexes

Speaker notes

MVCC makes it possible to effectively implement snapshot isolation, but as a result, old versions of rows accumulate in table pages, and references to these versions accumulate in index pages. Historical versions are needed for some time so that transactions can work with their data snapshots. But over time, a row version will no longer have any snapshot that needs it. Such a version is called “dead”.

The vacuum procedure cleans out dead row versions from table pages, as well as unnecessary index entries that reference such versions.

If historical data is not vacuumed in a timely manner, tables and indexes will bloat uncontrollably and the search for current row versions in them will slow down.

https://postgrespro.com/docs/postgresql/16/routine-vacuuming

Page 4: Routine Tasks

Updating the visibility map

  • tracks pages where all row versions are visible in all snapshots
  • used to optimize vacuuming and speed up index access
  • exists only for tables

Speaker notes

In addition to this main task, vacuuming also performs other instance maintenance tasks. Vacuuming updates the visibility map and the free space map. This is service information that is stored alongside the main data.

The visibility map shows pages that contain only the current row versions visible in all data snapshots. In other words, these are pages that have not changed for long enough to be cleared out of outdated row versions.

The visibility map has several uses:

● Vacuuming optimization.

The marked pages cannot contain dead row versions, so they can be skipped during vacuuming.

● Index-Only access speedup.

Versioning information is stored only for tables, but not for indexes (that is why indexes do not have visibility maps). After getting a reference to a row version from an index, you usually need to read the table page to check its visibility. But if the index itself already has all the necessary columns, and at the same time the page is marked in the visibility map, then reading the table page can be skipped.

If the visibility map is not updated regularly, index access can slow down. This is described in more detail in the Query Performance Tuning (QPT) course.

Page 5: Routine Tasks

Updating the free space map

  • tracks the free space in the pages after vacuuming
  • used when inserting new row versions
  • exists for both tables and indexes

Speaker notes

The free space map tracks available free space within pages. This space is constantly changing, decreasing when new row versions are added and increasing when they are removed.

When inserting new row versions, the map helps to quickly find a suitable page to record the data into. The free space map has a complex tree-like structure designed to improve search speed.

Indexes can have free space maps as well. However, since index entries are inserted into specific positions within an index, the map only tracks empty pages, which form when all index entries are deleted from them. These pages are excluded from the index and later can be included again into the proper part of the index.

Page 6: Routine Tasks

Updating statistics

  • used by the query optimizer
  • calculated based on a random sample

Speaker notes

The query optimizer requires statistical information about the data it is working with, such as the number of rows in tables and the distribution of data in columns. The process of collecting the statistics is called analysis.

For analysis, a random sample of data of a certain size is read from the table. This way, the system can quickly collect statistics even on very large tables. The result is not accurate, but it is not expected to be. The data constantly changes, so it is impossible to maintain absolutely accurate statistics all of the time anyway. It is sufficient to keep it relatively up-to-date and relatively accurate.

If statistics are not updated regularly, they will no longer represent the data accurately, leading to the optimizer proposing inefficient execution plans. Because of this, queries may start executing orders of magnitude slower than they could.

Page 7: Routine Tasks

Source slide page 7: Routine Tasks{#source-figure-007 caption=“Source slide page 7: Routine Tasks”}

Freezing

  • prevents the consequences of 32-bit transaction counter overflow

Speaker notes

As already mentioned, PostgreSQL orders events by transaction ID. The counter has 32 bits allocated for it, and sooner or later it will overflow.

This is why the transaction ID scope is looped. For each transaction, half of the IDs are considered to be in the future, half in the past.

But when the counter wraps around to zero, the order of transactions will be disrupted. To prevent this, sufficiently old row versions are marked as frozen. This means that they were created so far in the past that their transaction ID no longer means anything and can be reused. Frozen row versions are visible in all snapshots.

To avoid scanning extra pages, the visibility map has a bit that marks the pages where all row versions are frozen.

Without regular freezing, the server may end up with no available transaction IDs for new transactions. This is an emergency: the server stops, all active transactions are aborted, and the administrator has to manually start the server and perform the freezing.

Page 8: Autovacuum

Source slide page 8: Autovacuum{#source-figure-008 caption=“Source slide page 8: Autovacuum”}

Autovacuum launcher

  • background process
  • periodically launches
  • worker processes
  • Autovacuum worker
    • vacuums tables of a
    • specific database that
    • require processing

Speaker notes

All the maintenance tasks discussed above are taken care of by the autovacuum process. It dynamically reacts to the frequency of table updates, and the more active the changes, the more often the table will be vacuumed.

The autovacuum launcher process is permanently running in the background. It schedules the vacuuming work and launches the required number of autovacuum workers working in parallel.

Vacuuming works page-by-page, it does not block other transactions, though it still does require additional I/O resources.

Autovacuum will not work if either of the two parameters autovacuum or track_counts is switched off. It may seem that disabling autovacuum can increase system performance by eliminating “unnecessary” I/O operations, but it cannot. Failure to vacuum will lead to the consequences described above: uncontrolled bloating, slower query processing, and the risk of an emergency server shutdown. Ultimately, this will lead to a complete system paralysis.

Autovacuuming is absolutely necessary. There is a large number of configuration parameters that allow tweaking the autovacuum process. They are discussed in detail in the DBA2 Configuration and Monitoring course.

Page 9: Manual Vacuuming

Vacuuming

  • VACUUM [table, …]
  • vacuum specific tables
  • VACUUM
  • vacuum the entire database
  • $ vacuumdb
  • wrapper for the OS
  • Analysis
ANALYZE
$ vacuumdb --analyze-only
  • Vacuum and analysis
VACUUM ANALYZE
$ vacuumdb --analyze

Speaker notes

If necessary, vacuuming and analysis can be started manually using the following commands:

VACUUM (vacuuming only), ANALYZE (analysis only), and VACUUM ANALYZE (both vacuuming and analysis).

Autovacuum is different from running scheduled vacuuming and analysis as it reacts to the frequency of data changes. Running vacuum on a schedule too often will create unnecessary load on the system. On the other hand, if you vacuum too rarely, and data is changed often, the files may have time to bloat significantly between vacuums.

https://postgrespro.com/docs/postgresql/16/sql-vacuum

https://postgrespro.com/docs/postgresql/16/sql-analyze

Page 10: Vacuuming

=> CREATE DATABASE arch_vacuum_overview;
CREATE DATABASE
=> \c arch_vacuum_overview
You are now connected to database "arch_vacuum_overview" as user "student".

Let’s create a table and turn autovacuum off so that we can control the vacuuming manually in our experiments:

=> CREATE TABLE bloat(
  id integer GENERATED ALWAYS AS IDENTITY,
  d timestamptz
) WITH (autovacuum_enabled = off);
CREATE TABLE

Fill the table with data and build an index:

=> INSERT INTO bloat(d)
  SELECT current_timestamp FROM generate_series(1,100_000);
INSERT 0 100000
=> CREATE INDEX ON bloat(d);
CREATE INDEX

Each table row has only one version, the last one. Now, let’s update some rows:

=> UPDATE bloat SET d = d + interval '1 day' WHERE id <= 10_000;
UPDATE 10000

Run vacuum and have it tell us what it is doing:

=> VACUUM (verbose) bloat;
INFO:  vacuuming "arch_vacuum_overview.public.bloat"
INFO:  finished vacuuming "arch_vacuum_overview.public.bloat": index scans: 1
pages: 0 removed, 595 remain, 595 scanned (100.00% of total)
tuples: 10000 removed, 100000 remain, 0 are dead but not yet removable
removable cutoff: 738, which was 0 XIDs old when operation ended
new relfrozenxid: 735, which is 1 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan needed: 55 pages from table (9.24% of total) had 10000 dead item identifiers
removed
index "bloat_d_idx": pages: 95 in total, 8 newly deleted, 8 currently deleted, 0 reusable
avg read rate: 20.539 MB/s, avg write rate: 3.423 MB/s
buffer usage: 1307 hits, 84 misses, 14 dirtied
WAL usage: 733 records, 1 full page images, 91580 bytes
system usage: CPU: user: 0.01 s, system: 0.00 s, elapsed: 0.03 s
VACUUM

The important lines are: Dead versions of rows have been removed from the table (tuples: 10000 removed…). References to them have been removed from the index (index scan needed… 10000 dead item identifiers removed).

Page 11: Bloating

Vacuuming does not reduce the size of tables and indexes

  • the “holes” in the pages are used for new data,
  • but the space is never returned to the operating system
  • Causes of bloating
    • incorrect autovacuum configuration
    • massive changes of data
    • long-running transactions
  • Negative consequences
    • inefficient disk space use
    • slower sequential table scan
    • less efficient index access

Speaker notes

Vacuuming cleans out outdated row versions from pages. This creates spots of free space in the pages, which is then used to store new data. But the free space is not returned to the operating system, so, from the point of view of the OS, the size of the data files does not decrease.

In the case of indexes (B-trees), it is complicated by the fact that if there is not enough space in the page to place an index entry, the page is split into two. The resulting pages are never merged again, even if all index entries are removed from them.

If autovacuuming is configured correctly, the data files grow by a certain constant amount due to updates between vacuumings. But if a large amount of data is being changed at the same time, or there are active long transactions (keeping old data snapshots active and not allowing you to vacuum out old row versions), vacuuming will not be able to free up the space in time. As a result, the tables and indexes may continue to grow in size.

File bloating leads not only to disk space overuse (including for backups), but also to a decrease in performance.

https://wiki.postgresql.org/wiki/Show\_database\_bloat

https://postgrespro.com/docs/postgresql/16/pgstattuple

Page 12: Table and Index Bloating

There are several ways to assess bloating and its impact: Queries to the system catalog Using the pgstattuple extension

=> CREATE EXTENSION pgstattuple;
CREATE EXTENSION

The extension helps monitor the state of a table:

=> SELECT * FROM pgstattuple('bloat') \gx
-[ RECORD 1 ]------+--------
table_len          | 4874240
tuple_count        | 100000
tuple_len          | 4000000
tuple_percent      | 82.06
dead_tuple_count   | 0
dead_tuple_len     | 0
dead_tuple_percent | 0
free_space         | 457324
free_percent       | 9.38

tuple_percent is the proportion of useful information (not 100% due to overhead). Same for an index:

=> SELECT * FROM pgstatindex('bloat_d_idx') \gx
-[ RECORD 1 ]------+-------
version            | 4
tree_level         | 1
index_size         | 778240
root_block_no      | 3
internal_pages     | 1
leaf_pages         | 85
empty_pages        | 0
deleted_pages      | 8
avg_leaf_density   | 89.17
leaf_fragmentation | 0

leaf_pages is the number of leaf pages of the index. avg_leaf_density is storage density of leaf pages. leaf_fragmentation is characteristic of physical order of leaf pages (0 — order, 100 — disorder). Let’s update half the rows at once:

=> UPDATE bloat SET d = d + interval '1 day' WHERE id % 2 = 0;
UPDATE 50000

Check the table again:

=> SELECT * FROM pgstattuple('bloat') \gx
-[ RECORD 1 ]------+--------
table_len          | 6643712
tuple_count        | 100000
tuple_len          | 4000000
tuple_percent      | 60.21
dead_tuple_count   | 50000
dead_tuple_len     | 2000000
dead_tuple_percent | 30.1
free_space         | 21004
free_percent       | 0.32

The density went down. To avoid scanning the whole table, pgstattuple can display approximate information:

=> SELECT * FROM pgstattuple_approx('bloat') \gx

Page 13: Page 13

-[ RECORD 1 ]--------+--------------------
table_len            | 6643712
scanned_percent      | 100
approx_tuple_count   | 100000
approx_tuple_len     | 4000000
approx_tuple_percent | 60.207305795314426
dead_tuple_count     | 50000
dead_tuple_len       | 2000000
dead_tuple_percent   | 30.103652897657213
approx_free_space    | 21004
approx_free_percent  | 0.31614856273119607

Check the index:

=> SELECT * FROM pgstatindex('bloat_d_idx') \gx
-[ RECORD 1 ]------+--------
version            | 4
tree_level         | 1
index_size         | 1171456
root_block_no      | 3
internal_pages     | 1
leaf_pages         | 133
empty_pages        | 0
deleted_pages      | 8
avg_leaf_density   | 85.41
leaf_fragmentation | 2.26

Leaf page density remained the same, but the number of pages has increased.

Page 14: Rebuilding Objects

Full vacuum

VACUUM FULL
$ vacuumdb --full
  • completely rebuilds the contents of tables and indexes
  • locks the table completely
  • Rebuilding indexes
REINDEX
  • rebuilds indexes
  • locks the index completely
  • and locks the associated table for write operations

Speaker notes

In order to reduce the physical size of bloated tables and indexes, a full vacuum is required.

The VACUUM FULL command completely rewrites the contents of the table and its indexes, minimizing the space occupied. However, this process requires an exclusive table lock and therefore cannot be executed in parallel with other transactions.

https://postgrespro.com/docs/postgresql/16/sql-vacuum

You can rebuild an index or several indexes without touching the table. This is done by the REINDEX command. It locks the table for writing (reading is still available), so transactions trying to change the table or plan a query on it will be blocked.

https://postgrespro.com/docs/postgresql/16/sql-reindex

If prolonged exclusive locking is undesirable, you can consider pg_repack, a third-party extension (https://github.com/reorg/pg\_repack) that allows you to rebuild tables and their indexes on the fly.

Page 15: Rebuilding Objects

Non-blocking index rebuilding

REINDEX ... CONCURRENTLY
  • rebuilds indexes without locking tables for writing
  • takes longer and may fail
  • not transactional
  • does not work for system indexes
  • does not work for indexes associated with exclusion constraints

Speaker notes

The REINDEX … CONCURRENTLY command can work without locking the table for writing. However, non-blocking rebuilding takes longer and may fail (due to deadlocks). In this case, the index will need to be rebuilt again.

Non-blocking index rebuilding has some limitations: it cannot be performed inside a transaction, and it cannot rebuild system indexes and indexes for exclusion constraints (EXCLUDE).

Page 16: Rebuilding Objects

You can rebuild an index using the REINDEX command with the CONCURRENTLY clause. It rebuilds the index without stopping the system.

=> REINDEX TABLE CONCURRENTLY bloat;
REINDEX

Check the index again:

=> SELECT * FROM pgstatindex('bloat_d_idx') \gx
-[ RECORD 1 ]------+-------
version            | 4
tree_level         | 1
index_size         | 712704
root_block_no      | 3
internal_pages     | 1
leaf_pages         | 85
empty_pages        | 0
deleted_pages      | 0
avg_leaf_density   | 89.17
leaf_fragmentation | 0

The page count and density have returned to their initial values. To rebuild a table together with its indexes, you can use VACUUM FULL. However, unlike REINDEX CONCURRENTLY, this operation fully blocks all access to the table.

=> VACUUM FULL bloat;
VACUUM
=> SELECT * FROM pgstattuple('bloat') \gx
-[ RECORD 1 ]------+--------
table_len          | 4431872
tuple_count        | 100000
tuple_len          | 4000000
tuple_percent      | 90.26
dead_tuple_count   | 0
dead_tuple_len     | 0
dead_tuple_percent | 0
free_space         | 16724
free_percent       | 0.38

The density has increased, the freed up space is returned to the operating system.

Page 17: Takeaways

Row versions are accumulated, so periodic vacuuming is necessary

  • Vacuuming serves multiple goals:
    • updating visibility maps and free space maps
    • collecting statistics for the optimizer
    • freezing old row versions
  • Autovacuuming is necessary, but requires configuration
  • Full vacuum may be necessary to combat bloating

Page 18: Practice

  1. Disable autovacuuming and make sure it does not work. 2. In a new database, create a table with one numeric column and an index for this table. Insert 100,000 random numbers into the table. 3. Change half of the table rows several times. Write down the size of the table and the index each time. 4. Run a full vacuum. 5. Repeat step 3, running a regular vacuum each time you change the values. Compare the results. 6. Turn autovacuuming back on.

Speaker notes

  1. Set the autovacuum parameter to off and reload the configuration files.

  2. Use pg_table_size(table-name) and pg_indexes_size(table-name) functions. For more information about calculating the sizes of various objects, see the Data organization module.

  3. Set the autovacuum parameter back to on (or reset its value with the RESET command), then reload the configuration files.

Page 19: 1. Switching Autovacuum Off

Autovacuum is running for now:

=> SELECT pid, backend_start, backend_type
FROM pg_stat_activity
WHERE backend_type = 'autovacuum launcher';
  pid  |         backend_start         |    backend_type
-------+-------------------------------+---------------------
 34776 | 2025-11-27 13:57:17.980347+03 | autovacuum launcher
(1 row)

We disable autovacuum and reload the configuration settings.

=> ALTER SYSTEM SET autovacuum = off;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

The autovacuum launcher process is no longer present.

=> SELECT pid, backend_start, backend_type
FROM pg_stat_activity
WHERE backend_type = 'autovacuum launcher';
 pid | backend_start | backend_type
-----+---------------+--------------
(0 rows)

2. Database, Table and Index

Create a database, a table and an index:

=> CREATE DATABASE arch_vacuum_overview;
CREATE DATABASE
=> \c arch_vacuum_overview
You are now connected to database "arch_vacuum_overview" as user "student".
=> CREATE TABLE t(n numeric);
CREATE TABLE
=> CREATE INDEX t_n on t(n);
CREATE INDEX

Insert rows:

=> INSERT INTO t SELECT random() FROM generate_series(1,100_000);
INSERT 0 100000

3. Changing Rows without Vacuuming

Store a query to calculate the size of the table and the index as a psql variable so that we can use it later:

=> \set SIZE 'SELECT pg_size_pretty(pg_table_size(''t'')) table_size, pg_size_pretty(pg_indexes_size(''t'')) index_size\\g (footer=off)'
=> :SIZE
 table_size | index_size
------------+------------
 4360 kB    | 4504 kB
=> UPDATE t SET n=n WHERE n < 0.5;
UPDATE 50183
=> :SIZE
 table_size | index_size
------------+------------
 6528 kB    | 6696 kB
=> UPDATE t SET n=n WHERE n < 0.5;
UPDATE 50183
=> :SIZE

Page 20: Page 20

 table_size | index_size
------------+------------
 8696 kB    | 7760 kB
=> UPDATE t SET n=n WHERE n < 0.5;
UPDATE 50183
=> :SIZE
 table_size | index_size
------------+------------
 11 MB      | 11 MB

The table and index sizes keep growing.

4. Full Vacuum

=> VACUUM FULL t;
VACUUM
=> :SIZE
 table_size | index_size
------------+------------
 4336 kB    | 3104 kB

The table size is almost back to where it was initially, and the index size decreased (building an index for a large data set is more efficient than adding data to the index row by row).

5. Changing Rows with Vacuuming

=> UPDATE t SET n=n WHERE n < 0.5;
UPDATE 50183
=> VACUUM t;
VACUUM
=> :SIZE
 table_size | index_size
------------+------------
 6536 kB    | 4656 kB
=> UPDATE t SET n=n WHERE n < 0.5;
UPDATE 50183
=> VACUUM t;
VACUUM
=> :SIZE
 table_size | index_size
------------+------------
 6536 kB    | 4656 kB
=> UPDATE t SET n=n WHERE n < 0.5;
UPDATE 50183
=> VACUUM t;
VACUUM
=> :SIZE
 table_size | index_size
------------+------------
 6536 kB    | 4656 kB

The size increased once and then stabilized. The example demonstrates that removing (or changing) large amount of data should be done in multiple transactions, if possible. This will allow autovacuum to clean up unneeded row versions in time, avoiding table bloating.

6. Turn Autovacuum Back On

=> ALTER SYSTEM RESET autovacuum;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Page 21: Page 21

07 Lesson 07: Architecture / Buffer Cache and WAL

Native Markdown source page: Architecture / Buffer Cache and WAL
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_07_arch_wal_overview.html Archive: DBA1-handouts-16-en.zip PDF pages: 17

Page 1: Architecture / Buffer Cache and WAL

© Postgres Professional, 2017–2025 Authors Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Igor Gnatyuk Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo by: Oleg Bartunov (Phu monastery, Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Buffer Cache
  • Replacement Algorithm
  • Write-Ahead Log
  • Checkpoint
  • Processes Related to the Buffer Cache and WAL

Page 3: Buffer Cache

Source slide page 3: Buffer Cache{#source-figure-003 caption=“Source slide page 3: Buffer Cache”}

Buffer array

  • data page (8 KB)
  • additional information
  • “Dirty” buffers
    • asynchronous write
  • Locks in memory
    • for shared access

Speaker notes

The buffer cache is used to smooth out the difference between the RAM and disk speed. It consists of an array of buffers which contain data pages and some additional information (for example, the file name and the position of the page inside this file).

The page size is usually 8 KB; the size can only be changed when building PostgreSQL.

Any work with data pages goes through the buffer cache. If any process is going to work with the page, it first tries to find it in the cache. If the page does not exist, the process requests the operating system to read this page and places it in the buffer cache. (Note that the OS can read the page either from disk or from its own cache.)

After the page is written to the buffer cache, it can be accessed repeatedly without the overhead of operating system calls.

If a process has changed the data in the page, the corresponding buffer becomes “dirty”. The modified page must be written on disk, but for performance reasons, the recording occurs asynchronously and may be delayed.

The buffer cache, like other shared memory structures, is protected by locks to control concurrent access. Although locks are implemented effectively, access to the buffer cache is not nearly as fast as simply accessing RAM. Therefore, in general, the less data a query reads and modifies, the faster it will work.

Page 4: Replacement

Source slide page 4: Replacement{#source-figure-004 caption=“Source slide page 4: Replacement”}

Least Recently Used replacement

  • dirty buffer is
  • written on disk
  • another page is read
  • into the vacant space

Speaker notes

The buffer cache size is usually not so large as to fit the entire database. It is limited by the available RAM. Also, the larger the buffer cache, the greater the overhead. Therefore, when reading the next page, sooner or later the buffer cache has to run out of space. In this case, page replacement happens.

The replacement algorithm selects a page in the cache that has been used less often than others. If the selected buffer is dirty, the page is written on disk first to store the changes made to it. Then, a new page is written into the buffer.

This replacement is called LRU (Least Recently Used). It keeps the most frequently accessed data in the cache. Such “hot” blocks of data are not very common, and this approach helps to significantly reduce the number of requests to OS (and disk operations), provided enough cache memory.

Page 5: The Impact of Buffer Cache on Query Execution

Creating a new database in the cluster and connecting to it (for more details about databases, see the Data Organization module):

=> CREATE DATABASE arch_wal_overview;
CREATE DATABASE
=> \c arch_wal_overview
You are now connected to database "arch_wal_overview" as user "student".

Create a table:

=> CREATE TABLE t(n integer);
CREATE TABLE

Populate it with rows:

=> INSERT INTO t SELECT id FROM generate_series(1,100_000) AS id;
INSERT 0 100000

The shared_buffers parameter indicates the buffer cache size:

=> SHOW shared_buffers;
 shared_buffers
----------------
 128MB
(1 row)

The default value is too low. In the real world, you should increase it immediately after server installation (it will be applied after restart). Restart the server to wipe the cache clean.

=> \q
student$ sudo pg_ctlcluster 16 main restart
student$ psql arch_wal_overview

Now, let’s compare the behaviour of the system as we run a query once, and then the same query again. Query plans is not the topic of this course, but we will peek into them every now and again. The EXPLAIN ANALYZE command used below will execute the query as well as display the execution plan and some extra details:

=> EXPLAIN (analyze, buffers, costs off, timing off)
SELECT * FROM t;
                 QUERY PLAN
--------------------------------------------
 Seq Scan on t (actual rows=100000 loops=1)
   Buffers: shared read=443
 Planning:
   Buffers: shared hit=12 read=8 dirtied=1
 Planning Time: 0.219 ms
 Execution Time: 8.476 ms
(6 rows)

The “Buffers: shared” line shows the buffer utilization. read — the number of buffers where pages had to be read from disk.

=> EXPLAIN (analyze, buffers, costs off, timing off)
SELECT * FROM t;
                 QUERY PLAN
--------------------------------------------
 Seq Scan on t (actual rows=100000 loops=1)
   Buffers: shared hit=443
 Planning Time: 0.028 ms
 Execution Time: 13.547 ms
(4 rows)

hit — the number of buffers where requested pages were found.

Page 6: Page 6

Note

Note Note that on the second query execution, not only the execution time went down, but the planning time too (because system catalog pages are cached as well).

Page 7: Write-ahead Log (WAL)

Problem: when a crash occurs, data from RAM that is not written on disk is lost

  • WAL
    • stream of records of the actions being performed;
    • can be used to redo the steps lost during the crash
    • records are written to disk earlier than the changed data
  • WAL tracks changes to
    • pages in tables, indexes and other objects
    • transaction status (clog)
  • WAL does not track changes to
    • temporary and unlogged tables

Speaker notes

Having a buffer cache (and other buffers in RAM) increases performance at the cost of reliability. When a crash happens, all buffer cache content is lost. If the crash occurs on the OS or hardware level, the content of OS buffers will also be lost (the OS may have its own failsafes for this).

To increase reliability, PostgreSQL uses the Write-ahead log. When performing any operation, the WAL records minimum necessary information about the operation to be able to perform it again. The record must be written into the disk (or another persistent storage) before the data modified by the operation is (that is why it is called Write-ahead log).

WAL files are located in the PGDATA/pg_wal directory.

All objects that are being worked on in RAM have their operations logged. These include tables, indexes and other objects, as well as transaction statuses. Operations with temporary tables (tables which exist only during the scope of a session or a transaction and are only available to the user who has created them) are not logged. You can also set a regular table to be explicitly unlogged. The table will be quicker to work with, but will be wiped on crash.

https://postgrespro.com/docs/postgresql/16/wal-intro

Page 8: Write-Ahead Log

The WAL can be considered as a continuous stream of records. Each record has a unique ID called an LSN (Log Sequence Number). This 64-bit number represents the record’s byte offset from the start of the WAL. The current WAL position can be seen with pg_current_wal_lsn function:

=> SELECT pg_current_wal_lsn();
 pg_current_wal_lsn
--------------------
 0/2378728
(1 row)

The position is displayed as two 32-bit numbers separated by a slash. Let’s save it for future reference. Now let’s perform some operations and see what’s changed.

=> UPDATE t SET n = 100_001 WHERE n = 1;
UPDATE 1
=> SELECT pg_current_wal_lsn();
 pg_current_wal_lsn
--------------------
 0/237B740
(1 row)

It’s not the absolute values we’re interested in, but the distance between them, as it shows the size of generated WAL records in bytes:

=> SELECT '0/237B740'::pg_lsn - '0/2378728'::pg_lsn AS bytes;
 bytes
-------
 12312
(1 row)

The WAL is stored in files in a separate catalog (PGDATA/pg_wal). By default, the files are 16 MB each, but you can change that during cluster initialization. In addition to browsing the files by means of the OS, you can also display them by the following command:

=> SELECT * FROM pg_ls_waldir() ORDER BY name LIMIT 10;
           name           |   size   |      modification
--------------------------+----------+------------------------
 000000010000000000000002 | 16777216 | 2025-11-27 13:48:20+03
 000000010000000000000003 | 16777216 | 2025-11-27 13:48:11+03
(2 rows)

Page 9: Checkpoint

Source slide page 9: Checkpoint{#source-figure-009 caption=“Source slide page 9: Checkpoint”}

Regular flushing of all dirty buffers to disk

  • ensures that all data changes before the checkpoint get to the disk
  • limits the size of the WAL required for recovery
  • Crash recovery
    • starts from the last checkpoint
    • WAL records are replayed one-by-one to restore data

Speaker notes

When PostgreSQL crashes, it enters the recovery mode on the next start. The data on disk at this point is inconsistent. Changes to hot pages were in the buffer cache, and are now lost, while some of the later changes have been flushed to disk already.

To restore consistency, PostgreSQL sequentially reads the WAL records, replaying the changes that did not make it to the disk. This way, the state of all transactions at the time of the crash is restored. Then, any transactions that have not been logged as committed are considered aborted.

However, logging all changes throughout a server’s lifetime and replaying everything from day one after each crash is impractical, if not impossible. Instead, PostgreSQL uses checkpoints. Every now and then, it forces all dirty buffers to disk (including clog buffers, which store transaction statuses).

A checkpoint is the moment in time when the flushing of all data to disk is started. However, you only have a valid checkpoint when the flushing of all such buffers is complete. It ensures that all data changes up to this point are safe in persistent memory.

In production environments with a large buffer cache, a checkpoint can flush many dirty buffers, so the server spreads this flushing over time to smooth out the I/O load.

When a crash occurs, recovery is started from the last completed checkpoint. Consequently, it is sufficient to store WAL files only as far back as the last completed checkpoint goes.

Page 10: Crash Recovery Using WAL

Modified table pages exist in the buffer cache but haven’t been written to disk yet. During normal shutdown, the server performs a checkpoint to flush all dirty pages to disk. However, we’ll simulate a system crash by sending a signal to the postmaster process.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/main/postmaster.pid)

When the server comes back up, it should begin the recovery. Let’s try:

student$ sudo pg_ctlcluster 16 main start
student$ psql arch_wal_overview
=> SELECT min(n), max(n) FROM t;
 min |  max
-----+--------
   2 | 100001
(1 row)

All the changes have been recovered. After performing a checkpoint, PostgreSQL automatically deletes WAL files that are no longer necessary for recovery.

Page 11: Performance

Source slide page 11: Performance{#source-figure-011 caption=“Source slide page 11: Performance”}

Synchronous mode

  • write on commit
  • backend
  • Asynchronous mode
    • background write
    • walwriter

Speaker notes

The WAL approach is faster than working directly with disk without a buffer cache. Firstly, a WAL record is smaller than an entire page of data. Secondly, the WAL is written sequentially (and usually not read until a crash occurs), which is better for basic hard disk drives.

Performance can also be managed via configuration settings. If the records are stored to disk immediately (synchronous mode), this guarantees that the committed transaction will not be lost. But recording to disk is expensive and forces the committing backend process to wait in line. To prevent WAL records from getting “stuck” in the OS cache, PostgreSQL relies on call of the fsync function, which forces the data into persistent storage.

There is also asynchronous mode, which has a background process walwriter constantly flushing WAL records to disk, with a certain delay. It is more efficient at the cost of some reliability, but still ensures consistency after crash recovery.

In fact, both modes work together. WAL records of a long transaction are written asynchronously (to free up WAL buffers). And if a page is getting flushed to disk and the corresponding WAL record is not there yet, it will immediately be flushed in synchronous mode.

Page 12: Main Processes

Source slide page 12: Main Processes{#source-figure-012 caption=“Source slide page 12: Main Processes”}

WAL writer

  • Checkpointer
    • flush all
    • dirty buffers
  • Background writer
    • flush some
    • dirty buffers
  • Backends
    • flush replaced
    • dirty buffer

Speaker notes

Let’s take a step back and look at the processes that maintain the buffer cache and the WAL.

First, there is walwriter. This process writes WAL records to disk in asynchronous mode. In synchronous mode, this job is handled by the backend that commits the transaction.

Second, checkpointer, the checkpoint process. It periodically flushes all dirty buffers to disk.

Third, bgwriter (or background writer). It operates similarly to checkpointer, but it only flushes some of the dirty buffers, specifically, those that are most likely to be replaced soon. It frees up buffer space so that when backend selects a buffer to put a new page in, it does not have to flush the old contents of the buffer to disk itself.

Fourth, there are backends that put data into the buffer cache. Whenever a buffer being replaced is still dirty (despite the efforts of checkpointer and bgwriter), the backend will flush it to disk.

Page 13: WAL Levels

Minimal

  • guarantees crash recovery
  • Replica (default)
    • backup
    • replication: transmit the WAL on another server and replay it there
  • Logical
    • logical replication: information about adding, changing,
    • and deleting table rows

Speaker notes

WAL was developed as a data protection tool to mitigate the risk of data loss due to crashes.

However, the WAL mechanism turned out to have other applications, if it is supplemented with additional information.

The data stored in the WAL is controlled by the wal_level parameter.

● The minimal level is sufficient to recover after a crash, and nothing else.

● The replica level stores additional information that allows WAL to be used for backup and replication. During replication, WAL records are transmitted to another server and applied there, creating an exact copy (replica) of the original server.

● At the logical level, information is added to the WAL that allows decoding “physical” WAL records and forming “logical” records of adding, changing and deleting table rows. This enables logical replication (see corresponding lessons of DEV2 and DBA3 courses for details).

Page 14: Takeaways

Buffer cache increases performance by reducing the number of disk operations

  • WAL ensures reliability
  • WAL size is kept in check by checkpoints
  • WAL has multiple uses:
    • crash recovery
    • backup
    • replication

Page 15: Practice

  1. Using the OS tools, find the processes responsible for the buffer cache and the WAL. 2. Stop PostgreSQL in fast mode; start it again. Check the server message log. 3. Now stop PostgreSQL in immediate mode; start it again. Check the server message log and compare with the previous one.

Speaker notes

  1. To stop in fast mode, use the command
pg_ctlcluster 16 main stop

This makes the server abort all open connections and perform a checkpoint before shutting down, so that all data is flushed to disk and consistent. In this mode, the shutdown may take some time, but on startup the server will be good to go right away.

  1. To stop in immediate mode, use the command
pg_ctlcluster 16 main stop -m immediate --skip-systemctl-redirect

The server will also abort open connections, but will not perform a checkpoint. Data on disk will be inconsistent, like after a crash. In this mode, the server shuts down quickly, but will have to restore data consistency using WAL on startup.

If your PostgreSQL is compiled from source code, the fast stop command will be

pg_ctl stop

and the immediate stop command will be

pg_ctl stop -m immediate

Page 16: 1. Operating System Processes

First, get the postmaster process ID. It is stored in the first line of postmaster.pid. This file is located in the data directory and is created every time the server starts.

student$ sudo cat /var/lib/postgresql/16/main/postmaster.pid
35853
/var/lib/postgresql/16/main
1764241062
5432
/var/run/postgresql
localhost
  1342170     32770
ready

Now, find all the processes spawned by postmaster:

student$ sudo ps -o pid,command --ppid 35853
    PID COMMAND
  35854 postgres: 16/main: checkpointer
  35855 postgres: 16/main: background writer
  35857 postgres: 16/main: walwriter
  35858 postgres: 16/main: autovacuum launcher
  35859 postgres: 16/main: logical replication launcher
  35902 postgres: 16/main: student student [local] idle

The processes that serve the buffer cache and the WAL include: checkpointer background writer walwriter

2. Stop in Fast Mode

To easily separate old messages from new ones, clear the message log before restarting the server. Do not do this on a real production server.

student$ sudo -u postgres truncate -cs0 /var/log/postgresql/postgresql-16-main.log
student$ sudo pg_ctlcluster 16 main stop
student$ sudo pg_ctlcluster 16 main start

Server message log:

student$ cat /var/log/postgresql/postgresql-16-main.log
2025-11-27 13:57:47.123 MSK [35853] LOG:  received fast shutdown request
2025-11-27 13:57:47.139 MSK [35853] LOG:  aborting any active transactions
2025-11-27 13:57:47.139 MSK [35902] student@student FATAL:  terminating connection due to
administrator command
2025-11-27 13:57:47.155 MSK [35853] LOG:  background worker "logical replication
launcher" (PID 35859) exited with exit code 1
2025-11-27 13:57:47.155 MSK [35854] LOG:  shutting down
2025-11-27 13:57:47.175 MSK [35854] LOG:  checkpoint starting: shutdown immediate
2025-11-27 13:57:47.566 MSK [35854] LOG:  checkpoint complete: wrote 5 buffers (0.0%); 0
WAL file(s) added, 0 removed, 0 recycled; write=0.136 s, sync=0.042 s, total=0.411 s;
sync files=4, longest=0.016 s, average=0.011 s; distance=0 kB, estimate=0 kB;
lsn=0/1921C20, redo lsn=0/1921C20
2025-11-27 13:57:47.574 MSK [35853] LOG:  database system is shut down
2025-11-27 13:57:48.079 MSK [36155] LOG:  starting PostgreSQL 16.11 (Ubuntu
16.11-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu
13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
2025-11-27 13:57:48.080 MSK [36155] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2025-11-27 13:57:48.094 MSK [36155] LOG:  listening on Unix socket
"/var/run/postgresql/.s.PGSQL.5432"
2025-11-27 13:57:48.133 MSK [36158] LOG:  database system was shut down at 2025-11-27
13:57:47 MSK
2025-11-27 13:57:48.157 MSK [36155] LOG:  database system is ready to accept connections

3. Stop in Immediate Mode

student$ sudo -u postgres truncate -cs0 /var/log/postgresql/postgresql-16-main.log

Page 17: Page 17

student$ sudo pg_ctlcluster 16 main stop -m immediate --skip-systemctl-redirect
student$ sudo pg_ctlcluster 16 main start

Server message log:

student$ cat /var/log/postgresql/postgresql-16-main.log
2025-11-27 13:57:50.878 MSK [36155] LOG:  received immediate shutdown request
2025-11-27 13:57:50.900 MSK [36155] LOG:  database system is shut down
2025-11-27 13:57:51.432 MSK [36343] LOG:  starting PostgreSQL 16.11 (Ubuntu
16.11-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu
13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
2025-11-27 13:57:51.433 MSK [36343] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2025-11-27 13:57:51.448 MSK [36343] LOG:  listening on Unix socket
"/var/run/postgresql/.s.PGSQL.5432"
2025-11-27 13:57:51.483 MSK [36346] LOG:  database system was interrupted; last known up
at 2025-11-27 13:57:48 MSK
2025-11-27 13:58:01.507 MSK [36346] LOG:  syncing data directory (fsync), elapsed time:
10.00 s, current path: ./base/1/2659
2025-11-27 13:58:11.520 MSK [36346] LOG:  syncing data directory (fsync), elapsed time:
20.01 s, current path: ./base/16385/4158
2025-11-27 13:58:21.512 MSK [36346] LOG:  syncing data directory (fsync), elapsed time:
30.00 s, current path: ./base/5/2612_fsm
2025-11-27 13:58:29.113 MSK [36346] LOG:  database system was not properly shut down;
automatic recovery in progress
2025-11-27 13:58:29.138 MSK [36346] LOG:  invalid record length at 0/1921C98: expected at
least 24, got 0
2025-11-27 13:58:29.138 MSK [36346] LOG:  redo is not required
2025-11-27 13:58:29.216 MSK [36344] LOG:  checkpoint starting: end-of-recovery immediate
wait
2025-11-27 13:58:29.321 MSK [36344] LOG:  checkpoint complete: wrote 3 buffers (0.0%); 0
WAL file(s) added, 0 removed, 0 recycled; write=0.030 s, sync=0.015 s, total=0.124 s;
sync files=2, longest=0.008 s, average=0.008 s; distance=0 kB, estimate=0 kB;
lsn=0/1921C98, redo lsn=0/1921C98
2025-11-27 13:58:29.344 MSK [36343] LOG:  database system is ready to accept connections

Before starting to accept connections, the DBMS performed an automatic recovery.

08 Lesson 08: Data Organization / Databases and Schemas

Native Markdown source page: Data Organization / Databases and Schemas
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_08_data_databases.html Archive: DBA1-handouts-16-en.zip PDF pages: 24

Page 1: Data Organization / Databases and Schemas

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Databases and Templates
  • Schemas and Search Path
  • Special Schemas, Temporary Objects
  • Managing Databases, Schemas, and Objects

Page 3: Database Cluster

Source slide page 3: Database Cluster{#source-figure-003 caption=“Source slide page 3: Database Cluster”}

Cluster initialization creates three databases

  • A new database is always cloned from an existing one

Speaker notes

A PostgreSQL instance manages a database cluster which comprises multiple databases. When a cluster is initialized, three identical databases are created in a specific way.

https://postgrespro.com/docs/postgresql/16/bki

All other databases created by users are cloned from an existing one.

The template1 database is used by default when creating new databases. Any objects and extensions added to the template will be copied into any database created from it.

The template0 must never be modified. It is required in at least two scenarios. Firstly, it is necessary when restoring a database from a backup created via pg_dump (since the copy will include not only the database objects, but also any objects from template1). Secondly, it is used when creating a new database with an encoding different from the one specified during cluster initialization.

The postgres database is used to connect to by default by the postgres user. It is not a hard requirement to have it, but some utilities expect it to be there, so removing it is not a good idea, even if you never use the database directly.

https://postgrespro.com/docs/postgresql/16/manage-ag-templatedbs

Page 4: Databases

The \l[ist] command shows a list of all databases:

=> \l
                                                       List of databases
   Name    |  Owner   | Encoding | Locale Provider |   Collate   |    Ctype    | ICU
Locale | ICU Rules |   Access privileges
-----------+----------+----------+-----------------+-------------+-------------+----------
--+-----------+-----------------------
 postgres  | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           |
 student   | student  | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           |
 template0 | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           | =c/postgres          +
           |          |          |                 |             |             |
  |           | postgres=CTc/postgres
 template1 | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           | =c/postgres          +
           |          |          |                 |             |             |
  |           | postgres=CTc/postgres
(4 rows)

The list includes a fourth database, student, which is not created during cluster initialization. This database was specifically added for course purposes — it allows users to omit the database name when launching psql, as it defaults to a database matching the OS username. You can also view the list of databases in the database itself:

=> SELECT datname, datistemplate, datallowconn, datconnlimit FROM pg_database;
  datname  | datistemplate | datallowconn | datconnlimit
-----------+---------------+--------------+--------------
 postgres  | f             | t            |           -1
 student   | f             | t            |           -1
 template1 | t             | t            |           -1
 template0 | t             | f            |           -1
(4 rows)

datistemplate — whether the database is a template datallowconn — whether database connections are allowed datconnlimit — maximum number of connections (-1 = unlimited)

Creating a Database from a Template

Connect to the template1 database:

=> \c template1
You are now connected to database "template1" as user "student".

Check if we can use the digest function, which calculates the hash code of a text string:

=> SELECT digest('Hello, world!', 'md5');
ERROR:  function digest(unknown, unknown) does not exist
LINE 1: SELECT digest('Hello, world!', 'md5');
               ^
HINT:  No function matches the given name and argument types. You might need to add
explicit type casts.

There is no such function. In fact, the digest function is defined in the pgcrypto extension. Install it:

=> CREATE EXTENSION pgcrypto;
CREATE EXTENSION

Now, we can use the functions provided by the pgcrypto extension. For example, the MD5 digest calculation function:

Page 5: Page 5

=> SELECT digest('Hello, world!', 'md5');
               digest
------------------------------------
 \x6cd3556deb0da54bca060b4c39479839
(1 row)

In order to be used to create a database from, the template must have no active connections. So, disconnect from template1 first.

=> \c student
You are now connected to database "student" as user "student".

New databases are created with the CREATE DATABASE command:

=> CREATE DATABASE db;
CREATE DATABASE
=> \c db
You are now connected to database "db" as user "student".

A new database can also be created from the OS by using the createdb tool.

=> SELECT datname, datistemplate, datallowconn, datconnlimit FROM pg_database;
  datname  | datistemplate | datallowconn | datconnlimit
-----------+---------------+--------------+--------------
 postgres  | f             | t            |           -1
 student   | f             | t            |           -1
 template1 | t             | t            |           -1
 template0 | t             | f            |           -1
 db        | f             | t            |           -1
(5 rows)

Since template1 is the template used by default, the newly created database will have the pgcrypto extension already installed:

=> SELECT digest('Hello, world!', 'md5');
               digest
------------------------------------
 \x6cd3556deb0da54bca060b4c39479839
(1 row)

Database Management

The created database can be renamed (must have no active connections):

=> \c student
You are now connected to database "student" as user "student".
=> ALTER DATABASE db RENAME TO appdb;
ALTER DATABASE
=> SELECT datname, datistemplate, datallowconn, datconnlimit FROM pg_database;
  datname  | datistemplate | datallowconn | datconnlimit
-----------+---------------+--------------+--------------
 postgres  | f             | t            |           -1
 student   | f             | t            |           -1
 template1 | t             | t            |           -1
 template0 | t             | f            |           -1
 appdb     | f             | t            |           -1
(5 rows)

Other parameters can also be changed, such as the maximum number of concurrent connections:

=> ALTER DATABASE appdb CONNECTION LIMIT 10;
ALTER DATABASE
=> SELECT datname, datistemplate, datallowconn, datconnlimit FROM pg_database;

Page 6: Database Size

  datname  | datistemplate | datallowconn | datconnlimit
-----------+---------------+--------------+--------------
 postgres  | f             | t            |           -1
 student   | f             | t            |           -1
 template1 | t             | t            |           -1
 template0 | t             | f            |           -1
 appdb     | f             | t            |           10
(5 rows)

A database size is shown by the following function:

=> SELECT pg_database_size('appdb');
 pg_database_size
------------------
          7729635
(1 row)

The output can be displayed in a more readable form:

=> SELECT pg_size_pretty(pg_database_size('appdb'));
 pg_size_pretty
----------------
 7548 kB
(1 row)

The database has no user objects so far (except for pgcrypto), so this is the size of an “empty” database.

Page 7: Schemas

Database object namespace

  • each object belongs to a schema
  • Purposes
    • dividing objects into logical groups
    • prevent name conflicts between applications
  • Schemas and users are different entities

Speaker notes

Schemas are namespaces for database objects. They separate objects into groups for easier management and serve to prevent name conflicts when multiple users or applications access the same database.

Every object that exists in the database belongs to a schema.

In PostgreSQL, schema and user are different entities (although the default settings allow users to conveniently operate schemas of the same name).

https://postgrespro.com/docs/postgresql/16/ddl-schemas

Page 8: Databases and Schemas

Source slide page 8: Databases and Schemas{#source-figure-008 caption=“Source slide page 8: Databases and Schemas”}

Speaker notes

A cluster comprises multiple databases. Each database contains various schemas across which database objects are distributed.

There are several standard schemas that exist in any database. More schemas can be added by users.

Clients may connect to only one database at a time, but within the database, the client can work with objects in any schema.

Page 9: Schemas

=> \c appdb
You are now connected to database "appdb" as user "student".

The psql command (dn = describe namespace) shows a list of schemas in a database:

=> \dn
      List of schemas
  Name  |       Owner
--------+-------------------
 public | pg_database_owner
(1 row)

Create a new schema:

=> CREATE SCHEMA app;
CREATE SCHEMA
=> \dn
      List of schemas
  Name  |       Owner
--------+-------------------
 app    | student
 public | pg_database_owner
(2 rows)

Now create a table (it will get into the public schema by default):

=> CREATE TABLE t(s text);
CREATE TABLE
=> INSERT INTO t VALUES ('I am table t');
INSERT 0 1

You can get a list of tables with the \dt command:

=> \dt
        List of relations
 Schema | Name | Type  |  Owner
--------+------+-------+---------
 public | t    | table | student
(1 row)

Objects can be moved between schemas. Since this is the logical level, the moving takes place only in the system catalog; physically, the data remains in place.

=> ALTER TABLE t SET SCHEMA app;
ALTER TABLE

Now, the table t can be accessed using its schema name:

=> SELECT * FROM app.t;
      s
--------------
 I am table t
(1 row)

Without the schema name, the table is not found.

=> SELECT * FROM t;
ERROR:  relation "t" does not exist
LINE 1: SELECT * FROM t;
                      ^

So, how do you access objects located in various schemas?

Page 10: Page 10

Page 11: Search Path

Determining an object’s schema

  • a schema is explicitly defined by a qualified name (schema.name)
  • a name without a qualifier is looked up in the schemas specified in the search
  • path
  • Search path
    • defined by the search_path parameter
    • non-existent schemas and schemas with restricted access are excluded;
    • implicit schemas are included
    • the actual value is shown by the current_schemas function
    • the first explicitly specified schema in the path is where objects are created

Speaker notes

When specifying an object, the schema it belongs to must be determined, since different schemas may contain objects of the same name.

When the object name is qualified (with the schema name), the object is looked up in the given schema, as shown on the previous slide. If the name is used without a qualifier, PostgreSQL tries to look the name up in one of the schemas listed in the search path, which is determined by the search_path configuration parameter.

The actual search path may differ from the search_path parameter value: any non-existent schemas are excluded, as well as schemas to which the user does not have access (see the Access Control module for details). In addition, some special schemas are implicitly added to the beginning of the search path.

The actual search path, including implicit schemas, is returned by a call to the current_schemas(true) function. The schemas are searched in the order specified in the search path, from left to right. If the desired object name is not found in the first schema, the next one is searched, and so on.

When an object is created with an unqualified name, it is placed into the first explicitly defined schema in the path.

The concept of search_path is similar to the PATH variable in operating systems.

https://postgrespro.com/docs/postgresql/16/ddl-schemas#DDL-SCHEMAS-P ATH

https://postgrespro.com/docs/postgresql/16/runtime-config-client#GUC-SEA RCH-PATH

Page 12: Special Schemas

Public schema

  • included in the search path by default
  • all objects will belong to this schema, unless configured otherwise
  • Schema named after the user
    • included in the search path by default but is not created automatically
    • if created, the user objects will belong to this schema
  • pg_catalog schema
    • a schema for system catalog objects
    • if not explicitly included in the path, implicitly included as the first one

Speaker notes

There are several special schemas usually present in any database.

The public schema is used by default for storing objects, unless intentionally configured otherwise.

The pg_catalog schema contains system catalog objects. The system catalog is a collection of tables containing metadata about objects belonging to the cluster. information_schema is another schema with an alternative representation of the system catalog (as defined in the SQL standard).

If pg_catalog is not specified in the search path, the schema will be implicitly placed in the front of the path so that system objects remain visible.

Page 13: Search Path

First, let’s find out why the table was created in the public schema. To do that, look at the search path:

=> SHOW search_path;
   search_path
-----------------
 "$user", public
(1 row)

Construction “$user” defines the schema with the same name as the current user (in this case, student). Since there is no such schema, it is ignored, and the table is created in public. To avoid the headache of tracking which schemas exist, which do not, and which are not explicitly declared, use the following function:

=> SELECT current_schemas(true);
   current_schemas
---------------------
 {pg_catalog,public}
(1 row)

When the function is called with a true argument, the result includes system schemas (e.g., pg_catalog). We can define the search path, for example:

=> SET search_path = public, app;
SET

Now, the table t will be found:

=> SELECT * FROM t;
      s
--------------
 I am table t
(1 row)

We have just set a configuration parameter at the session level. If we restart the session, it will have reverted to default. Setting it on the cluster level would not be a good solution either, since this path may not be needed often and not by all users. Thankfully, you can set such parameters for a specific database:

=> ALTER DATABASE appdb SET search_path = public, app;
ALTER DATABASE

Now, it will be applied for all new connections to appdb. Let’s try:

=> \c appdb
You are now connected to database "appdb" as user "student".
=> SHOW search_path;
 search_path
-------------
 public, app
(1 row)
=> SELECT current_schemas(true);
     current_schemas
-------------------------
 {pg_catalog,public,app}
(1 row)

Another function current_schema() returns the first non-system schema name. This identifies the regular schema where new objects will be created by default.

=> SELECT current_schema();

Page 14: Page 14

 current_schema
----------------
 public
(1 row)

Page 15: Special Schemas

Temporary tables

  • exist for the duration of the session or transaction
  • not logged (no recovery after a crash)
  • do not utilize the shared buffer cache
  • pg_temp_N schema
    • created automatically for temporary tables
    • pg_temp: a link to a specific temporary schema for the session
    • if not explicitly included in the path, implicitly included as the very first one
    • at the end of the session, all objects of the temporary schema are dropped;
    • the schema itself remains and is reused for other sessions

Speaker notes

PostgreSQL can work with temporary tables. Temporary tables store data that should only be available to the current session and only for as long as the session is active (or even for the duration of a single transaction within the session).

When recovering after a crash, previous sessions do not exist, so temporary tables contents will be lost. Therefore, such tables are unlogged. Additionally, temporary table pages are never stored in the shared buffer cache, residing in the internal memory of their backend process instead. Thanks to that, temporary tables can be accessed a bit more quickly than regular ones.

Temporary tables are organized using schemas. When a session is started, a temporary schema named pg_temp_N (pg_temp_1, pg_temp_2, etc.) is created for it. It can be accessed by the name pg_temp (without a number, the name will always refer to the temporary schema specific for the session).

If pg_temp is not specified in the path, it is searched before all others. Otherwise, you can specify its position in the path explicitly, like with pg_catalog.

After the session ends, all objects belonging to the temporary schema are dropped, and the schema itself remains to be reused later on.

There are other special schemas, more technical in nature.

Page 16: Temporary Tables and pg_temp

Create a temporary table:

=> CREATE TEMP TABLE t(s text);
CREATE TABLE
=> \dt
         List of relations
  Schema   | Name | Type  |  Owner
-----------+------+-------+---------
 pg_temp_3 | t    | table | student
(1 row)

The table is created in a special schema. Each session has its own temporary schema, so that sessions cannot access each other’s temporary schemas. But, where did the regular table t disappear? The answer lies in the extended search path. Now, the search path includes the temporary schema, and the object in it “overshadows” the object in the app schema by the same name.

=> SELECT current_schemas(true);
          current_schemas
-----------------------------------
 {pg_temp_3,pg_catalog,public,app}
(1 row)
=> INSERT INTO t VALUES ('I am temporary table');
INSERT 0 1

Still, you can access objects in either schema by providing the schema name explicitly. For the temporary table, use the pseudo-schema pg_temp: it will automatically transform to the current session’s pg_temp_N:

=> SELECT * FROM app.t;
      s
--------------
 I am table t
(1 row)
=> SELECT * FROM pg_temp.t;
          s
----------------------
 I am temporary table
(1 row)

Temporary schemas are not limited to just tables.

=> CREATE VIEW v AS SELECT * FROM pg_temp.t;
NOTICE:  view "v" will be a temporary view
CREATE VIEW

Temporary schemas and data in them may have different scopes of life (depending on the clauses ON COMMIT DELETE, PRESERVE, and DROP). In any case, all temporary objects are deleted upon reconnecting to a database:

=> \c appdb
You are now connected to database "appdb" as user "student".
=> SELECT current_schemas(true);
     current_schemas
-------------------------
 {pg_catalog,public,app}
(1 row)
=> SELECT * FROM pg_temp.v;

Page 17: Deleting Objects

ERROR:  relation "pg_temp.v" does not exist
LINE 1: SELECT * FROM pg_temp.v;
                      ^
=> SELECT * FROM pg_temp.t;
ERROR:  relation "pg_temp.t" does not exist
LINE 1: SELECT * FROM pg_temp.t;
                      ^

A schema cannot be deleted as long as any objects remain in it:

=> DROP SCHEMA app;
ERROR:  cannot drop schema app because other objects depend on it
DETAIL:  table t depends on schema app
HINT:  Use DROP ... CASCADE to drop the dependent objects too.

But you can delete a schema together with all its objects:

=> DROP SCHEMA app CASCADE;
NOTICE:  drop cascades to table t
DROP SCHEMA

If the database is no longer needed, it can be deleted as well.

=> \conninfo
You are connected to database "appdb" as user "student" via socket in
"/var/run/postgresql" at port "5432".
=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE appdb;
DROP DATABASE

Page 18: Takeaways

On the logical level

  • cluster contains databases,
  • database contains schemas,
  • schema contains specific objects (tables, indexes etc.)
  • Databases are created by cloning existing ones
  • A schema can be specified explicitly or determined using the search path
  • Some schemas have specific purposes

Page 19: Practice

  1. Create a new database and connect to it. 2. Check the size of the created database. 3. Create two schemas. Name one app, and the other after your user name. Create several tables in both schemas and populate them with data. 4. Check how much the database size has increased. 5. Modify the search path variable value so that when connecting to the database, tables from both schemas are accessible by an unqualified name. The “username” schema should have priority.

Page 20: 1. Database

=> CREATE DATABASE data_databases;
CREATE DATABASE
=> \c data_databases
You are now connected to database "data_databases" as user "student".

2. Database Size

=> SELECT pg_size_pretty(pg_database_size('data_databases'));
 pg_size_pretty
----------------
 7516 kB
(1 row)

Save the value in a psql variable:

=> SELECT pg_database_size('data_databases') AS oldsize \gset

3. Schemas and Tables

=> CREATE SCHEMA app;
CREATE SCHEMA
=> CREATE SCHEMA student;
CREATE SCHEMA

Which schema receives tables created without explicit schema qualification?

=> SELECT current_schema();
 current_schema
----------------
 student
(1 row)

student schema tables:

=> CREATE TABLE a(s text);
CREATE TABLE
=> INSERT INTO a VALUES ('student');
INSERT 0 1
=> CREATE TABLE b(s text);
CREATE TABLE
=> INSERT INTO b VALUES ('student');
INSERT 0 1

app schema tables:

=> CREATE TABLE app.a(s text);
CREATE TABLE
=> INSERT INTO app.a VALUES ('app');
INSERT 0 1
=> CREATE TABLE app.c(s text);
CREATE TABLE
=> INSERT INTO app.c VALUES ('app');
INSERT 0 1

Page 21: 4. Database Size Change

=> SELECT pg_size_pretty(pg_database_size('data_databases'));
 pg_size_pretty
----------------
 7612 kB
(1 row)
=> SELECT pg_database_size('data_databases') AS newsize \gset

The size has changed to:

=> SELECT pg_size_pretty(:newsize::bigint - :oldsize::bigint);
 pg_size_pretty
----------------
 96 kB
(1 row)

5. Search Path

With the current search path configuration, only student schema tables are visible:

=> SELECT * FROM a;
    s
---------
 student
(1 row)
=> SELECT * FROM b;
    s
---------
 student
(1 row)
=> SELECT * FROM c;
ERROR:  relation "c" does not exist
LINE 1: SELECT * FROM c;
                      ^

Modify the search path:

=> ALTER DATABASE data_databases SET search_path = "$user",app,public;
ALTER DATABASE
=> \c
You are now connected to database "data_databases" as user "student".
=> SHOW search_path;
     search_path
----------------------
 "$user", app, public
(1 row)

Now, tables from both schemas are visible, but student takes priority:

=> SELECT * FROM a;
    s
---------
 student
(1 row)
=> SELECT * FROM b;
    s
---------
 student
(1 row)

Page 22: Page 22

=> SELECT * FROM c;
  s
-----
 app
(1 row)

Page 23: Practice+

  1. Create a database. For all sessions connecting to this database, set the temp_buffers parameter to four times the default value.

Speaker notes

  1. Use the command ALTER DATABASE … SET:

https://postgrespro.com/docs/postgresql/16/sql-alterdatabase

More about the temp_buffers parameter:

https://postgrespro.com/docs/postgresql/16/runtime-config-resource#GUC-T EMP-BUFFERS

Page 24: 1. Setting the temp_buffers Parameter

=> CREATE DATABASE data_databases;
CREATE DATABASE
=> \c data_databases
You are now connected to database "data_databases" as user "student".

The temp_buffers parameter determines the amount of memory allocated for the local cache for temporary tables during any session. If the temporary tables data does not fit into temp_buffers, the pages are evicted, like with the regular buffer cache. Too low of a value may affect the server performance if temporary tables are used frequently. The default temp_buffers value is 8MB:

=> SELECT name, setting, unit, boot_val, reset_val
FROM pg_settings
WHERE name = 'temp_buffers' \gx
-[ RECORD 1 ]-----------
name      | temp_buffers
setting   | 1024
unit      | 8kB
boot_val  | 1024
reset_val | 1024

Set the value to 32MB for all new sessions:

=> ALTER DATABASE data_databases SET temp_buffers = '32MB';
ALTER DATABASE
=> \c
You are now connected to database "data_databases" as user "student".
=> SHOW temp_buffers;
 temp_buffers
--------------
 32MB
(1 row)

The parameter changes made by ALTER DATABASE are stored in the pg_db_role_setting table. You can view them in psql with the following command:

=> \drds
             List of settings
 Role |    Database    |     Settings
------+----------------+-------------------
      | data_databases | temp_buffers=32MB
(1 row)

Naturally, you do not have to set the temp_buffers parameter just at the database level. For example, you can set them for the whole cluster if you define it in postgresql.conf.

09 Lesson 09: Data Organization / System Catalog

Native Markdown source page: Data Organization / System Catalog
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_09_data_catalog.html Archive: DBA1-handouts-16-en.zip PDF pages: 18

Page 1: Data Organization / System Catalog

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • What Is the System Catalog and how to Access It
  • System Catalog Objects and their Location
  • Object Naming Rules
  • Special Data Types

Page 3: System Catalog

A set of tables and views describing all objects in a database cluster

  • Schemas
    • primary schema: pg_catalog
    • alternative representation: information_schema (SQL standard)
  • SQL access
    • view: SELECT
    • change: CREATE, ALTER, DROP
  • psql access
    • commands for convenient data visualization

Speaker notes

The system catalog is a collection of tables and views that describe all database objects. It is metadata for the contents of the cluster: https://postgrespro.com/docs/postgresql/16/catalogs

Starting from version 14 of PostgreSQL, primary keys and unique constraints have been added for most system catalog tables.

You can access this metadata using regular SQL queries. SELECT commands can give you a description of an object, and DDL (Data Definition Language) commands let you add and modify objects.

All system catalog tables and views are located in the pg_catalog schema. There is another schema, as defined by the SQL standard: information_schema. It is more stable and portable than pg_catalog, but does not reflect specific features of PostgreSQL.

Client programs can read the contents of the system catalog and display it to the user in a convenient way. For example, GUI-based development and management environments usually come with a hierarchical object navigation tool.

The psql client also offers a number of convenient built-in commands specifically designed for working with the system catalog. Most of these commands start with \d (as in “describe”). For the full list of commands and their descriptions, see: https://postgrespro.com/docs/postgresql/16/app-psql#APP-PSQL-META-CO MMANDS

We will look at the most commonly used ones in the demo. The course materials also include the catalogs.pdf file that features a diagram of the main system catalog tables and related psql commands.

Page 4: Cluster-Level Objects

Source slide page 4: Cluster-Level Objects{#source-figure-004 caption=“Source slide page 4: Cluster-Level Objects”}

Speaker notes

In a database cluster, each database has its own set of system catalog tables. However, there are several system catalog objects that are shared between all cluster databases. The most obvious example is the list of the databases themselves.

These tables are stored outside of any single database, but at the same time they are accessible from any database within the cluster.

Page 5: Naming Rules

Object (table, view) and column name prefixes

pg_database.datname
  • column prefix
  • (usually derived from
  • common prefix
  • for all objects
  • the object name)
  • Object names are always lowercase

Speaker notes

All system catalog tables and views begin with the prefix pg_. In order to avoid potential conflicts, it is not recommended to create your own objects starting with pg_.

Column names have a three-letter prefix, which is usually derived from the name of the table. There is no underscore after the prefix. There are some exceptions to this rule, such as the oid column and others.

Object names are always stored in lowercase.

Page 6: Specific pg_catalog Objects

Create a database and some test objects:

=> CREATE DATABASE data_catalog;
CREATE DATABASE
=> \c data_catalog
You are now connected to database "data_catalog" as user "student".
=> CREATE TABLE employees(
  id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name text,
  manager integer
);
CREATE TABLE
=> CREATE VIEW top_managers AS
  SELECT * FROM employees WHERE manager IS NULL;
CREATE VIEW
=> CREATE TEMP TABLE emp_salaries(
  employee integer,
  salary numeric
);
CREATE TABLE

We are familiar with some of the system catalog tables from the previous lesson. This is databases:

=> SELECT * FROM pg_database WHERE datname = 'data_catalog' \gx
-[ RECORD 1 ]--+-------------
oid            | 16386
datname        | data_catalog
datdba         | 16384
encoding       | 6
datlocprovider | c
datistemplate  | f
datallowconn   | t
datconnlimit   | -1
datfrozenxid   | 722
datminmxid     | 1
dattablespace  | 1663
datcollate     | en_US.UTF-8
datctype       | en_US.UTF-8
daticulocale   |
daticurules    |
datcollversion | 2.39
datacl         |

And schemas:

=> SELECT * FROM pg_namespace WHERE nspname = 'public' \gx
-[ RECORD 1 ]-----------------------------------------------------------
oid      | 2200
nspname  | public
nspowner | 6171
nspacl   | {pg_database_owner=UC/pg_database_owner,=U/pg_database_owner}

pg_class is an important table that stores descriptions for multiple types of objects: tables, views, indexes, sequences. All these objects in PostgreSQL are called relations, thus the prefix “rel” in the column names:

=> SELECT relname, relkind, relnamespace, relfilenode, relowner, relpersistence
FROM pg_class WHERE relname ~ '^(emp|top)';

Page 7: Page 7

     relname      | relkind | relnamespace | relfilenode | relowner | relpersistence
------------------+---------+--------------+-------------+----------+----------------
 employees_id_seq | S       |         2200 |       16387 |    16384 | p
 employees        | r       |         2200 |       16388 |    16384 | p
 employees_pkey   | i       |         2200 |       16393 |    16384 | p
 top_managers     | v       |         2200 |           0 |    16384 | p
 emp_salaries     | r       |        16399 |       16401 |    16384 | t
(5 rows)

The object type is determined by the relkind column, while relpersistence distinguishes temporary objects from permanent ones. When actively using temporary objects, the system catalog tables accumulate numerous obsolete row versions, which may degrade query performance at all query processing stages. In such cases, timely catalog table vacuuming becomes essential. Naturally, only a subset of columns in pg_class are relevant for each object type. Moreover, examining object names (rather than identifiers like relnamespace, relowner, etc.) is more practical. For this purpose, there are specialized system views such as:

=> SELECT schemaname, tablename, tableowner
FROM pg_tables WHERE schemaname ~ '(public|pg_temp.+)';
 schemaname |  tablename   | tableowner
------------+--------------+------------
 public     | employees    | student
 pg_temp_4  | emp_salaries | student
(2 rows)
=> SELECT *
FROM pg_views WHERE schemaname = 'public';
 schemaname |   viewname   | viewowner |         definition
------------+--------------+-----------+----------------------------
 public     | top_managers | student   |  SELECT id,               +
            |              |           |     name,                 +
            |              |           |     manager               +
            |              |           |    FROM employees         +
            |              |           |   WHERE (manager IS NULL);
(1 row)

Using psql

psql has a set of built-in commands for obtaining information from the system catalog. These short commands are more convenient than making direct queries to system tables and views. A list of all tables is obtained with the command:

=> \dt
             List of relations
  Schema   |     Name     | Type  |  Owner
-----------+--------------+-------+---------
 pg_temp_4 | emp_salaries | table | student
 public    | employees    | table | student
(2 rows)

This command returns a list of all views in the public schema:

=> \dv public.*
           List of relations
 Schema |     Name     | Type |  Owner
--------+--------------+------+---------
 public | top_managers | view | student
(1 row)

List of tables, views, indexes, and sequences:

=> \dtvis

Page 8: Page 8

                       List of relations
  Schema   |       Name       |   Type   |  Owner  |   Table
-----------+------------------+----------+---------+-----------
 pg_temp_4 | emp_salaries     | table    | student |
 public    | employees        | table    | student |
 public    | employees_id_seq | sequence | student |
 public    | employees_pkey   | index    | student | employees
 public    | top_managers     | view     | student |
(5 rows)

Appended with the + modifier, these commands will return more detailed data:

=> \dt+
                                          List of relations
  Schema   |     Name     | Type  |  Owner  | Persistence | Access method |    Size    |
Description
-----------+--------------+-------+---------+-------------+---------------+------------+--
-----------
 pg_temp_4 | emp_salaries | table | student | temporary   | heap          | 8192 bytes |
 public    | employees    | table | student | permanent   | heap          | 8192 bytes |
(2 rows)

To get detailed information about a specific object, use the \d command (without any additional letters):

=> \d top_managers
             View "public.top_managers"
 Column  |  Type   | Collation | Nullable | Default
---------+---------+-----------+----------+---------
 id      | integer |           |          |
 name    | text    |           |          |
 manager | integer |           |          |

The + modifier still works:

=> \d+ top_managers
                         View "public.top_managers"
 Column  |  Type   | Collation | Nullable | Default | Storage  | Description
---------+---------+-----------+----------+---------+----------+-------------
 id      | integer |           |          |         | plain    |
 name    | text    |           |          |         | extended |
 manager | integer |           |          |         | plain    |
View definition:
 SELECT id,
    name,
    manager
   FROM employees
  WHERE manager IS NULL;

You can use the command not only on relations, but other objects as well, such as schemas (\dn) and functions (\df). The S modifier makes the command display system objects in addition to user ones. You can use wildcard patterns to filter the output:

=> \dfS pg*size
                                  List of functions
   Schema   |          Name          | Result data type | Argument data types | Type
------------+------------------------+------------------+---------------------+------
 pg_catalog | pg_column_size         | integer          | "any"               | func
 pg_catalog | pg_database_size       | bigint           | name                | func
 pg_catalog | pg_database_size       | bigint           | oid                 | func
 pg_catalog | pg_indexes_size        | bigint           | regclass            | func
 pg_catalog | pg_relation_size       | bigint           | regclass            | func
 pg_catalog | pg_relation_size       | bigint           | regclass, text      | func
 pg_catalog | pg_table_size          | bigint           | regclass            | func
 pg_catalog | pg_tablespace_size     | bigint           | name                | func
 pg_catalog | pg_tablespace_size     | bigint           | oid                 | func
 pg_catalog | pg_total_relation_size | bigint           | regclass            | func
(10 rows)

Usually, such psql commands have mnemonic names. For example, \df is describe function, \sf is show function:

Page 9: Exploring the System Catalog Structure

=> \sf pg_catalog.pg_database_size(oid)
CREATE OR REPLACE FUNCTION pg_catalog.pg_database_size(oid)
 RETURNS bigint
 LANGUAGE internal
 PARALLEL SAFE STRICT
AS $function$pg_database_size_oid$function$

You can get the full list of commands from the documentation or with the psql \? command. All psql commands that describe objects query system catalog tables. To view these queries, set the psql variable ECHO_HIDDEN. For example, to examine the employees table structure:

=> \set ECHO_HIDDEN on
=> \dt employees
********* QUERY **********
SELECT n.nspname as "Schema",
  c.relname as "Name",
  CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized
view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 't' THEN 'TOAST table' WHEN 'f'
THEN 'foreign table' WHEN 'p' THEN 'partitioned table' WHEN 'I' THEN 'partitioned index'
END as "Type",
  pg_catalog.pg_get_userbyid(c.relowner) as "Owner"
FROM pg_catalog.pg_class c
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
     LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam
WHERE c.relkind IN ('r','p','t','s','')
  AND c.relname OPERATOR(pg_catalog.~) '^(employees)$' COLLATE pg_catalog.default
  AND pg_catalog.pg_table_is_visible(c.oid)
ORDER BY 1,2;
**************************
          List of relations
 Schema |   Name    | Type  |  Owner
--------+-----------+-------+---------
 public | employees | table | student
(1 row)
=> \unset ECHO_HIDDEN

Most system catalog tables feature primary keys (typically the oid column) and uniqueness constraints. As an example, the pg_attribute table (containing relation attribute information) implements these constraints:

=> \d pg_attribute

Page 10: Page 10

               Table "pg_catalog.pg_attribute"
     Column     |   Type    | Collation | Nullable | Default
----------------+-----------+-----------+----------+---------
 attrelid       | oid       |           | not null |
 attname        | name      |           | not null |
 atttypid       | oid       |           | not null |
 attlen         | smallint  |           | not null |
 attnum         | smallint  |           | not null |
 attcacheoff    | integer   |           | not null |
 atttypmod      | integer   |           | not null |
 attndims       | smallint  |           | not null |
 attbyval       | boolean   |           | not null |
 attalign       | "char"    |           | not null |
 attstorage     | "char"    |           | not null |
 attcompression | "char"    |           | not null |
 attnotnull     | boolean   |           | not null |
 atthasdef      | boolean   |           | not null |
 atthasmissing  | boolean   |           | not null |
 attidentity    | "char"    |           | not null |
 attgenerated   | "char"    |           | not null |
 attisdropped   | boolean   |           | not null |
 attislocal     | boolean   |           | not null |
 attinhcount    | smallint  |           | not null |
 attstattarget  | smallint  |           | not null |
 attcollation   | oid       |           | not null |
 attacl         | aclitem[] |           |          |
 attoptions     | text[]    | C         |          |
 attfdwoptions  | text[]    | C         |          |
 attmissingval  | anyarray  |           |          |
Indexes:
    "pg_attribute_relid_attnum_index" PRIMARY KEY, btree (attrelid, attnum)
    "pg_attribute_relid_attnam_index" UNIQUE CONSTRAINT, btree (attrelid, attname)

Referential integrity is maintained through foreign-key-like constraints with additional complexity: the referencing column may be an array of elements, zero may represent undefined references. The pg_get_catalog_foreign_keys() function lists these pseudo-foreign keys. For instance, pg_attribute references:

=> SELECT *
FROM   pg_get_catalog_foreign_keys()
WHERE  fktable = 'pg_attribute'::regclass;
   fktable    |     fkcols     |   pktable    | pkcols | is_array | is_opt
--------------+----------------+--------------+--------+----------+--------
 pg_attribute | {attrelid}     | pg_class     | {oid}  | f        | f
 pg_attribute | {atttypid}     | pg_type      | {oid}  | f        | t
 pg_attribute | {attcollation} | pg_collation | {oid}  | f        | t
(3 rows)

fktable, fkcols — referencing table and its columns pktable, pkcols — key referenced is_array — whether the referencing column is an array is_opt — whether the referencing column can contain 0

Page 11: Special Data Types

oid type – object identifier

  • oid column ensures object uniqueness in system catalog tables
  • integer with an auto increment
  • reg* types
    • oid aliases for some system catalog tables
    • (regclass for pg_class, etc.)
    • converting the text name of an object to the oid type and vice versa

Speaker notes

Most system catalog tables use a column with oid name and data type of the same name as a primary key.

The oid (Object Identifier) type is a 32 bit integer (about 4 billion possible values) with an auto increment.

There are several special data types (in fact, oid aliases) starting with reg that are used to convert object names to oids and back.

https://postgrespro.com/docs/postgresql/16/datatype-oid

Page 12: oid and reg* Data Types

As shown before, table and view descriptions are stored in pg_class table, and column descriptions are in a separate pg_attribute table. So, to get a list of columns in a specific table, you need to join pg_class and pg_attribute:

=> SELECT a.attname, a.atttypid
FROM pg_attribute a
WHERE a.attrelid = (
  SELECT oid FROM pg_class WHERE relname = 'employees'
)
AND a.attnum > 0;
 attname | atttypid
---------+----------
 id      |       23
 name    |       25
 manager |       23
(3 rows)

Using reg* types, the query can be simplified by omitting the explicit access to pg_class:

=> SELECT a.attname, a.atttypid
FROM pg_attribute a
WHERE a.attrelid = 'employees'::regclass
AND a.attnum > 0;
 attname | atttypid
---------+----------
 id      |       23
 name    |       25
 manager |       23
(3 rows)

Here, the string “employees” was converted to the oid type. Similarly, oid can be displayed as a text value:

=> SELECT a.attname, a.atttypid::regtype
FROM pg_attribute a
WHERE a.attrelid = 'employees'::regclass
AND a.attnum > 0;
 attname | atttypid
---------+----------
 id      | integer
 name    | text
 manager | integer
(3 rows)

A list of all reg* types:

=> \dT reg*
                        List of data types
   Schema   |     Name      |             Description
------------+---------------+--------------------------------------
 pg_catalog | regclass      | registered class
 pg_catalog | regcollation  | registered collation
 pg_catalog | regconfig     | registered text search configuration
 pg_catalog | regdictionary | registered text search dictionary
 pg_catalog | regnamespace  | registered namespace
 pg_catalog | regoper       | registered operator
 pg_catalog | regoperator   | registered operator (with args)
 pg_catalog | regproc       | registered procedure
 pg_catalog | regprocedure  | registered procedure (with args)
 pg_catalog | regrole       | registered role
 pg_catalog | regtype       | registered type
(11 rows)

Page 13: Takeaways

The system catalog contains cluster metadata stored within the cluster itself

  • SQL access and additional psql commands
  • Some system catalog tables are stored in databases, some are shared across the entire cluster
  • The system catalog uses special data types

Page 14: Practice

  1. Get a description of the pg_class table.
    1. Get a detailed description of the pg_tables view.
    1. Create a database and a temporary table in it. Get a complete list of schemas in the database, including system schemas.
    1. Get a list of views in the information_schema.
    1. What queries does the following psql command perform? \d+ pg_views

Page 15: 1. Description of pg_class

=> \d pg_class
                     Table "pg_catalog.pg_class"
       Column        |     Type     | Collation | Nullable | Default
---------------------+--------------+-----------+----------+---------
 oid                 | oid          |           | not null |
 relname             | name         |           | not null |
 relnamespace        | oid          |           | not null |
 reltype             | oid          |           | not null |
 reloftype           | oid          |           | not null |
 relowner            | oid          |           | not null |
 relam               | oid          |           | not null |
 relfilenode         | oid          |           | not null |
 reltablespace       | oid          |           | not null |
 relpages            | integer      |           | not null |
 reltuples           | real         |           | not null |
 relallvisible       | integer      |           | not null |
 reltoastrelid       | oid          |           | not null |
 relhasindex         | boolean      |           | not null |
 relisshared         | boolean      |           | not null |
 relpersistence      | "char"       |           | not null |
 relkind             | "char"       |           | not null |
 relnatts            | smallint     |           | not null |
 relchecks           | smallint     |           | not null |
 relhasrules         | boolean      |           | not null |
 relhastriggers      | boolean      |           | not null |
 relhassubclass      | boolean      |           | not null |
 relrowsecurity      | boolean      |           | not null |
 relforcerowsecurity | boolean      |           | not null |
 relispopulated      | boolean      |           | not null |
 relreplident        | "char"       |           | not null |
 relispartition      | boolean      |           | not null |
 relrewrite          | oid          |           | not null |
 relfrozenxid        | xid          |           | not null |
 relminmxid          | xid          |           | not null |
 relacl              | aclitem[]    |           |          |
 reloptions          | text[]       | C         |          |
 relpartbound        | pg_node_tree | C         |          |
Indexes:
    "pg_class_oid_index" PRIMARY KEY, btree (oid)
    "pg_class_relname_nsp_index" UNIQUE CONSTRAINT, btree (relname, relnamespace)
    "pg_class_tblspc_relfilenode_index" btree (reltablespace, relfilenode)

2. Detailed Description of pg\_tables

=> \d+ pg_tables
                          View "pg_catalog.pg_tables"
   Column    |  Type   | Collation | Nullable | Default | Storage | Description
-------------+---------+-----------+----------+---------+---------+-------------
 schemaname  | name    |           |          |         | plain   |
 tablename   | name    |           |          |         | plain   |
 tableowner  | name    |           |          |         | plain   |
 tablespace  | name    |           |          |         | plain   |
 hasindexes  | boolean |           |          |         | plain   |
 hasrules    | boolean |           |          |         | plain   |
 hastriggers | boolean |           |          |         | plain   |
 rowsecurity | boolean |           |          |         | plain   |
View definition:
 SELECT n.nspname AS schemaname,
    c.relname AS tablename,
    pg_get_userbyid(c.relowner) AS tableowner,
    t.spcname AS tablespace,
    c.relhasindex AS hasindexes,
    c.relhasrules AS hasrules,
    c.relhastriggers AS hastriggers,
    c.relrowsecurity AS rowsecurity
   FROM pg_class c
     LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
     LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
  WHERE c.relkind = ANY (ARRAY['r'::"char", 'p'::"char"]);

Page 16: 3. List of All Schemas

=> CREATE DATABASE data_catalog;
CREATE DATABASE
=> \c data_catalog
You are now connected to database "data_catalog" as user "student".
=> CREATE TEMP TABLE t(n integer);
CREATE TABLE
=> \dnS
            List of schemas
        Name        |       Owner
--------------------+-------------------
 information_schema | postgres
 pg_catalog         | postgres
 pg_temp_4          | postgres
 pg_toast           | postgres
 pg_toast_temp_4    | postgres
 public             | pg_database_owner
(6 rows)

Temporary tables are stored in schemas named pg_temp_N, where N is a number. Such schemas are created for each session where temporary objects appear, so there can be multiple schemas. To get the name of the schema for the current session, use the following system function:

=> SELECT pg_my_temp_schema()::regnamespace;
 pg_my_temp_schema
-------------------
 pg_temp_4
(1 row)

In general, the exact name of the schema is not required: you can access temporary objects in your session by using just pg_temp:

=> SELECT * FROM pg_temp.t;
 n
---
(0 rows)

We already know what some of the schemas are there for, and we will learn more about the rest (pg_toast*) in a later lesson.

4. Get a List of Views in the information\_schema

Use the template:

=> \dv information_schema.*

Page 17: Page 17

                              List of relations
       Schema       |                 Name                  | Type |  Owner
--------------------+---------------------------------------+------+----------
 information_schema | _pg_foreign_data_wrappers             | view | postgres
 information_schema | _pg_foreign_servers                   | view | postgres
 information_schema | _pg_foreign_table_columns             | view | postgres
 information_schema | _pg_foreign_tables                    | view | postgres
 information_schema | _pg_user_mappings                     | view | postgres
 information_schema | administrable_role_authorizations     | view | postgres
 information_schema | applicable_roles                      | view | postgres
 information_schema | attributes                            | view | postgres
 information_schema | character_sets                        | view | postgres
 information_schema | check_constraint_routine_usage        | view | postgres
 information_schema | check_constraints                     | view | postgres
 information_schema | collation_character_set_applicability | view | postgres
 information_schema | collations                            | view | postgres
 information_schema | column_column_usage                   | view | postgres
 information_schema | column_domain_usage                   | view | postgres
 information_schema | column_options                        | view | postgres
 information_schema | column_privileges                     | view | postgres
 information_schema | column_udt_usage                      | view | postgres
 information_schema | columns                               | view | postgres
 information_schema | constraint_column_usage               | view | postgres
 information_schema | constraint_table_usage                | view | postgres
 information_schema | data_type_privileges                  | view | postgres
 information_schema | domain_constraints                    | view | postgres
 information_schema | domain_udt_usage                      | view | postgres
 information_schema | domains                               | view | postgres
 information_schema | element_types                         | view | postgres
 information_schema | enabled_roles                         | view | postgres
 information_schema | foreign_data_wrapper_options          | view | postgres
 information_schema | foreign_data_wrappers                 | view | postgres
 information_schema | foreign_server_options                | view | postgres
 information_schema | foreign_servers                       | view | postgres
 information_schema | foreign_table_options                 | view | postgres
 information_schema | foreign_tables                        | view | postgres
 information_schema | information_schema_catalog_name       | view | postgres
 information_schema | key_column_usage                      | view | postgres
 information_schema | parameters                            | view | postgres
 information_schema | referential_constraints               | view | postgres
 information_schema | role_column_grants                    | view | postgres
 information_schema | role_routine_grants                   | view | postgres
 information_schema | role_table_grants                     | view | postgres
 information_schema | role_udt_grants                       | view | postgres
 information_schema | role_usage_grants                     | view | postgres
 information_schema | routine_column_usage                  | view | postgres
 information_schema | routine_privileges                    | view | postgres
 information_schema | routine_routine_usage                 | view | postgres
 information_schema | routine_sequence_usage                | view | postgres
 information_schema | routine_table_usage                   | view | postgres
 information_schema | routines                              | view | postgres
 information_schema | schemata                              | view | postgres
 information_schema | sequences                             | view | postgres
 information_schema | table_constraints                     | view | postgres
 information_schema | table_privileges                      | view | postgres
 information_schema | tables                                | view | postgres
 information_schema | transforms                            | view | postgres
 information_schema | triggered_update_columns              | view | postgres
 information_schema | triggers                              | view | postgres
 information_schema | udt_privileges                        | view | postgres
 information_schema | usage_privileges                      | view | postgres
 information_schema | user_defined_types                    | view | postgres
 information_schema | user_mapping_options                  | view | postgres
 information_schema | user_mappings                         | view | postgres
 information_schema | view_column_usage                     | view | postgres
 information_schema | view_routine_usage                    | view | postgres
 information_schema | view_table_usage                      | view | postgres
 information_schema | views                                 | view | postgres
(65 rows)

5. Queries to the System Catalog

To see the queries psql runs, use the ECHO_HIDDEN variable:

=> \set ECHO_HIDDEN on
=> \d+ pg_views

Page 18: Page 18

********* QUERY **********
SELECT c.oid,
  n.nspname,
  c.relname
FROM pg_catalog.pg_class c
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname OPERATOR(pg_catalog.~) '^(pg_views)$' COLLATE pg_catalog.default
  AND pg_catalog.pg_table_is_visible(c.oid)
ORDER BY 2, 3;
**************************
********* QUERY **********
SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, c.relhastriggers,
c.relrowsecurity, c.relforcerowsecurity, false AS relhasoids, c.relispartition,
pg_catalog.array_to_string(c.reloptions || array(select 'toast.' || x from
pg_catalog.unnest(tc.reloptions) x), ', ')
, c.reltablespace, CASE WHEN c.reloftype = 0 THEN '' ELSE
c.reloftype::pg_catalog.regtype::pg_catalog.text END, c.relpersistence, c.relreplident,
am.amname
FROM pg_catalog.pg_class c
 LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)
LEFT JOIN pg_catalog.pg_am am ON (c.relam = am.oid)
WHERE c.oid = '12028';
**************************
********* QUERY **********
SELECT a.attname,
  pg_catalog.format_type(a.atttypid, a.atttypmod),
  (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid, true)
   FROM pg_catalog.pg_attrdef d
   WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef),
  a.attnotnull,
  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t
   WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <>
t.typcollation) AS attcollation,
  a.attidentity,
  a.attgenerated,
  a.attstorage,
  pg_catalog.col_description(a.attrelid, a.attnum)
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = '12028' AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum;
**************************
********* QUERY **********
SELECT pg_catalog.pg_get_viewdef('12028'::pg_catalog.oid, true);
**************************
********* QUERY **********
SELECT r.rulename, trim(trailing ';' from pg_catalog.pg_get_ruledef(r.oid, true))
FROM pg_catalog.pg_rewrite r
WHERE r.ev_class = '12028' AND r.rulename != '_RETURN' ORDER BY 1;
**************************
                         View "pg_catalog.pg_views"
   Column   | Type | Collation | Nullable | Default | Storage  | Description
------------+------+-----------+----------+---------+----------+-------------
 schemaname | name |           |          |         | plain    |
 viewname   | name |           |          |         | plain    |
 viewowner  | name |           |          |         | plain    |
 definition | text |           |          |         | extended |
View definition:
 SELECT n.nspname AS schemaname,
    c.relname AS viewname,
    pg_get_userbyid(c.relowner) AS viewowner,
    pg_get_viewdef(c.oid) AS definition
   FROM pg_class c
     LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
  WHERE c.relkind = 'v'::"char";

psql ran five queries to display this result.

=> \set ECHO_HIDDEN off

10 Lesson 10: Data Organization / Tablespaces

Native Markdown source page: Data Organization / Tablespaces
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_10_data_tablespaces.html Archive: DBA1-handouts-16-en.zip PDF pages: 14

Page 1: Data Organization / Tablespaces

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Tablespaces and Directories
  • Creating, Modifying, and Deleting Tablespaces
  • Storing Data in the File Hierarchy
  • Moving Data

Page 3: Tablespaces

Source slide page 3: Tablespaces{#source-figure-003 caption=“Source slide page 3: Tablespaces”}

Speaker notes

Tablespaces are used to organize the physical storage of data and determine the location of data in the file hierarchy.

For example, one tablespace can use slow disks for archived data, and another one can use fast disks with frequent activity.

On cluster initialization, two tablespaces are created: pg_default and pg_global.

A tablespace can be used by multiple databases, and a database can use multiple tablespaces at once.

Each database has a default tablespace where all database objects are created (unless specified otherwise). System catalog objects are also stored in the default tablespace. Databases will use the pg_default tablespace as their default, unless another one is set by the user.

The pg_global tablespace is special as it stores only those objects that are shared across the whole cluster.

https://postgrespro.com/docs/postgresql/16/manage-ag-tablespaces

Page 4: Directories

Source slide page 4: Directories{#source-figure-004 caption=“Source slide page 4: Directories”}

PGDATA/global/
PGDATA/base/dboid/
PGDATA/pg_tblspc/tsoid
/path-to-catalog/ver/dboid/

Speaker notes

Essentially, a tablespace is a reference to the directory in which the data is located. The standard tablespaces pg_global and pg_default are always located in PGDATA/global/ and PGDATA/base/, respectively. When a custom tablespace is created, an arbitrary catalog can be specified. For its convenience, PostgreSQL also creates a symbolic link to the directory in PGDATA/pg_tblspc/.

The PGDATA/base/ directory comprises different directories for each database (unlike PGDATA/global/, which stores data referring to the whole cluster).

Inside a custom tablespace directory, there is another level for different PostgreSQL server versions. This is helpful during server upgrade.

Actual objects are stored in files in these directories, one or more files per object.

Page 5: System Tablespaces

Upon a cluster initialization, two tablespaces are created:

=> SELECT * FROM pg_tablespace;
 oid  |  spcname   | spcowner | spcacl | spcoptions
------+------------+----------+--------+------------
 1663 | pg_default |       10 |        |
 1664 | pg_global  |       10 |        |
(2 rows)

pg_global — shared cluster objects pg_default — default tablespace

Custom Tablespaces

A new tablespace needs an empty directory owned by the user postgres.

student$ sudo -u postgres mkdir /var/lib/postgresql/ts_dir

Now, a new tablespace can be created:

=> CREATE TABLESPACE ts LOCATION '/var/lib/postgresql/ts_dir';
CREATE TABLESPACE

The following psql command returns a list of all tablespaces:

=> \db
                List of tablespaces
    Name    |  Owner   |          Location
------------+----------+----------------------------
 pg_default | postgres |
 pg_global  | postgres |
 ts         | student  | /var/lib/postgresql/ts_dir
(3 rows)

Each database has a “default” tablespace. Let’s create a database and assign ts as its default:

=> CREATE DATABASE appdb TABLESPACE ts;
CREATE DATABASE

This makes all tables and indexes created within the database fall into ts, unless specified otherwise. Connect to the database:

=> \c appdb
You are now connected to database "appdb" as user "student".

Create a table:

=> CREATE TABLE t1(
  id integer GENERATED ALWAYS AS IDENTITY,
  name text
);
CREATE TABLE

When creating objects, you may explicitly specify a tablespace for it:

=> CREATE TABLE t2(
  n numeric
) TABLESPACE pg_default;
CREATE TABLE
=> SELECT tablename, tablespace FROM pg_tables WHERE schemaname = 'public';

Page 6: Page 6

 tablename | tablespace
-----------+------------
 t1        |
 t2        | pg_default
(2 rows)

An empty tablespace field means that the default tablespace is used. The second table has this field filled in. You can also specify a tablespace for an index:

=> CREATE INDEX ON t1(id) TABLESPACE pg_default;
CREATE INDEX
=> SELECT indexname,tablespace FROM pg_indexes WHERE tablename='t1';
 indexname | tablespace
-----------+------------
 t1_id_idx | pg_default
(1 row)

Another way to assign a tablespace without defining it at object creation is to preemptively set a value of default_tablespace parameter. A single tablespace may contain objects from multiple databases.

=> CREATE DATABASE configdb;
CREATE DATABASE

This database’s default tablespace will be pg_default.

=> \c configdb
You are now connected to database "configdb" as user "student".
=> CREATE TABLE t(
  n integer
) TABLESPACE ts;
CREATE TABLE
=> \d t
                 Table "public.t"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 n      | integer |           |          |
Tablespace: "ts"

For temporary tables and their indexes, you can specify a separate tablespace as the default:

=> SET temp_tablespaces = 'ts';
SET
=> CREATE TEMP TABLE temp(s text);
CREATE TABLE
=> \d temp
             Table "pg_temp_3.temp"
 Column | Type | Collation | Nullable | Default
--------+------+-----------+----------+---------
 s      | text |           |          |
Tablespace: "ts"

The temp_tablespaces parameter accepts multiple tablespace names. When set, the server selects one randomly for each temporary object.

Managing Objects within Tablespaces

Tables (and other objects, such as indexes) can be moved between tablespaces.

=> \c appdb
You are now connected to database "appdb" as user "student".

Page 7: Page 7

=> ALTER TABLE t1 SET TABLESPACE pg_default;
ALTER TABLE
=> SELECT tablename, tablespace FROM pg_tables WHERE schemaname = 'public';
 tablename | tablespace
-----------+------------
 t2        | pg_default
 t1        | pg_default
(2 rows)

Indexes can also be moved to a different tablespace during rebuild:

=> REINDEX (TABLESPACE ts) TABLE t1;
REINDEX

You can move all objects from one tablespace to another:

=> ALTER TABLE ALL IN TABLESPACE pg_default SET TABLESPACE ts;
ALTER TABLE
=> SELECT tablename, tablespace FROM pg_tables WHERE schemaname = 'public';
 tablename | tablespace
-----------+------------
 t2        |
 t1        |
(2 rows)

Keep in mind that moving objects between tablespaces (unlike moving between schemas) is a physical operation that involves moving actual files from one directory to another. Access to the objects being moved is completely blocked for the duration of the operation.

Tablespace Size

We already know how to get the size of a database. Now, we can learn how to get the size of objects in a tablespace:

=> SELECT pg_size_pretty( pg_tablespace_size('ts') );
 pg_size_pretty
----------------
 7588 kB
(1 row)

Why is the size so large, despite the tablespace containing just a few empty tables? This is because ts is the default tablespace for the database appdb, so it is where the system catalog objects are stored. They take up space. You can get the size of the tablespaces by the psql command:

=> \db+
                                           List of tablespaces
    Name    |  Owner   |          Location          | Access privileges | Options |  Size
  | Description
------------+----------+----------------------------+-------------------+---------+-------
--+-------------
 pg_default | postgres |                            |                   |         | 36 MB
  |
 pg_global  | postgres |                            |                   |         | 589
kB  |
 ts         | student  | /var/lib/postgresql/ts_dir |                   |         | 7588
kB |
(3 rows)

Dropping a Tablespace

You can only delete a tablespace that is empty:

=> DROP TABLESPACE ts;

Page 8: Page 8

ERROR:  tablespace "ts" is not empty

Unlike with schemas, there is no CASCADE clause in the DROP TABLESPACE command. Objects within the tablespace may belong to multiple databases, while we are only connected to one. You can still figure out what databases contain dependent objects. This is where the system catalog comes in. First, find out and remember the tablespace ID:

=> SELECT oid FROM pg_tablespace WHERE spcname = 'ts';
  oid
-------
 16386
(1 row)

Next, get a list of databases that have objects in the tablespace we are going to remove:

=> SELECT datname
FROM pg_database
WHERE oid IN (SELECT pg_tablespace_databases(16386));
 datname
----------
 configdb
 appdb
(2 rows)

Then, connect to each of the databases and get a list of objects from pg_class:

=> \c configdb
You are now connected to database "configdb" as user "student".
=> SELECT relnamespace::regnamespace, relname, relkind
FROM pg_class
WHERE reltablespace = 16386;
 relnamespace | relname | relkind
--------------+---------+---------
 public       | t       | r
(1 row)

The table is no longer needed, drop it.

=> DROP TABLE t;
DROP TABLE

Now, for the second database. Since ts is the default tablespace, the tablespace ID of the objects in pg_class equals zero. These are the system catalog objects, as we already know:

=> \c appdb
You are now connected to database "appdb" as user "student".
=> SELECT count(*) FROM pg_class WHERE reltablespace = 0;
 count
-------
   371
(1 row)

You can set another tablespace as default. This will physically move all the tables into the new one. You need to disconnect from the database first.

=> \c postgres
You are now connected to database "postgres" as user "student".
=> ALTER DATABASE appdb SET TABLESPACE pg_default;
ALTER DATABASE

Finally, the tablespace can be deleted.

=> DROP TABLESPACE ts;
DROP TABLESPACE

Page 9: Page 9

On behalf of the postgres user, we remove the directory created for the tablespace.

student$ sudo -u postgres rm -rf /var/lib/postgresql/ts_dir

Page 10: Takeaways

Tablespaces organize physical data storage

  • Logical (databases, schemas) and physical (tablespaces) forms of data organization are independent

Page 11: Practice

Why does pg_default become the default tablespace when creating a database without the TABLESPACE keyword?

    1. Create a new tablespace.
    1. Set it as the default tablespace for the template1 database.
    1. Create a new database. Check which default tablespace is set for the new database.
    1. Find the symbolic link to the tablespace directory in PGDATA.
    1. Delete the created tablespace.

Page 12: 1. New Tablespace

student$ sudo -u postgres mkdir /var/lib/postgresql/ts_dir
=> CREATE TABLESPACE ts LOCATION '/var/lib/postgresql/ts_dir';
CREATE TABLESPACE

2. Default Tablespace for template1

=> ALTER DATABASE template1 SET TABLESPACE ts;
ALTER DATABASE

3. New Database, Verification

=> CREATE DATABASE db;
CREATE DATABASE
=> SELECT spcname
FROM pg_tablespace
WHERE oid = (SELECT dattablespace FROM pg_database WHERE datname = 'db');
 spcname
---------
 ts
(1 row)

The default tablespace is ts. Conclusion: the default tablespace is determined by the template from which the new database is cloned.

=> SELECT oid AS tsoid FROM pg_tablespace WHERE spcname = 'ts';
 tsoid
-------
 16386
(1 row)
student$ sudo -u postgres ls -l /var/lib/postgresql/16/main/pg_tblspc/16386
lrwxrwxrwx 1 postgres postgres 26 Nov 27 13:59
/var/lib/postgresql/16/main/pg_tblspc/16386 -> /var/lib/postgresql/ts_dir

5. Dropping the Tablespace

=> ALTER DATABASE template1 SET TABLESPACE pg_default;
ALTER DATABASE
=> DROP DATABASE db;
DROP DATABASE
=> DROP TABLESPACE ts;
DROP TABLESPACE
student$ sudo -u postgres rm -rf /var/lib/postgresql/ts_dir

Page 13: Practice+

  1. Set the random_page_cost parameter for the pg_default tablespace to 1.1.

Speaker notes

  1. Use the ALTER TABLESPACE … SET command:

https://postgrespro.com/docs/postgresql/16/sql-altertablespace

The seq_page_cost and random_page_cost parameters are used by the planner. They refer to the approximate cost of reading one page of data from disk with sequential and random access, respectively.

The lower the ratio between these parameters, the more often the planner will prefer index access over sequential table scanning.

The parameters *_cost and, more specifically, random_page_cost are discussed in more detail in the Query Optimization course (QPT).

Page 14: 1. Setting Random_page_cost for a Tablespace

The default seq_page_cost and random_page_cost values are suitable for slower HDD drives. It is assumed that random page access is four times as costly as sequential page access:

=> \dconfig *page_cost
List of configuration parameters
    Parameter     | Value
------------------+-------
 random_page_cost | 4
 seq_page_cost    | 1
(2 rows)

If you use drives with different properties, you can create different tablespaces with appropriate seq_page_cost and random_page_cost values for each. For example, quick SSD drives can have the random_page_cost value almost as low as seq_page_cost.

=> ALTER TABLESPACE pg_default SET (random_page_cost = 1.1);
ALTER TABLESPACE

Configuration adjustments made using the ALTER TABLESPACE command are stored in the pg_tablespace table. You can view them in psql with the following command:

=> \db+
                                         List of tablespaces
    Name    |  Owner   | Location | Access privileges |        Options         |  Size  |
Description
------------+----------+----------+-------------------+------------------------+--------+-
------------
 pg_default | postgres |          |                   | {random_page_cost=1.1} | 29 MB  |
 pg_global  | postgres |          |                   |                        | 589 kB |
(2 rows)

The *_cost parameters can also be set in postgresql.conf. This will apply them to all tablespaces.

11 Lesson 11: Data Organization / Low Level

Native Markdown source page: Data Organization / Low Level
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_11_data_lowlevel.html Archive: DBA1-handouts-16-en.zip PDF pages: 20

Page 1: Data Organization / Low Level

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Data Files
  • Forks: Main, Visibility Map, Free Space Map
  • Oversized Row Versions and TOAST

Page 3: Object Forks

Source slide page 3: Object Forks{#source-figure-003 caption=“Source slide page 3: Object Forks”}

Speaker notes

Usually, each database object that stores data (table, index, sequence, materialized view) has several corresponding forks. Each fork contains a specific type of data.

Initially, each fork contains a single file. The file name is a numeric identifier and may include a suffix derived from the fork name.

The file gradually increases in size until it reaches 1 GB, at which point the next file for the same fork is created. Such files are sometimes called segments.The segment sequence number is appended to the end of the file name. The pg_relation_size function displays the total size of a fork.

The 1 GB file size limit was established in the past to support file systems that cannot operate with larger file sizes. A different file size limit can be set during source code compilation with the –with-segsize option.

So, a single database object may consist of multiple files on disk. A small table will have three corresponding files on disk, and an index will have two. All object files belonging to the same tablespace and the same database are stored in the same directory. This may become an issue as some file systems may perform poorly on directories with a large number of files.

Page 4: Forks

Main fork

  • actual data (row versions)
  • exists for all objects
  • Initialization fork (init)
    • “template” of the main fork
    • used in case of failure; exists only for unlogged tables
  • Visibility map (vm)
    • exists only for tables
  • Free space map (fsm)
    • exists for both tables and indexes

Speaker notes

There are several types of forks.

The main fork contains the data itself, namely table row versions and index records. The main fork file names match the identifier. All objects have a main fork.

The file names of the initialization fork end with the _init suffix. This fork exists only for unlogged tables (created with the UNLOGGED clause) and their indexes. Such objects do not differ from regular ones, except that actions performed on them are not logged in WAL. This makes operations on them faster, but their content cannot be recovered if a failure occurs. During a recovery PostgreSQL just removes all the forks of such objects and writes the initialization fork in place of the main fork. The result is an empty table.

https://postgrespro.com/docs/postgresql/16/storage-init

The vm (visibility map) fork’s filenames end in _vm. The fork exists only for tables, separate MVCC for indexes is not supported.

The fsm (free space map) fork’s filenames end in _fsm. This fork exists for both tables and indexes.

These two maps were discussed in the Architecture module.

https://postgrespro.com/docs/postgresql/16/storage-fsm

https://postgrespro.com/docs/postgresql/16/storage-vm

Page 5: File Locations

=> CREATE DATABASE data_lowlevel;
CREATE DATABASE
=> \c data_lowlevel
You are now connected to database "data_lowlevel" as user "student".

Create a table and look where its files are.

=> CREATE TABLE t(
  id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  n numeric
);
CREATE TABLE
=> INSERT INTO t(n) SELECT id FROM generate_series(1,10_000) AS id;
INSERT 0 10000

To form additional forks, we will execute vacuuming:

=> VACUUM t;
VACUUM

The path to the main file relative to PGDATA is shown with the following function:

=> SELECT pg_relation_filepath('t');
 pg_relation_filepath
----------------------
 base/16386/16388
(1 row)

Since the table is located in the pg_default tablespace, the path starts with “base”, followed by the database directory:

=> SELECT oid FROM pg_database WHERE datname = 'data_lowlevel';
  oid
-------
 16386
(1 row)

Then follows the file name. You can get it using the query:

=> SELECT relfilenode FROM pg_class WHERE relname = 't';
 relfilenode
-------------
       16388
(1 row)

But the pg_relation_filepath function is more convenient because it returns the full path so that you don’t need to run several queries on the system catalog. Let’s have a look at the files themselves. Only the OS user postgres has access to PGDATA, so run the ls on their behalf:

postgres$ ls -l /var/lib/postgresql/16/main/base/16386/16388*
-rw------- 1 postgres postgres 450560 Nov 27 13:50
/var/lib/postgresql/16/main/base/16386/16388
-rw------- 1 postgres postgres  24576 Nov 27 13:50
/var/lib/postgresql/16/main/base/16386/16388_fsm
-rw------- 1 postgres postgres   8192 Nov 27 13:50
/var/lib/postgresql/16/main/base/16386/16388_vm

There are three forks: the main fork, the free space map (fsm) and the visibility map (vm). You can view the index files in a similar way:

=> \d t

Page 6: Page 6

                            Table "public.t"
 Column |  Type   | Collation | Nullable |           Default
--------+---------+-----------+----------+------------------------------
 id     | integer |           | not null | generated always as identity
 n      | numeric |           |          |
Indexes:
    "t_pkey" PRIMARY KEY, btree (id)
=> SELECT pg_relation_filepath('t_pkey');
 pg_relation_filepath
----------------------
 base/16386/16393
(1 row)
postgres$ ls -l /var/lib/postgresql/16/main/base/16386/16393*
-rw------- 1 postgres postgres 245760 Nov 27 13:50
/var/lib/postgresql/16/main/base/16386/16393

And files for the sequence that has been created for the primary key:

=> SELECT pg_relation_filepath(pg_get_serial_sequence('t','id'));
 pg_relation_filepath
----------------------
 base/16386/16387
(1 row)
postgres$ ls -l /var/lib/postgresql/16/main/base/16386/16387*
-rw------- 1 postgres postgres 8192 Nov 27 13:50
/var/lib/postgresql/16/main/base/16386/16387

For an index, the free space map is built only when empty pages exist, while a sequence has only a main fork. Temporary tables are stored in the same way as permanent tables.

=> CREATE TEMP TABLE temp AS SELECT * FROM t;
SELECT 10000
=> VACUUM temp;
VACUUM
=> SELECT pg_relation_filepath('temp');
 pg_relation_filepath
----------------------
 base/16386/t4_16397
(1 row)

A prefix matching the schema number is added to the filename for temporary objects.

postgres$ ls -l /var/lib/postgresql/16/main/base/16386/t4_16397*
-rw------- 1 postgres postgres 450560 Nov 27 13:50
/var/lib/postgresql/16/main/base/16386/t4_16397
-rw------- 1 postgres postgres  24576 Nov 27 13:50
/var/lib/postgresql/16/main/base/16386/t4_16397_fsm
-rw------- 1 postgres postgres   8192 Nov 27 13:50
/var/lib/postgresql/16/main/base/16386/t4_16397_vm

The oid2name additional supplied extension lets you quickly and easily find out which database objects relate to which files. You can view all databases:

student$ /usr/lib/postgresql/16/bin/oid2name
All databases:
    Oid  Database Name  Tablespace
----------------------------------
  16386  data_lowlevel  pg_default
      5       postgres  pg_default
  16385        student  pg_default
      4      template0  pg_default
      1      template1  pg_default

All objects in a database:

Page 7: Page 7

student$ /usr/lib/postgresql/16/bin/oid2name -d data_lowlevel
From database "data_lowlevel":
  Filenode  Table Name
----------------------
     16388           t
     16397        temp

All tablespaces in a database:

student$ /usr/lib/postgresql/16/bin/oid2name -d data_lowlevel -s
All tablespaces:
   Oid  Tablespace Name
-----------------------
  1663       pg_default
  1664        pg_global

Find the file name by table name:

student$ /usr/lib/postgresql/16/bin/oid2name -d data_lowlevel -t t
From database "data_lowlevel":
  Filenode  Table Name
----------------------
     16388           t

Or the table name by file name:

student$ /usr/lib/postgresql/16/bin/oid2name -d data_lowlevel -f 16388
From database "data_lowlevel":
  Filenode  Table Name
----------------------
     16388           t

Fork Sizes

You can get the size of the files that comprise a fork from the file system, but there is an easier way to get the size of each fork individually:

=> SELECT pg_relation_size('t','main') main,
          pg_relation_size('t','fsm') fsm,
          pg_relation_size('t','vm') vm;
  main  |  fsm  |  vm
--------+-------+------
 450560 | 24576 | 8192
(1 row)

Page 8: TOAST

A row version must fit into one page

  • some fields can be compressed
  • some fields can be moved into a TOAST table
  • fields can be both compressed and moved
  • TOAST table
    • located in the pg_toast (pg_toast_temp_N) schema
    • supported by its own index
    • contains chunks of oversized values, each chunk is smaller than a page
    • retrieved only when the oversized field is accessed
    • has its own row versions
    • used transparently for the application

Speaker notes

Any row version in PostgreSQL must fit entirely into one page. Oversized row versions are stored using TOAST, The Oversized Attributes Storage Technique. TOAST comprises several approaches to storing oversized field values. Firstly, the value can be compressed so that the row version fits into the page. Secondly, the value can be moved from the row version to a separate service table. Both strategies can be applied to the same row versions: some values would be compressed, some moved, some compressed and moved.

Any table can have a separate TOAST table (with a dedicated index) created for it, if necessary. Such tables and indexes are located in a separate schema named pg_toast and, therefore, are usually not visible (for temporary tables, pg_toast_temp_N schema is used, similarly to the regular pg_temp_N).

The row versions in the TOAST table must also fit into one page each, so longer values are split into multiple chunks, and are transparently “glued together” by PostgreSQL when the application demands.

TOAST table is used only when oversized values are accessed. Besides that, TOAST tables have their own row versions. Whenever a data update in the main table does not affect the oversized value, the new row version will refer to the same TOAST value, saving disk space.

https://postgrespro.com/docs/postgresql/16/storage-toast

Page 9: TOAST

The table t has a numeric type column. This type can hold very large numbers. For example:

=> SELECT length( (123456789::numeric ^ 12345::numeric)::text );
 length
--------
  99890
(1 row)

However, when inserted into the table, this humongous value does not change the table size:

=> SELECT pg_relation_size('t','main');
 pg_relation_size
------------------
           450560
(1 row)
=> INSERT INTO t(n) SELECT 123456789::numeric ^ 12345::numeric;
INSERT 0 1
=> SELECT pg_relation_size('t','main');
 pg_relation_size
------------------
           450560
(1 row)

Since the row version does not fit into a single page, the value of attribute n is stored in a separate TOAST table. TOAST tables and their indexes are created automatically for all tables that include potentially “oversized” data types and are used as needed. You can find the name and oid of the TOAST table:

=> SELECT relname, relfilenode FROM pg_class WHERE oid = (
    SELECT reltoastrelid FROM pg_class WHERE oid = 't'::regclass
);
    relname     | relfilenode
----------------+-------------
 pg_toast_16388 |       16391
(1 row)

And here are the TOAST table files:

postgres$ ls -l /var/lib/postgresql/16/main/base/16386/16391*
-rw------- 1 postgres postgres 57344 Nov 27 13:50
/var/lib/postgresql/16/main/base/16386/16391
-rw------- 1 postgres postgres 24576 Nov 27 13:50
/var/lib/postgresql/16/main/base/16386/16391_fsm

When it comes to oversized values, there are several strategies that can be employed. The name of the current strategy is listed in the Storage column:

=> \d+ t
                                                      Table "public.t"
 Column |  Type   | Collation | Nullable |           Default            | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+------------------------------+---------+-------
------+--------------+-------------
 id     | integer |           | not null | generated always as identity | plain   |
      |              |
 n      | numeric |           |          |                              | main    |
      |              |
Indexes:
    "t_pkey" PRIMARY KEY, btree (id)
Access method: heap

plain — TOAST is not used (type has a fixed length)

Page 10: Page 10

extended — both compression and out-of-line storage are used external — compression is not used, only out-of-line storage main — such fields are processed last and are moved to the toast table only if compression is not enough A storage strategy is assigned for each column when creating a table. It can be specified explicitly, and the default value depends on the data type. If necessary, the strategy can be modified later. For example, if you know that data in a column is already compressed, you can switch the strategy to external. For example:

=> ALTER TABLE t ALTER COLUMN n SET STORAGE external;
ALTER TABLE

This operation does not change the existing data, but defines the strategy to be used for new row versions.

Page 11: Table Size

Source slide page 11: Table Size{#source-figure-011 caption=“Source slide page 11: Table Size”}

pg_indexes_size
pg_table_size
pg_total_relation_size

Speaker notes

As already mentioned, the size of a single fork can be obtained by the pg_relation_size function. To get the total object size, other functions can be used:

● pg_table_size shows the size of the table and its TOAST part (the TOAST table and its index), but not the regular index sizes. The same function can be used to find the size of an individual index: both tables and indexes are relations, and despite the name, the function accepts any relation as an input.

● pg_indexes_size sums up the sizes of all table indexes except the TOAST table index.

● pg_total_relation_size shows the full size of the table, along with all its indexes.

Page 12: Table Size

The size of a table (including the TOAST table and its index):

=> SELECT pg_table_size('t');
 pg_table_size
---------------
        581632
(1 row)

Total size of all table indexes:

=> SELECT pg_indexes_size('t');
 pg_indexes_size
-----------------
          245760
(1 row)

You can get the size of a single index by using the pg_table_size function. Indexes have no TOASTs, so the function only shows the size of all index forks (main, fsm). Currently, the table t has just the primary key index, so its size matches the size returned by pg_indexes_size:

=> SELECT pg_table_size('t_pkey') AS t_pkey;
 t_pkey
--------
 245760
(1 row)

Total table size, including TOAST and all indexes:

=> SELECT pg_total_relation_size('t');
 pg_total_relation_size
------------------------
                 827392
(1 row)

Page 13: Takeaways

An object comprises several forks

  • A fork consists of one or more segment files
  • Oversized row versions are stored using TOAST

Page 14: Practice

  1. Create an unlogged table in a custom tablespace and make sure that it has an init fork.
  • Delete the created tablespace. 2. Create a table with a column of the text type. What storage strategy is used for this column?
    • Change the strategy to external and insert a short and a long row
    • into the table.
    • Check if the rows are in the TOAST table by making a direct
    • query to it. Explain why.

Page 15: 1. Unlogged Тable

student$ sudo -u postgres mkdir /var/lib/postgresql/ts_dir
=> CREATE TABLESPACE ts LOCATION '/var/lib/postgresql/ts_dir';
CREATE TABLESPACE
=> CREATE DATABASE data_lowlevel;
CREATE DATABASE
=> \c data_lowlevel
You are now connected to database "data_lowlevel" as user "student".
=> CREATE UNLOGGED TABLE u(n integer) TABLESPACE ts;
CREATE TABLE
=> INSERT INTO u(n) SELECT n FROM generate_series(1,1000) n;
INSERT 0 1000
=> SELECT pg_relation_filepath('u');
            pg_relation_filepath
---------------------------------------------
 pg_tblspc/16386/PG_16_202307071/16387/16388
(1 row)

Let’s look at the table files.

Note

Note Note that the ls command is executed on behalf of the postgres user. You can open a second terminal window and switch to another user with the following command:

student$ sudo -i -u postgres

Now, in the same window, run:

postgres$ ls -l /var/lib/postgresql/16/main/pg_tblspc/16386/PG_16_202307071/16387/16388*
-rw------- 1 postgres postgres 40960 Nov 27 14:00
/var/lib/postgresql/16/main/pg_tblspc/16386/PG_16_202307071/16387/16388
-rw------- 1 postgres postgres 24576 Nov 27 14:00
/var/lib/postgresql/16/main/pg_tblspc/16386/PG_16_202307071/16387/16388_fsm
-rw------- 1 postgres postgres     0 Nov 27 14:00
/var/lib/postgresql/16/main/pg_tblspc/16386/PG_16_202307071/16387/16388_init

Drop the created tablespace:

=> DROP TABLE u;
DROP TABLE
=> DROP TABLESPACE ts;
DROP TABLESPACE
student$ sudo -u postgres rm -rf /var/lib/postgresql/ts_dir

2. Table with a Text Column

=> CREATE TABLE t(s text);
CREATE TABLE
=> \d+ t
                                           Table "public.t"
 Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target |
Description
--------+------+-----------+----------+---------+----------+-------------+--------------+-
------------
 s      | text |           |          |         | extended |             |              |
Access method: heap

Dy default, the extended strategy is used for text data.

Page 16: Page 16

Change the strategy to external:

=> ALTER TABLE t ALTER COLUMN s SET STORAGE external;
ALTER TABLE
=> INSERT INTO t (s) VALUES ('Short string.');
INSERT 0 1
=> INSERT INTO t(s) VALUES (repeat('A',3456));
INSERT 0 1

Check the TOAST table:

=> SELECT relname FROM pg_class WHERE oid = (
  SELECT reltoastrelid FROM pg_class WHERE relname='t'
);
    relname
----------------
 pg_toast_16391
(1 row)

The TOAST table is “hidden”, because it is located in a schema that is excluded from the search path. This is a good thing, because TOAST is intended to work transparently for the user. However, there still are ways to view the table:

=> SELECT chunk_id, chunk_seq, length(chunk_data)
FROM pg_toast.pg_toast_16391
ORDER BY chunk_id, chunk_seq;
 chunk_id | chunk_seq | length
----------+-----------+--------
    16396 |         0 |   1996
    16396 |         1 |   1460
(2 rows)

Only the long string went into the TOAST table (two chunks, total size matches the string size). The short string wasn’t TOAST’ed: there is no need, as it fits into one page.

Page 17: Practice+

  1. Create a database.
  • Compare the database size returned by the pg_database_size
  • function with the total size of all tables in the database.
  • Explain the result. 2. TOAST supports two compression methods: pglz and lz4. Use SQL to check whether PostgreSQL was compiled with these methods support. 3. Create a text file of at least 10 MB size. Load its contents into a table with a text field, first without compression, and then using each of the algorithms. Compare the final table size and the data loading time of all three options.

Speaker notes

  1. You can get the list of database tables from the pg_class table.

  2. Using the pg_config view, you can find out which options were set for the configure script when the server software was compiled. The string containing the list of options is long; you can extract the necessary options using the string_to_table function.

  3. To obtain text for the experiment, you can take a sufficiently large binary file (for example, the postgres executable) and convert it to text. For the conversion, you can use the Base32 algorithm (the -w0 option disables line breaks): base32 -w0 < binary-file > text-file

Page 18: 1. Comparing the Size of a Database to the Total Size of its Tables

=> CREATE DATABASE data_lowlevel;
CREATE DATABASE
=> \c data_lowlevel
You are now connected to database "data_lowlevel" as user "student".

Even an empty database contains some system catalog tables. The list of all relations is stored in pg_class. Exclude from the calculation: Cluster-wide tables (they do not belong to the current database) Indexes and toast tables (they are automatically taken into account when calculating the size)

=> SELECT sum(pg_total_relation_size(oid))
FROM pg_class
WHERE NOT relisshared -- local database objects
AND relkind = 'r';  -- ordinary tables
   sum
---------
 7536640
(1 row)

The size of the database is a bit larger:

=> SELECT pg_database_size('data_lowlevel');
 pg_database_size
------------------
          7696867
(1 row)

This is because the pg_database_size function returns the size of the catalog in the file system, and the catalog contains some service files.

=> SELECT oid FROM pg_database WHERE datname = 'data_lowlevel';
  oid
-------
 16386
(1 row)
Note

Note Note that the ls command is executed on behalf of the postgres user. You can open a second terminal window and switch to another user with the following command:

student$ sudo -i -u postgres

Now, in the same window, run:

postgres$ ls -l /var/lib/postgresql/16/main/base/16386/[^0-9]*
-rw------- 1 postgres postgres    524 Nov 27 14:00
/var/lib/postgresql/16/main/base/16386/pg_filenode.map
-rw------- 1 postgres postgres 159700 Nov 27 14:00
/var/lib/postgresql/16/main/base/16386/pg_internal.init
-rw------- 1 postgres postgres      3 Nov 27 14:00
/var/lib/postgresql/16/main/base/16386/PG_VERSION

pg_filenode.map — mapping oid of some tables to file names; pg_internal.init — system catalog cache; PG_VERSION — PostgreSQL version. As some functions operate on the database object level, and others on the file system level, it is sometimes hard to compare the results directly. The same goes for the pg_tablespace_size function.

2. TOAST Compression Methods Support

The pg_config view displays options passed to the configure script during PostgreSQL compilation.

Page 19: Page 19

=> SELECT * FROM (
  SELECT string_to_table(setting, ''' ''') AS setting
  FROM pg_config WHERE name = 'CONFIGURE'
)
WHERE setting ~ '(lz|zs)';
   setting
-------------
 --with-lz4
 --with-zstd
(2 rows)

Which TOAST compression method is used by default?

=> \dconfig *toast*
 List of configuration parameters
         Parameter         | Value
---------------------------+-------
 default_toast_compression | pglz
(1 row)

What methods are available?

=> SELECT setting, enumvals FROM pg_settings WHERE name = 'default_toast_compression';
 setting |  enumvals
---------+------------
 pglz    | {pglz,lz4}
(1 row)

3. Comparison of Compression Methods

Let’s compare compression methods using text data as an example. To obtain a large text volume, we take the postgres executable file and convert it to text using the Base32 algorithm (commonly used in email encoding).

student$ sudo cat /usr/lib/postgresql/16/bin/postgres | base32 -w0 > /tmp/gram.input

The resulting text file is sufficiently large.

student$ ls -l --block-size=K /tmp/gram.input
-rw-rw-r-- 1 student student 16398K Nov 27 14:00 /tmp/gram.input

We create a table to load the text data. For the txt column, we set the external storage strategy, which allows out-of-line storage but prohibits compression.

=> CREATE TABLE t (
  txt text STORAGE EXTERNAL
);
CREATE TABLE

Next, we load the data from the text file.

=> \timing on
Timing is on.
=> COPY t FROM '/tmp/gram.input';
COPY 1
Time: 479.433 ms
=> \timing off
Timing is off.

We check the table size, including TOAST storage.

=> SELECT pg_table_size('t')/1024;
 ?column?
----------
    17064
(1 row)

After emptying the table, we activate compression using pglz.

Page 20: Page 20

=> TRUNCATE TABLE t;
TRUNCATE TABLE
=> ALTER TABLE t
ALTER COLUMN txt SET STORAGE EXTENDED,
ALTER COLUMN txt SET COMPRESSION pglz;
ALTER TABLE

Now, the extended strategy is applied, allowing both compression and out-of-line storage. We reload the data.

=> \timing on
Timing is on.
=> COPY t FROM '/tmp/gram.input';
COPY 1
Time: 1838.330 ms (00:01.838)
=> \timing off
Timing is off.
=> SELECT pg_table_size('t')/1024;
 ?column?
----------
    10384
(1 row)

The table size is significantly reduced, but loading time has been increased noticeably. After clearing the table again, we set lz4 compression.

=> TRUNCATE TABLE t;
TRUNCATE TABLE
=> ALTER TABLE t ALTER COLUMN txt SET COMPRESSION lz4;
ALTER TABLE

We reload the data once again and compare the results.

=> \timing on
Timing is on.
=> COPY t FROM '/tmp/gram.input';
COPY 1
Time: 524.215 ms
=> \timing off
Timing is off.
=> SELECT pg_table_size('t')/1024;
 ?column?
----------
    10720
(1 row)

The lz4 algorithm provides slightly worse compression than pglz but operates much faster. Finally, we delete the text file.

student$ sudo rm -f /tmp/gram.input

12 Lesson 12: Administrative Tasks / Monitoring

Native Markdown source page: Administrative Tasks / Monitoring
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_12_admin_monitoring.html Archive: DBA1-handouts-16-en.zip PDF pages: 30

Page 1: Administrative Tasks / Monitoring

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • OS Tools
  • Сumulative Statistics
  • Server Message Log
  • External Monitoring Systems

Page 3: OS Tools

Processes

  • ps, pgrep…
  • update_process_title parameter for updating the status of processes
  • cluster_name parameter for setting the cluster name
  • Resource usage
    • iostat, vmstat, sar, top…
  • Disk space
    • df, du, quota…

Speaker notes

PostgreSQL runs on an operating system and to a certain extent depends on its configuration.

PostgreSQL process information is accessible via OS tools. The server parameter update_process_title (on by default) displays the state of each process next to its title. The cluster_name parameter specifies the instance name used to identify it among running processes.

Various tools are available to monitor the use of system resources (CPU, RAM, disks) in Unix: iostat, vmstat, sar, top, etc.

Disk space monitoring is also necessary. The space occupied by the database on disk can be viewed both from the database itself (see the Data Organization module) and from the OS (with the du command). The amount of disk space available is also displayed with the df command in the OS. If disk quotas are used, they must also be taken into account.

The tools and approaches to monitoring differ significantly between various OS and file systems, so we will not discuss them in detail.

https://postgrespro.com/docs/postgresql/16/monitoring-ps

https://postgrespro.com/docs/postgresql/16/diskusage

Page 4: Сumulative Statistics

Statistics Collection

  • Current System Activities
  • Command Execution Monitoring
  • Extensions

Speaker notes

There are two primary sources of information about the state of the system. The first one is statistical information collected by PostgreSQL and stored inside the cluster.

Page 5: Statistics Collection

Settings of cumulative statistics

  • parameter
  • action
  • track_activities
  • monitor current commands
  • track_counts
  • collect table and index access statistics
  • track_functions
  • monitor user function calls
  • off by default
  • track_io_timing
  • monitor block read and write timing statistics
  • off by default
  • track_wal_io_timing
  • monitor write timing of WAL operations
  • off by default

Speaker notes

The cumulative statistics system in PostgreSQL collects and provides data on server operations. Cumulative statistics track access to tables and indexes at both the disk block and row levels. Additionally, they record details such as the number of rows, vacuum and analyze operations for each table.

It is also possible to track the number of user function calls and their execution time.

The amount of information collected is controlled by several server parameters, since the more information is collected, the greater the overhead.

https://postgrespro.com/docs/postgresql/16/monitoring-stats

Page 6: Architecture

Source slide page 6: Architecture{#source-figure-006 caption=“Source slide page 6: Architecture”}

Speaker notes

Backends collect statistics in running transactions. The process stores this data in shared memory, updating it at most once per second (compile-time setting).

Cumulative statistics are stored in PGDATA/pg_stat/ during a normal server shutdown and reloaded upon startup. In case of a crash shutdown, all counters are reset.

Backend may cache statistical data. The caching level is controlled by the stats_fetch_consistency parameter.

● none — no caching; statistics reside only in shared memory.

● cache — statistics for a single object are cached.

● snapshot — statistics for the entire database are cached.

Cache is the default mode, which balances consistency and performance efficiency.

Cached statistics are not refreshed and are discarded at the end of a transaction, or when pg_stat_clear_snapshot() is called.

Due to latency and caching, the backend will not always have the latest statistics, but it is seldom necessary.

Page 7: Cumulative Statistics

=> CREATE DATABASE admin_monitoring;
CREATE DATABASE
=> \c admin_monitoring
You are now connected to database "admin_monitoring" as user "student".

Enable collection of I/O statistics first:

=> ALTER SYSTEM SET track_io_timing=on;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Monitoring server activity only makes sense if there is any activity. We can imitate load with pgbench, a standard benchmarking utility. First, it creates a number of tables and fills them with data.

student$ pgbench -i admin_monitoring
dropping old tables...
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
100000 of 100000 tuples (100%) done (elapsed 0.27 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 0.65 s (drop tables 0.00 s, create tables 0.03 s, client-side generate 0.31 s,
vacuum 0.13 s, primary keys 0.18 s).

Let’s reset the previously accumulated database statistics.

=> SELECT pg_stat_reset();
 pg_stat_reset
---------------
(1 row)

Including instance I/O statistics:

=> SELECT pg_stat_reset_shared('io');
 pg_stat_reset_shared
----------------------
(1 row)

Start the TPC-B test and let it run for a few seconds:

student$ pgbench -T 10 admin_monitoring
pgbench (16.11 (Ubuntu 16.11-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 10 s
number of transactions actually processed: 1119
number of failed transactions: 0 (0.000%)

Page 8: Page 8

latency average = 8.933 ms
initial connection time = 10.450 ms
tps = 111.944576 (without initial connection time)

Now, let’s check the statistics on table access in terms of rows:

=> SELECT *
FROM pg_stat_all_tables
WHERE relid = 'pgbench_accounts'::regclass \gx
-[ RECORD 1 ]-------+------------------------------
relid               | 16393
schemaname          | public
relname             | pgbench_accounts
seq_scan            | 0
last_seq_scan       |
seq_tup_read        | 0
idx_scan            | 2238
last_idx_scan       | 2025-11-27 13:51:06.378593+03
idx_tup_fetch       | 2238
n_tup_ins           | 0
n_tup_upd           | 1119
n_tup_del           | 0
n_tup_hot_upd       | 290
n_tup_newpage_upd   | 829
n_live_tup          | 0
n_dead_tup          | 1062
n_mod_since_analyze | 1119
n_ins_since_vacuum  | 0
last_vacuum         |
last_autovacuum     |
last_analyze        |
last_autoanalyze    |
vacuum_count        | 0
autovacuum_count    | 0
analyze_count       | 0
autoanalyze_count   | 0

And in terms of tables:

=> SELECT *
FROM pg_statio_all_tables
WHERE relid = 'pgbench_accounts'::regclass \gx
-[ RECORD 1 ]---+-----------------
relid           | 16393
schemaname      | public
relname         | pgbench_accounts
heap_blks_read  | 0
heap_blks_hit   | 7824
idx_blks_read   | 273
idx_blks_hit    | 5866
toast_blks_read |
toast_blks_hit  |
tidx_blks_read  |
tidx_blks_hit   |

There are similar views for indexes:

=> SELECT *
FROM pg_stat_all_indexes
WHERE relid = 'pgbench_accounts'::regclass \gx
-[ RECORD 1 ]-+------------------------------
relid         | 16393
indexrelid    | 16407
schemaname    | public
relname       | pgbench_accounts
indexrelname  | pgbench_accounts_pkey
idx_scan      | 2238
last_idx_scan | 2025-11-27 13:51:06.378593+03
idx_tup_read  | 3071
idx_tup_fetch | 2238
=> SELECT *
FROM pg_statio_all_indexes
WHERE relid = 'pgbench_accounts'::regclass \gx

Page 9: Page 9

-[ RECORD 1 ]-+----------------------
relid         | 16393
indexrelid    | 16407
schemaname    | public
relname       | pgbench_accounts
indexrelname  | pgbench_accounts_pkey
idx_blks_read | 273
idx_blks_hit  | 5866

These views can be used to pinpoint unused indexes. Such indexes not only occupy useful space on the disk, but also waste resources on updates every time data in the table changes. There are also views for user-defined and system objects (all, user, sys), current transaction statistics (pg_stat_xact*), and more. You can view global statistics across the whole database:

=> SELECT *
FROM pg_stat_database
WHERE datname = 'admin_monitoring' \gx
-[ RECORD 1 ]------------+-----------------------------
datid                    | 16386
datname                  | admin_monitoring
numbackends              | 1
xact_commit              | 1138
xact_rollback            | 0
blks_read                | 275
blks_hit                 | 23537
tup_returned             | 18052
tup_fetched              | 3371
tup_inserted             | 1119
tup_updated              | 3358
tup_deleted              | 0
conflicts                | 0
temp_files               | 0
temp_bytes               | 0
deadlocks                | 0
checksum_failures        |
checksum_last_failure    |
blk_read_time            | 10.404
blk_write_time           | 0.697
session_time             | 22702.047
active_time              | 9444.514
idle_in_transaction_time | 516.089
sessions                 | 2
sessions_abandoned       | 0
sessions_fatal           | 0
sessions_killed          | 0
stats_reset              | 2025-11-27 13:50:55.98728+03

It provides a lot of data on the number of deadlocks occurred, committed and cancelled transactions, utilization of temporary files, and checksum errors. It also maintains total session count and statistics on sessions terminated for various reasons. The numbackends column specifically indicates the current number of active backend processes connected to this database. For monitoring I/O operations at the server level, administrators can query the pg_stat_io view. Let’s first execute a checkpoint operation, then examine the resulting page read and write counts categorized by process type:

=> CHECKPOINT;
CHECKPOINT
=> SELECT backend_type, sum(hits) hits, sum(reads) reads, sum(writes) writes
FROM pg_stat_io
GROUP BY backend_type;

Page 10: Page 10

    backend_type     | hits  | reads | writes
---------------------+-------+-------+--------
 background worker   |     0 |     0 |      0
 client backend      | 23723 |   275 |      0
 walsender           |     0 |     0 |      0
 standalone backend  |     0 |     0 |      0
 autovacuum worker   |   559 |     0 |      0
 autovacuum launcher |     0 |     0 |      0
 background writer   |       |       |      0
 startup             |     0 |     0 |      0
 checkpointer        |       |       |   2874
(9 rows)

Page 11: Current Activities

Configuration

  • statistics
  • parameter
  • current activities and waits
  • track_activities
  • of backends
  • on by default
  • and background processes

Speaker notes

The current activities of all backends and background processes are displayed in the pg_stat_activity view. We will focus on it more in the demo.

This view depends on the track_activities parameter (enabled by default).

Page 12: Current Activities

Source slide page 12: Current Activities{#source-figure-012 caption=“Source slide page 12: Current Activities”}

Let’s imitate a scenario when one process blocks another, and then figure it out using system views. Create a table with one row:

=> CREATE TABLE t(n integer);
CREATE TABLE
=> INSERT INTO t VALUES(42);
INSERT 0 1

Start two sessions, one of which changes the table and does not complete the transaction:

student$ psql -d admin_monitoring
=> BEGIN;
BEGIN
=> UPDATE t SET n = n + 1;
UPDATE 1

And the other tries to change the same row and gets blocked:

student$ psql -d admin_monitoring
=> UPDATE t SET n = n + 2;

View data about backend processes:

=> SELECT pid, query, state, wait_event, wait_event_type, pg_blocking_pids(pid)
FROM pg_stat_activity
WHERE backend_type = 'client backend' \gx
-[ RECORD 1
]----+-----------------------------------------------------------------------------
pid              | 22152
query            | UPDATE t SET n = n + 1;
state            | idle in transaction
wait_event       | ClientRead
wait_event_type  | Client
pg_blocking_pids | {}
-[ RECORD 2
]----+-----------------------------------------------------------------------------
pid              | 21005
query            | SELECT pid, query, state, wait_event, wait_event_type,
pg_blocking_pids(pid)+
                 | FROM pg_stat_activity
     +
                 | WHERE backend_type = 'client backend'
state            | active
wait_event       |
wait_event_type  |
pg_blocking_pids | {}
-[ RECORD 3
]----+-----------------------------------------------------------------------------
pid              | 22245
query            | UPDATE t SET n = n + 2;
state            | active
wait_event       | transactionid
wait_event_type  | Lock
pg_blocking_pids | {22152}

The state “idle in transaction” means that the session has started a transaction, but isn’t doing anything at the moment, and the transaction isn’t closed. This could become a problem if the situation comes up regularly (for example, because of poor application code or driver errors), because an open transaction holds a data snapshot and prevents vacuuming. The administrator has a parameter idle_in_transaction_session_timeout at their disposal to force sessions to close after their transaction is idle for a certain period of time. The idle_session_timeout parameter forcibly terminates sessions that remain idle beyond the specified duration. You can also terminate a blocking session manually. First, you need the blocked process ID. The function pg_blocking_pids can help you with that:

Page 13: Page 13

=> SELECT pid AS blocked_pid
FROM pg_stat_activity
WHERE backend_type = 'client backend'
AND cardinality(pg_blocking_pids(pid)) > 0;
 blocked_pid
-------------
       22245
(1 row)

Instead, you can use a query on the locks table. It will return two rows in this case: one transaction has been granted the lock, while the other is waiting for it.

=> SELECT locktype, transactionid, pid, mode, granted
FROM pg_locks
WHERE transactionid IN (
  SELECT transactionid FROM pg_locks WHERE pid = 22245 AND NOT granted
);
   locktype    | transactionid |  pid  |     mode      | granted
---------------+---------------+-------+---------------+---------
 transactionid |          1871 | 22245 | ShareLock     | f
 transactionid |          1871 | 22152 | ExclusiveLock | t
(2 rows)

Generally, you have to keep the lock type in mind. Running query can be cancelled with the pg_cancel_backend function. The transaction is idle in our case, so we use the pg_terminate_backend to terminate the session:

=> SELECT pg_terminate_backend(b.pid)
FROM unnest(pg_blocking_pids(22245)) AS b(pid);
 pg_terminate_backend
----------------------
 t
(1 row)

The unnest function is necessary because pg_blocking_pids returns an array of process IDs that block the specified process. There is only one in our examples, but there can be multiple. Locks are discussed in more detail in the DBA2 course. Check the backends:

=> SELECT pid, query, state, wait_event, wait_event_type
FROM pg_stat_activity
WHERE backend_type = 'client backend' \gx
-[ RECORD 1 ]---+------------------------------------------------------
pid             | 21005
query           | SELECT pid, query, state, wait_event, wait_event_type+
                | FROM pg_stat_activity                                +
                | WHERE backend_type = 'client backend'
state           | active
wait_event      |
wait_event_type |
-[ RECORD 2 ]---+------------------------------------------------------
pid             | 22245
query           | UPDATE t SET n = n + 2;
state           | idle
wait_event      | ClientRead
wait_event_type | Client

Only two remain, and the blocked one has completed its transaction successfully. The pg_stat_activity view shows the information not only about backend processes, but also about the system background processes running on the instance:

=> SELECT pid, backend_type, backend_start, state
FROM pg_stat_activity;

Page 14: Page 14

  pid  |         backend_type         |         backend_start         | state
-------+------------------------------+-------------------------------+--------
 20908 | autovacuum launcher          | 2025-11-27 13:50:46.90002+03  |
 20909 | logical replication launcher | 2025-11-27 13:50:46.901748+03 |
 21005 | client backend               | 2025-11-27 13:50:54.68677+03  | active
 22245 | client backend               | 2025-11-27 13:51:14.373422+03 | idle
 20905 | background writer            | 2025-11-27 13:50:46.818366+03 |
 20904 | checkpointer                 | 2025-11-27 13:50:46.817735+03 |
 20907 | walwriter                    | 2025-11-27 13:50:46.898508+03 |
(7 rows)

Compare that to what the OS reports:

student$ sudo head -n 1 /var/lib/postgresql/16/main/postmaster.pid
20903
student$ ps -o pid,command --ppid 20903
    PID COMMAND
  20904 postgres: 16/main: checkpointer
  20905 postgres: 16/main: background writer
  20907 postgres: 16/main: walwriter
  20908 postgres: 16/main: autovacuum launcher
  20909 postgres: 16/main: logical replication launcher
  21005 postgres: 16/main: student admin_monitoring [local] idle
  22245 postgres: 16/main: student admin_monitoring [local] idle

Page 15: Command Execution

Views for monitoring command executions

  • command
  • execution
  • ANALYZE
  • pg_stat_progress_analyze
  • CREATE INDEX, REINDEX
  • pg_stat_progress_create_index
  • VACUUM
  • pg_stat_progress_vacuum
  • including autovacuuming
  • CLUSTER, VACUUM FULL
  • pg_stat_progress_cluster
  • Create base backup
  • pg_stat_progress_basebackup
  • COPY
  • pg_stat_progress_copy

Speaker notes

You can monitor the progress of some potentially long-running commands using the corresponding views.

The structures of the views are described in the documentation:

https://postgrespro.com/docs/postgresql/16/progress-reporting

Backup is discussed in the Backup module.

Page 16: Additional Statistics

Additional supplied extensions

  • pg_stat_statements
  • query statistics
  • pgstattuple
  • row versions statistics
  • pg_buffercache
  • buffer cache status
  • Other extensions
    • pg_wait_sampling
    • statistics for waits
    • pg_stat_kcache
    • CPU and I/O statistics
    • pg_qualstats
    • predicate statistics
    • …

Speaker notes

There are extensions, both additional supplied and third-party, that enable the collection of additional statistics.

For example, the pg_stat_statements extension collects information about queries executed by the system, pg_buffercache provides tools for monitoring the buffer cache, etc.

Many key extensions are discussed in more detail in the DBA2 and DEV2 courses.

Page 17: Server Message Log

Log Record Configuration

  • Log File Rotation
  • Log Analysis

Speaker notes

The other primary source of information about the state of the server is the message log.

Page 18: Server Message Log

Message receiver (log\_destination = list)

  • stderr
  • error stream
  • csvlog
  • CSV format (if the collector is enabled)
  • jsonlog
  • JSON format (if the collector is enabled)
  • syslog
  • the syslog daemon
  • eventlog
  • Windows event log
  • Message collector (logging_collector = on)
    • can provide additional info
    • never loses messages (unlike syslog)
    • writes stderr, csvlog and jsonlog to the log_directory/log_filename

Speaker notes

The server log can be output in various formats and forwarded to various destinations. The format and the destination are determined primarily by the log_destination parameter (you can list multiple receivers separated by a comma).

The stderr value (on by default) sends messages to the standard error stream as plain text. The syslog value forwards messages to the syslog daemon (for Unix systems), and the eventlog value does the same for the Windows event log.

The message collector is an auxiliary process that collects additional information from all PostgreSQL processes to supplement the basic log messages. It is designed to keep track of every message, therefore it can become the bottleneck in high-load environments.

The message collector is switched on and off by the logging_collector parameter. When stderr is enabled, the messages are written into the file defined by the log_filename parameter, which is located in the directory defined by the log_directory parameter.

When the collector is on and csvlog is selected as a receiver, the info will also be output into a CSV file log_filename.csv. With the jsonlog output enabled, log files are written in JSON format and use the .json file extension.

Page 19: What to Log?

Settings

  • information
  • parameter
  • level of messages
  • log_min_messages
  • long command execution time
  • log_min_duration_statement
  • command execution time
  • log_duration
  • application name
  • application_name
  • checkpoints
  • log_checkpoints
  • connections and disconnections
  • log_(dis)connections
  • long lock waits
  • log_lock_waits
  • command executed
  • log_statement
  • temporary file usage
  • log_temp_files
  • …

Speaker notes

A lot of useful information can be output to the server message log. By default, almost all output is disabled so as not to turn logging into the bottleneck for the I/O subsystem. The administrator should decide what information is important, provide the necessary disk space to store it, and evaluate the impact of the log output on the overall system performance.

Page 20: Log File Rotation

By the message collector

  • setting
  • parameter
  • file name pattern
  • log_filename
  • rotation time, minutes
  • log_rotation_age
  • rotation file size, KB
  • log_rotation_size
  • overwrite file
  • log_truncate_on_rotation = on
  • combining file name patterns and rotation times allow for different rotation
  • schemes:
  • ‘postgresql-%H.log’, ‘1h’
  • 24 files a day
  • ‘postgresql-%a.log’, ‘1d’
  • 7 files a week
  • External tools
    • logrotate system utility

Speaker notes

If all the log output goes into a single file, sooner or later the file will grow to an unmanageable size, making administration and analysis highly inconvenient. Therefore, a log rotation scheme is usually employed.

https://postgrespro.com/docs/postgresql/16/logfile-maintenance

The message collector has its own rotation tools. Some of the parameters that configure them are listed on the slide.

The log_filename parameter can specify not just a name, but a file name pattern using designated date and time characters.

The log_rotation_age parameter determines how long a file is used before the output switches to a new one (and log_rotation_size is the file size at which to switch to the next one).

The log_truncate_on_rotation flag determines if PostgreSQL should overwrite existing files or append messages to them.

Different rotation schemes can be defined by using various file name patterns and switch time combinations.

https://postgrespro.com/docs/postgresql/16/runtime-config-logging#RUNTIM E-CONFIG-LOGGING-WHERE

Alternatively, rotation can be managed by external tools. For example, Ubuntu package uses logrotate system utility (it is configured through the /etc/logrotate.d/postgresql-common file).

Page 21: Log Analysis

OS tools

  • grep, awk…
  • Special analysis tools
    • pgBadger — requires a certain log configuration

Speaker notes

There are different ways to analyze logs.

You can search for certain information using OS tools or specially designed scripts.

The de facto standard for log analysis is the PgBadger application https://github.com/darold/pgbadger, but it imposes certain restrictions on the contents of the log. In particular, only messages in English are allowed.

Page 22: Log Analysis

Let’s consider a simple case. For example, display all messages of the FATAL level:

student$ sudo grep FATAL /var/log/postgresql/postgresql-16-main.log | tail -n 10
2025-11-27 13:45:53.443 MSK [2670] student@student FATAL:  terminating connection due to
administrator command
2025-11-27 13:51:16.330 MSK [22152] student@admin_monitoring FATAL:  terminating
connection due to administrator command

The “terminating connection” message is caused by us terminating the blocking process. Logs are usually used to analyse the queries that execute the longest. We can make the log display all executed commands and their execution times:

=> ALTER SYSTEM SET log_min_duration_statement=0;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Now, run a command:

=> SELECT sum(random()) FROM generate_series(1,1_000_000);
        sum
--------------------
 500117.78944922757
(1 row)

Check the log:

student$ sudo tail -n 1 /var/log/postgresql/postgresql-16-main.log
2025-11-27 13:51:18.277 MSK [21005] student@admin_monitoring LOG:  duration: 405.591 ms
statement: SELECT sum(random()) FROM generate_series(1,1_000_000);

Page 23: External Monitoring

Universal monitoring systems

  • Zabbix, Munin, Cacti…
  • cloud-based: Okmeter, NewRelic, Datadog…
  • PostgreSQL monitoring systems
    • pg_profile, pgpro_pwr
    • PGObserver
    • PostgreSQL Workload Analyzer (PoWA)
    • Open PostgreSQL Monitoring (OPM)
    • …

Speaker notes

In practice, you need a full-fledged monitoring system that collects various metrics from both PostgreSQL and the operating system, stores the history of these metrics, displays them as readable graphs, notifies when certain metrics exceed certain limits, etc.

PostgreSQL does not come with such a system by itself, it only provides the means by which such information can be acquired. We have gone over them already. Therefore, for full-scale monitoring, an external system is required. There are quite a few such systems on the market. Some are universal and come with PostgreSQL plugins or agents. These include Zabbix, Munin, Cacti, cloud services such as Okmeter, NewRelic, Datadog, and others.

There are also systems specifically designed for PostgreSQL: PGObserver, PoWA, OPM, etc. The pg_profile extension allows you to build snapshots of static data and compare them, identifying resource-intensive operations and their dynamics. pgpro_pwr is its extended, commercially available version.

https://postgrespro.com/docs/enterprise/16/pgpro-pwr

An incomplete but representative list of monitoring systems can be viewed here: https://wiki.postgresql.org/wiki/Monitoring

Page 24: Takeaways

Monitoring collects data on server operations both from the operating system and from the database points of view

  • PostgreSQL provides cumulative statistics and the server message log
  • Full-scale monitoring requires an external system

Page 25: Practice

  1. In a new database, create a table, insert several rows, and then delete all rows.
  • Look at the table access statistics and reference the values (n_tup_ins, n_tup_del, n_live_tup, n_dead_tup) against your activity. Perform a vacuum, check the statistics again and compare with the previous figures. 2. Create a deadlock with two transactions.
    • See what information is recorded in the server message log.

Speaker notes

  1. Deadlock is a situation when two (or more) transactions are waiting for each other to complete first. Unlike a normal lock, transactions have no way to get out of deadlock, and the DBMS has to resolve it by forcibly interrupting one of the transactions.

The easiest way to reproduce a deadlock is on a table with two rows. The first transaction changes (and locks) the first row, and the second one locks the second row. Then the first transaction tries to change the second row, discovers that it is locked, and starts waiting. And then the second transaction tries to change the first row, and also waits for the lock to be released.

Page 26: Table Access Statistics

Create a database and a table:

=> CREATE DATABASE admin_monitoring;
CREATE DATABASE
=> \c admin_monitoring
You are now connected to database "admin_monitoring" as user "student".
=> CREATE TABLE t(n numeric);
CREATE TABLE
=> INSERT INTO t SELECT 1 FROM generate_series(1,1000);
INSERT 0 1000
=> DELETE FROM t;
DELETE 1000

Check access statistics.

=> SELECT * FROM pg_stat_all_tables WHERE relid = 't'::regclass \gx
-[ RECORD 1 ]-------+------------------------------
relid               | 16387
schemaname          | public
relname             | t
seq_scan            | 1
last_seq_scan       | 2025-11-27 14:00:50.589242+03
seq_tup_read        | 1000
idx_scan            |
last_idx_scan       |
idx_tup_fetch       |
n_tup_ins           | 1000
n_tup_upd           | 0
n_tup_del           | 1000
n_tup_hot_upd       | 0
n_tup_newpage_upd   | 0
n_live_tup          | 0
n_dead_tup          | 1000
n_mod_since_analyze | 2000
n_ins_since_vacuum  | 1000
last_vacuum         |
last_autovacuum     |
last_analyze        |
last_autoanalyze    |
vacuum_count        | 0
autovacuum_count    | 0
analyze_count       | 0
autoanalyze_count   | 0

We inserted 1000 rows (n_tup_ins = 1000), then removed 1000 rows (n_tup_del = 1000). No live row versions remain (n_live_tup = 0), all 1000 rows are dead (n_dead_tup = 1000). Run vacuuming.

=> VACUUM;
VACUUM
=> SELECT * FROM pg_stat_all_tables WHERE relid = 't'::regclass \gx

Page 27: Page 27

-[ RECORD 1 ]-------+------------------------------
relid               | 16387
schemaname          | public
relname             | t
seq_scan            | 1
last_seq_scan       | 2025-11-27 14:00:50.589242+03
seq_tup_read        | 1000
idx_scan            |
last_idx_scan       |
idx_tup_fetch       |
n_tup_ins           | 1000
n_tup_upd           | 0
n_tup_del           | 1000
n_tup_hot_upd       | 0
n_tup_newpage_upd   | 0
n_live_tup          | 0
n_dead_tup          | 0
n_mod_since_analyze | 2000
n_ins_since_vacuum  | 0
last_vacuum         | 2025-11-27 14:00:52.37659+03
last_autovacuum     |
last_analyze        |
last_autoanalyze    |
vacuum_count        | 1
autovacuum_count    | 0
analyze_count       | 0
autoanalyze_count   | 0

Dead row versions vacuumed (n_dead_tup = 0), vacuuming performed one time (vacuum_count = 1).

2. Deadlocks

=> INSERT INTO t VALUES (1),(2);
INSERT 0 2

One transaction locks the first row of the table…

student$ psql
=> \c admin_monitoring
You are now connected to database "admin_monitoring" as user "student".
=> BEGIN;
BEGIN
=> UPDATE t SET n = 10 WHERE n = 1;
UPDATE 1

The other locks the second row…

student$ psql
=> \c admin_monitoring
You are now connected to database "admin_monitoring" as user "student".
=> BEGIN;
BEGIN
=> UPDATE t SET n = 200 WHERE n = 2;
UPDATE 1

Now, the first transaction tries to change the second row and waits for it to release…

=> UPDATE t SET n = 20 WHERE n = 2;

While the second transaction waits for the first row to release…

=> UPDATE t SET n = 100 WHERE n = 1;

…and so a deadlock occurs. The server terminates one of the transactions:

ERROR:  deadlock detected
DETAIL:  Process 43139 waits for ShareLock on transaction 739; blocked by process 43007.
Process 43007 waits for ShareLock on transaction 740; blocked by process 43139.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,1) in relation "t"

Page 28: Page 28

The other transaction gets unblocked:

UPDATE 1

Check the message log:

student$ sudo tail -n 8 /var/log/postgresql/postgresql-16-main.log
2025-11-27 14:00:56.806 MSK [43139] student@admin_monitoring ERROR:  deadlock detected
2025-11-27 14:00:56.806 MSK [43139] student@admin_monitoring DETAIL:  Process 43139 waits
for ShareLock on transaction 739; blocked by process 43007.
Process 43007 waits for ShareLock on transaction 740; blocked by process 43139.
Process 43139: UPDATE t SET n = 100 WHERE n = 1;
Process 43007: UPDATE t SET n = 20 WHERE n = 2;
2025-11-27 14:00:56.806 MSK [43139] student@admin_monitoring HINT:  See server log for
query details.
2025-11-27 14:00:56.806 MSK [43139] student@admin_monitoring CONTEXT:  while updating
tuple (0,1) in relation "t"
2025-11-27 14:00:56.806 MSK [43139] student@admin_monitoring STATEMENT:  UPDATE t SET n =
100 WHERE n = 1;

Page 29: Practice+

  1. Install the pg_stat_statements extension.
  • Execute several queries.
  • See what information gets into the pg_stat_statements view.

Speaker notes

  1. To install the extension, before executing the CREATE EXTENSION command, change the value of the shared_preload_libraries parameter, and restart the server.

https://postgrespro.com/docs/postgresql/16/pgstatstatements

Page 30: 1. The pg_stat_statements Extension

The extension collects planning and execution statistics for all queries. For the extension to work, a module with the same name has to be loaded. To do that, add the module name to shared_preload_libraries and restart the server. This is usually done through postgresql.conf, but for the purpose of the demo we will set it using the ALTER SYSTEM command.

=> ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements';
ALTER SYSTEM
=> \q
student$ sudo pg_ctlcluster 16 main restart
student$ psql
=> CREATE DATABASE admin_monitoring;
CREATE DATABASE
=> \c admin_monitoring
You are now connected to database "admin_monitoring" as user "student".
=> CREATE EXTENSION pg_stat_statements;
CREATE EXTENSION

Now, run some queries:

=> CREATE TABLE t(n numeric);
CREATE TABLE
=> SELECT format('INSERT INTO t VALUES (%L)', x)
FROM generate_series(1,5) AS x \gexec
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
=> DELETE FROM t;
DELETE 5
=> DROP TABLE t;
DROP TABLE

Check the statistics for the most frequently executed query.

=> SELECT query, calls, total_exec_time
FROM pg_stat_statements
ORDER BY calls DESC LIMIT 1;
           query           | calls | total_exec_time
---------------------------+-------+-----------------
 INSERT INTO t VALUES ($1) |     5 |         0.18377
(1 row)

The shared library is no longer required — restoring the original parameter value:

=> ALTER SYSTEM RESET shared_preload_libraries;
ALTER SYSTEM
=> \q
student$ sudo pg_ctlcluster 16 main restart

13 Lesson 13: Access Control / Overview

Native Markdown source page: Access Control / Overview
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_13_access_overview.html Archive: DBA1-handouts-16-en.zip PDF pages: 30

Page 1: Access Control / Overview

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Igor Gnatyuk Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Roles and Attributes
  • Connecting to a Server
  • Password Authentication
  • Privileges and Privilege Management
  • Role Categories
  • Group and Predefined Roles
  • Default Privileges
  • Privileges and Routines

Page 3: Roles and Attributes

Role can be a DBMS user

  • not associated with the OS user
  • Roles can be included into other roles
    • simplifies access setup
  • Attributes define the properties of a role
    • LOGIN
    • can log in
    • SUPERUSER
    • superuser privileges
    • CREATEDB
    • can create databases
    • CREATEROLE
    • can create roles
    • and others

Speaker notes

Roles in PostgreSQL are used for two purposes. Firstly, a role can be a DBMS user. Secondly, roles can be members of other roles — it is convenient when setting up access.

Formally, roles are not associated with operating system users in any way, but many programs imply it when choosing default values. For example, if psql is started on behalf of the student OS user, the connection is established on behalf of the database role with the same name, i.e., student (unless another role is explicitly specified in the psql options).

At the time of cluster initialization, an initial role is defined, which has superuser privileges (this role is usually called postgres). Later on, you can create, modify, and delete roles.

https://postgrespro.com/docs/postgresql/16/database-roles

A role has several attributes which define its general properties and rights (unrelated to particular objects).

Generally, attributes come in two opposite variations; for example, CREATEDB (can create databases) and NOCREATEDB (not allowed to create databases).

A role with the LOGIN attribute is considered a user role. A role with NOLOGIN cannot connect to the server and is typically used for grouping other roles.

The table lists only some of the possible attributes.

https://postgrespro.com/docs/postgresql/16/role-attributes

https://postgrespro.com/docs/postgresql/16/sql-createrole

Page 4: Roles and Attributes

Create a role for the user Alice. Two attributes are specified.

Note

Note Note which role the commands are executed as. The name of the current role is in the prompt.

student=# CREATE ROLE alice LOGIN PASSWORD 'alice';
CREATE ROLE

You can get the list of roles with the command:

student=# \du
                             List of roles
 Role name |                         Attributes
-----------+------------------------------------------------------------
 alice     |
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS
 student   | Superuser, Create role, Create DB, Replication, Bypass RLS
Note

Note Note that the student role is a superuser. This is why we did not have any access issues so far. Create a database:

student=# CREATE DATABASE access_overview;
CREATE DATABASE
student=# \c access_overview
You are now connected to database "access_overview" as user "student".

Page 5: Connecting to a Server

1. The lines of pg\_hba.conf are searched from top to bottom

    1. The first line that corresponds to the provided connection settings (type, database, user, address) will be used
# TYPE        DATABASE     USER             ADDRESS           METHOD
local         all          postgres                           peer
local         all          all                                peer
host          all          all              127.0.0.1/32      scram-sha-256
host          all          all              ::1/128           scram-sha-256
  • local — socket all — any role host — TCP/IP role name
    • listen_addresses
    • all — any DB
    • all — any IP
    • database name
    • IP/mask
    • domain name

Speaker notes

For each new client, the server has to evaluate whether a database connection should be allowed. Connection settings are defined in the pg_hba.conf configuration file (hba stands for host-based authentication). As with the main configuration file (postgresql.conf), changes come into effect only after the server reloads this file, either via the pg_reload_conf() SQL function or the reload command of the management utility.

When a new client appears, the server reads the configuration file from top to bottom to find the line that matches the requested connection. The match is defined by four fields: connection type, database name, user name, and IP address.

Here we list only the main basic options.

Connection: local (unix sockets) or host (a TCP/IP connection).

Database: all (corresponds to any database) or the name of a particular database.

User: all or the name of a particular role.

Address: all, IP address range, or a domain name. The address is omitted for the local connection type. By default, PostgreSQL listens only for connections coming from localhost; the listen_addresses parameter is usually set to * (listen on all interfaces), while the access is controlled using pg_hba.conf settings.

https://postgrespro.com/docs/postgresql/16/client-authentication

Page 6: Connecting to a Server

3. The server performs authentication using the chosen method

    1. If successful, access is allowed; otherwise, it is forbidden (if no rows match the given parameters, access is forbidden)
# TYPE        DATABASE     USER             ADDRESS           METHOD
local         all          postgres                           peer
local         all          all                                peer
host          all          all              127.0.0.1/32      scram-sha-256
host          all          all              ::1/128           scram-sha-256
  • trust — allow
  • reject — forbid
  • scram-sha-256 и md5 — request a password
  • peer — ask OS

Speaker notes

Once the server finds an appropriate line in the file, it performs client authentication using the method specified in this line, and checks for the LOGIN attribute and the CONNECT privilege. If everything is OK, the connection is allowed; otherwise, it is forbidden (other lines won’t be considered in this case).

If no appropriate line is found, the access is also forbidden.

Thus, more specific connection lines should precede more generic ones.

There are a lot of different authentication methods:

https://postgrespro.com/docs/postgresql/16/auth-methods

Here are some of the main ones.

The trust method allows connections unconditionally. If security is not a concern, you can specify the trust method and use all for all the other parameters; then all connections will be allowed.

The reject method, on the contrary, unconditionally forbids connections.

The scram-sha-256 method asks for a password and checks that the provided password matches the one stored in the system catalog of the database cluster. The md5 method is considered deprecated.

The peer method checks the name of the operating system user and allows connections on behalf of the database user with the same name (you can also define a different name mapping).

Page 7: Password Authentication

At the server side

  • the password is set when the role is created and can be changed later
  • a user that has no password won’t be able to connect
  • the password is stored in the pg_authid system catalog
  • Entering the password on the client
    • manually
    • using the PGPASSWORD environment variable
    • using the ~/.pgpass file (lines format: node:port:database:role:password)

Speaker notes

If password authentication is used, there must be a reference password stored for the user; otherwise the connection will be rejected.

Password hashes are stored in the pg_authid table in the system catalog.

The user can either enter the password manually, or automate password input using one of the following options.

First, the password can be set in the PGPASSWORD environment variable (on the client). However, it is inconvenient if you have to connect to several databases, and it is not recommended for security reasons.

The second option to store passwords on the client is to use the ~/.pgpass file. The access to this file must be allowed to its owner only (chmod 600), otherwise PostgreSQL will ignore it.

Page 8: Connection

In order for a role to connect to the database, it must have both the LOGIN attribute and the permission in the file pg_hba.conf. You can usually find it right beside the main configuration file:

student=# SHOW hba_file;
              hba_file
-------------------------------------
 /etc/postgresql/16/main/pg_hba.conf
(1 row)

And you can read it directly from SQL:

student=# SELECT type, database, user_name, address, auth_method
FROM pg_hba_file_rules();
 type  |   database    | user_name |  address  |  auth_method
-------+---------------+-----------+-----------+---------------
 local | {all}         | {all}     |           | trust
 host  | {all}         | {all}     | 127.0.0.1 | scram-sha-256
 host  | {all}         | {all}     | ::1       | scram-sha-256
 local | {replication} | {all}     |           | trust
 host  | {replication} | {all}     | 127.0.0.1 | scram-sha-256
 host  | {replication} | {all}     | ::1       | scram-sha-256
(6 rows)

(The contents may vary depending on the server build.) We will use a TCP/IP (host) connection to localhost. Such connection corresponds to the second string in the output, expecting password-based authentication. The role alice was created with a password right away, but the password can be changed at any time:

student=# ALTER ROLE alice PASSWORD 'alicepass';
ALTER ROLE

Attempt to connect using a connection string with all the info:

student$ psql 'host=localhost user=alice dbname=access_overview password=alicepass'
alice=> \conninfo
You are connected to database "access_overview" as user "alice" on host "localhost"
(address "127.0.0.1") at port "5432".
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)

Success!

Page 9: Privileges

Privileges define access rights of roles to objects

  • Tables and views
    • SELECT
    • read data
    • INSERT
    • insert rows
    • can be set per column
    • UPDATE
    • change rows
REFERENCES
 foreign key (for tables)
  • DELETE
  • delete rows
TRUNCATE
 empty (for tables)
TRIGGER
 create triggers

Speaker notes

Privileges establish a relation between subjects (roles) and objects in the cluster. They determine the actions that roles can perform with these objects.

There are different privileges available for different object types. This slide and the following one list privileges for basic database objects.

The widest choice of privileges is available for tables and views. Some of these privileges can be set not only at the table level, but also at the column level.

https://postgrespro.com/docs/postgresql/16/ddl-priv

https://postgrespro.com/docs/postgresql/16/sql-grant

Page 10: Privileges

Source slide page 10: Privileges{#source-figure-010 caption=“Source slide page 10: Privileges”}

Tablespaces, database, schemas

  • Sequences
    • SELECT
    • currval
    • UPDATE
    • nextval
    • setval
    • USAGE
    • currval
    • nextval

Speaker notes

Sequences have a somewhat unexpected set of privileges. They serve to allow or restrict access to the three control functions.

For tablespaces, there is a CREATE privilege that allows the creation of objects in this tablespace.

For databases, the CREATE privilege allows you to create schemas in this database, and for a schema, the CREATE privilege allows you to create objects in this schema.

Since the exact name of the schema for temporary objects is unknown in advance, the privilege to create temporary tables has been moved to the database level (TEMPORARY).

The USAGE schema privilege allows access to objects in this schema.

The CONNECT database privilege allows connection to this database.

Page 11: Role Categories

Superusers

  • full access to all objects, no checks performed
  • Owners
    • initially, all privileges on the object (can be revoked)
    • actions that are not regulated by privileges, such as deleting objects, granting
    • and revoking privileges, etc.
  • Other roles
    • access within the granted privileges

Speaker notes

Generally speaking, a role’s ability to access an object is defined by the role’s privileges. But it makes sense to single out three categories of roles and discuss them separately.

  1. Roles with the SUPERUSER attribute (superusers). These roles can do anything and ignore all access control checks.

  2. Object owner. Initially, this is the role that created the object, although it can be changed later. It’s not just the object creator role that becomes the owner, but also any other role included in it. The object owner gets the full range of privileges on this object.

Technically, these privileges can be revoked, but the owner always retains inherent rights on the actions that are not regulated by any privileges. In particular, the owner can grant and revoke privileges (including to and from themselves), delete the object, etc.

  1. All other roles have access to the object only as far as the privileges granted to them allow it.

To check if a role has the necessary privilege with respect to some object, you can use the has_*_privilege functions:

https://postgrespro.com/docs/postgresql/16/functions-info

Page 12: Privilege Management

Granting privileges

alice: GRANT privileges ON object TO bob;
  • alice
  • bob
  • privileges
  • on object
  • Revoking privileges
alice: REVOKE privileges ON object FROM bob;

Speaker notes

The right to grant and revoke privileges on an object belongs to the owner of that object (and the superuser).

The syntax of the GRANT and REVOKE commands is quite complex. You can specify both individual and all possible privileges, both individual objects and groups of objects included in certain schemas, etc.

https://postgrespro.com/docs/postgresql/16/sql-grant

https://postgrespro.com/docs/postgresql/16/sql-revoke

Page 13: Privileges

Source slide page 13: Privileges{#source-figure-013 caption=“Source slide page 13: Privileges”}

Alice has connected to the database. Now she wants to create a schema and some objects.

alice=> CREATE SCHEMA alice;
ERROR:  permission denied for database access_overview

What’s the issue? Alice does not have the privilege to create schemas in the database. Grant it:

student=# GRANT CREATE ON DATABASE access_overview TO alice;
GRANT

Try again:

alice=> CREATE SCHEMA alice;
CREATE SCHEMA

Now, since Alice is the owner of the schema, she has all the privileges on it and can create any objects in it. This schema will be used by default:

alice=> SELECT current_schemas(false);
 current_schemas
-----------------
 {alice,public}
(1 row)

Alice creates two tables.

alice=> CREATE TABLE t1(n numeric);
CREATE TABLE
alice=> INSERT INTO t1 VALUES (1);
INSERT 0 1
alice=> CREATE TABLE t2(n numeric, who text DEFAULT current_user);
CREATE TABLE
alice=> INSERT INTO t2(n) VALUES (1);
INSERT 0 1

The superuser creates another role for the user Bob, who will access objects belonging to Alice.

student=# CREATE ROLE bob LOGIN PASSWORD 'bobpass';
CREATE ROLE
student$ psql 'host=localhost user=bob dbname=access_overview password=bobpass'

Bob attempts to access the table t1:

bob=> SELECT * FROM alice.t1;
ERROR:  permission denied for schema alice
LINE 1: SELECT * FROM alice.t1;
                      ^

What happened? Bob is neither a superuser nor the owner of this schema, so access is denied. This command lists current access rights (Access privileges column):

alice=> \x \dn+ \x

Page 14: Page 14

Source slide page 14: Page 14{#source-figure-014 caption=“Source slide page 14: Page 14”}

Expanded display is on.
List of schemas
-[ RECORD 1 ]-----+---------------------------------------
Name              | alice
Owner             | alice
Access privileges |
Description       |
-[ RECORD 2 ]-----+---------------------------------------
Name              | public
Owner             | pg_database_owner
Access privileges | pg_database_owner=UC/pg_database_owner+
                  | =U/pg_database_owner
Description       | standard public schema
Expanded display is off.

Privileges are displayed in the format: role=privileges/granted_by. Each privilege is encoded with a single character. In particular, for schemas: U = usage; C = create. If the role name is omitted (as in the last line), the pseudorole public is implied. Note that the public pseudorole has only USAGE privilege on the public schema. Here, pg_database_owner is the owner of the database. If the entire field is omitted (as in the first line), then the default privileges are implied: Alice gets both available privileges on her schema, and the other roles do not get any. Grant Bob access to the schema. Alice can do that as the owner.

alice=> GRANT CREATE, USAGE ON SCHEMA alice TO bob;
GRANT

Bob attempts to access the table t1 again:

bob=> SELECT * FROM alice.t1;
ERROR:  permission denied for table t1

Another error. What happened now? This time, Bob has access to the schema, but not to the table itself. This command shows who has access to the table:

alice=> \dp alice.t1
                            Access privileges
 Schema | Name | Type  | Access privileges | Column privileges | Policies
--------+------+-------+-------------------+-------------------+----------
 alice  | t1   | table |                   |                   |
(1 row)

The field is empty: only the owner, Alice, has access. Alice grants Bob read and update access:

alice=> GRANT SELECT, UPDATE ON alice.t1 TO bob;
GRANT

And read and insert rights for one column in the second table:

alice=> GRANT SELECT(n), INSERT ON alice.t2 TO bob;
GRANT

The privileges have changed:

alice=> \dp alice.*
                             Access privileges
 Schema | Name | Type  |  Access privileges  | Column privileges | Policies
--------+------+-------+---------------------+-------------------+----------
 alice  | t1   | table | alice=arwdDxt/alice+|                   |
        |      |       | bob=rw/alice        |                   |
 alice  | t2   | table | alice=arwdDxt/alice+| n:               +|
        |      |       | bob=a/alice         |   bob=r/alice     |
(2 rows)

Now the empty field has “appeared” and contains a complete list of privileges. Below are the designations, not all of them are quite

Page 15: Page 15

Source slide page 15: Page 15{#source-figure-015 caption=“Source slide page 15: Page 15”}

obvious: a = insert r = select w = update d = delete D = truncate x = reference t = trigger Privileges for columns are displayed separately (under Column privileges). This time Bob is successful. He adds the schema name to his search path, so that he does not have to type it in every time.

bob=> ALTER ROLE bob SET search_path = public, alice;
ALTER ROLE

Now, the search path will be set in each of Bob’s sessions.

bob=> \c
You are now connected to database "access_overview" as user "bob".
bob=> SHOW search_path;
  search_path
---------------
 public, alice
(1 row)
bob=> UPDATE t1 SET n = n + 1;
UPDATE 1
bob=> SELECT * FROM t1;
 n
---
 2
(1 row)

However, all other operations remain restricted.

bob=> DELETE FROM t1;
ERROR:  permission denied for table t1

And the second table:

bob=> INSERT INTO t2(n) VALUES (100);
INSERT 0 1
bob=> SELECT n FROM t2;
  n
-----
   1
 100
(2 rows)

Reading of the other column is not permitted:

bob=> SELECT * FROM t2;
ERROR:  permission denied for table t2

Page 16: Granting Membership

Source slide page 16: Granting Membership{#source-figure-016 caption=“Source slide page 16: Granting Membership”}

public Granting membership

alice: GRANT dba TO bob;
  • alice
  • bob
  • dba
  • public pseudo-role implicitly includes
  • all other roles
  • Revoking membership
alice: REVOKE dba FROM bob;

Speaker notes

Any role can include other roles as members. In this case, the role acts as a group. PostgreSQL does not have a separate “group” entity.

A role can be a member of multiple roles; in turn, a member role may include other roles, but circular dependencies are not permitted.

By default, a role inherits the privileges of any group roles it is a member of. This behavior can be changed by using the NOINHERIT attribute for the role. This makes the user have to explicitly switch to the group role using SET ROLE in order to use its privileges. Role attributes are not inherited, but it is possible to switch to a parent role to use its attributes.

Roles that include other roles typically have the NOLOGIN attribute and are referred to as “group roles”. We can think of a group role as a predefined set of privileges that can be granted to a role just like any regular privilege. This simplifies access control and administration.

There is also a pseudorole called public, which implicitly includes all the other roles. Any privilege granted to the public role is automatically granted to all the other roles as well.

https://postgrespro.com/docs/postgresql/16/role-membership

Page 17: Predefined Roles

pg_monitor

  • pg_read_all_settings read all server parameters
  • pg_read_all_stats access statistics pg_stat_scan_tables monitoring and locking tables
  • pg_read_all_data read data in all tables pg_write_all_data change data in all tables
  • pg_read_server_files read files on the server pg_write_server_files write to files on the server
  • pg_execute_server_programs run programs on the server
  • …

Speaker notes

PostgreSQL provides a number of predefined roles that have access to some frequently used, but privileged capabilities and data. Membership in these roles may be granted to users in order to facilitate administrative and maintenance tasks without providing them superuser capabilities.

The list of predefined roles increases with each PostgreSQL version. The full list of all the roles, including predefined ones, cat be seen by \duS command in psql.

https://postgrespro.com/docs/postgresql/16/predefined-roles

You can create your own administrative group roles, e.g. for managing backups.

Page 18: Group Roles

Source slide page 18: Group Roles{#source-figure-018 caption=“Source slide page 18: Group Roles”}

Bob could not read the second column:

bob=> SELECT * FROM t2;
ERROR:  permission denied for table t2

The superuser includes Bob into the predefined group role pg_read_all_data:

student=# GRANT pg_read_all_data TO bob;
GRANT ROLE

Now Bob can read all tables as if he had been granted SELECT privileges for all tables and USAGE privileges for all schemas:

bob=> SELECT * FROM t2;
  n  |  who
-----+-------
   1 | alice
 100 | bob
(2 rows)

You can use the command \drg in psql to get role membership information:

student=# \drg
                  List of role grants
 Role name |    Member of     |   Options    | Grantor
-----------+------------------+--------------+----------
 bob       | pg_read_all_data | INHERIT, SET | postgres
(1 row)

The Options column shows attributes for the membership. For Bob as a member of pg_read_all_data, SET means the right to switch to the group role, and INHERIT means that Bob can use the group privileges without explicitly switching to the group. Exclude Bob from the group:

student=# REVOKE pg_read_all_data FROM bob;
REVOKE ROLE

Page 19: Routines

The only privilege for functions and procedures

  • EXECUTE
  • execution
  • Security modes
    • SECURITY INVOKER
    • executed with the privileges of the caller
    • (by default)
    • SECURITY DEFINER
    • executed with the privileges of the owner

Speaker notes

The privilege EXECUTE, the only privilege for functions and procedures, allows them to execute.

The user on behalf of which the routine is executed is important. If a routine is declared as a SECURITY INVOKER (by default), it is executed with the rights of the user that runs it. In this case, the statements inside the routine can access only those objects that are accessible to the calling user.

On the other hand, if declared with the SECURITY DEFINER, the routine will use the rights of its owner. This is a way to allow certain users perform certain actions on objects they personally have no access to.

https://postgrespro.com/docs/postgresql/16/sql-createfunction

https://postgrespro.com/docs/postgresql/16/sql-createprocedure

Page 20: Default Privileges

Privileges of the public pseudo-role

  • connect to any database
  • access to the system catalog
  • execution of any routines
  • privileges are granted automatically on each new object
  • Configurable default privileges
    • the possibility to additionally grant or revoke privileges on a newly created
    • object

Speaker notes

As we have already said, the public pseudo-role includes all other roles, so they inherit all the privileges granted to public.

And public has quite an extensive list of privileges by default. In particular:

● The right to connect to any database (that’s why the role alice could connect to the database although the CONNECT privilege had not been explicitly granted to this role)

● Access to the system catalog

● Execution of any routines

On the one hand, it enables seamless operation without having to deal with privileges; but on the other hand, it brings extra complications if access control is really required.

The public role automatically receives all the privileges listed above for all newly created objects. So it is not enough to simply revoke the EXECUTE privilege from public: once a new routine appears, public immediately gets the right to execute it.

There is a special mechanism of default privileges that enables you to automatically grant and revoke the required privileges on newly created objects. It can be also used to revoke the EXECUTE privilege from the public pseudo-role.

https://postgrespro.com/docs/postgresql/16/sql-alterdefaultprivileges

Page 21: Configurable Default Privileges

Source slide page 21: Configurable Default Privileges{#source-figure-021 caption=“Source slide page 21: Configurable Default Privileges”}

Alice creates a function:

alice=> CREATE FUNCTION foo() RETURNS SETOF t2
AS $$
SELECT * FROM t2;
$$ LANGUAGE sql STABLE;
CREATE FUNCTION

Can Bob execute the function without Alice granting him the EXECUTE privilege?

bob=> SELECT foo();
ERROR:  permission denied for table t2
CONTEXT:  SQL function "foo" statement 1

Bob can call the function, but still cannot access any objects for which he does not have the appropriate privileges. If Bob creates a table t2 in the schema public, the function will work for both users but will actually access different tables, since Alice and Bob have different search paths. In order for Bob to create a table in the schema public, he must have the CREATE privilege for the schema (in PostgreSQL 15+):

student=# GRANT CREATE ON SCHEMA public TO bob;
GRANT
bob=> CREATE TABLE t2(n numeric, who text DEFAULT current_user);
CREATE TABLE
bob=> INSERT INTO t2(n) VALUES (42);
INSERT 0 1
bob=> SELECT foo();
   foo
----------
 (42,bob)
(1 row)
alice=> SELECT foo();
    foo
-----------
 (1,alice)
 (100,bob)
(2 rows)

Alternatively, the function can be declared as using the privileges of its owner:

alice=> ALTER FUNCTION foo() SECURITY DEFINER;
ALTER FUNCTION

In this case, the function will always be executed with the privileges Alice has, regardless of who actually calls it. Bob deletes his table…

bob=> DROP TABLE t2;
DROP TABLE

…and gets access to the Alice’s table:

bob=> SELECT foo();
    foo
-----------
 (1,alice)
 (100,bob)
(2 rows)

In this situation, Alice needs to keep an eye on the granted privileges. For one, she should revoke the EXECUTE privilege from the role public and explicitly grant it only to the roles that should have it.

Page 22: Page 22

Source slide page 22: Page 22{#source-figure-022 caption=“Source slide page 22: Page 22”}

alice=> REVOKE EXECUTE ON ALL ROUTINES IN SCHEMA alice FROM public;
REVOKE
bob=> SELECT foo();
ERROR:  permission denied for function foo

To make matters worse, for each new function, the EXECUTE privilege is always granted to public by default. You can configure default privileges so that specific users would get (or lose) specific privileges on newly created objects:

alice=> ALTER DEFAULT PRIVILEGES
FOR ROLE alice
REVOKE EXECUTE ON ROUTINES FROM public;
ALTER DEFAULT PRIVILEGES
alice=> ALTER DEFAULT PRIVILEGES
FOR ROLE alice
GRANT EXECUTE ON ROUTINES TO bob;
ALTER DEFAULT PRIVILEGES
alice=> \ddp
           Default access privileges
 Owner | Schema |   Type   | Access privileges
-------+--------+----------+-------------------
 alice |        | function | alice=X/alice    +
       |        |          | bob=X/alice
(1 row)

Now, Bob is immediately granted execute privileges on routines created by Alice, while other users will not have permission to run them.

alice=> CREATE FUNCTION bar() RETURNS integer
LANGUAGE sql IMMUTABLE SECURITY DEFINER
RETURN 1;
CREATE FUNCTION
bob=> SELECT bar();
 bar
-----
   1
(1 row)

Page 23: Takeaways

Roles, attributes and privileges together form a flexible mechanism that allows you to set up access control in different ways

  • can grant all access widely
    • can restrict access heavily if necessary
  • When creating a new role, you need to ensure that it can connect to the server

Page 24: Practice

Set up privileges so that some users have full access to the tables, while others can only query, but not modify the data.

    1. Create a new database and two roles: writer and reader. 2. Revoke all privileges for the public schema from the public role, grant both privileges to the writer role, and only the usage privilege to the reader role. 3. Set up the default privileges so that the reader role gets read access to the tables owned by writer in the public schema. 4. Create user w1 as a member of the writer role, and user r1 as a member of the reader role. 5. As writer, create a table. 6. Verify that r1 has read-only access to the table, and w1 has full access to it, including the ability to remove it.

Page 25: 1. Databases and Roles

=> CREATE DATABASE access_overview;
CREATE DATABASE
=> CREATE USER writer;
CREATE ROLE
=> CREATE USER reader;
CREATE ROLE

2. Privileges

=> \c access_overview
You are now connected to database "access_overview" as user "student".
=> REVOKE ALL ON SCHEMA public FROM public;
REVOKE
=> GRANT ALL ON SCHEMA public TO writer;
GRANT
=> GRANT USAGE ON SCHEMA public TO reader;
GRANT

3. Default Privileges

=> ALTER DEFAULT PRIVILEGES
FOR ROLE writer
IN SCHEMA public
GRANT SELECT ON TABLES TO reader;
ALTER DEFAULT PRIVILEGES

4. Users

Writing role:

=> CREATE ROLE w1 LOGIN IN ROLE writer;
CREATE ROLE

The IN ROLE construct immediately includes the new role into the specified one. That is, this command is equivalent to these two:

CREATE ROLE w1 LOGIN;
GRANT writer TO w1;

Reading role:

=> CREATE ROLE r1 LOGIN IN ROLE reader;
CREATE ROLE

5. Table

=> \c - writer
You are now connected to database "access_overview" as user "writer".
=> CREATE TABLE t(n integer);
CREATE TABLE

6. Verification

w1 can insert rows:

=> \c - w1

Page 26: Page 26

You are now connected to database "access_overview" as user "w1".
=> INSERT INTO t VALUES (42);
INSERT 0 1

r1 can read the table:

=> \c - r1
You are now connected to database "access_overview" as user "r1".
=> SELECT * FROM t;
 n
----
 42
(1 row)

But cannot modify it:

=> UPDATE t SET n = n + 1;
ERROR:  permission denied for table t

w1 can delete the table:

=> \c - w1
You are now connected to database "access_overview" as user "w1".
=> DROP TABLE t;
DROP TABLE

In PostgreSQL 14+, the predefined role pg_read_all_data was added, which automatically provides read access to all data. Only the database owner or a superuser can drop the database:

=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
=> DROP DATABASE access_overview;
DROP DATABASE

Page 27: Practice+

  1. Register the user roles alice and bob. 2. Modify the pg_hba.conf file to allow access without password only for the postgres and student users, ensuring that access for alice and bob remains restricted. 3. Enable peer authentication method for alice and bob. Check that connection attempts fail without OS user mapping. Create such mapping for alice. 4. Check the capability to use the same OS user mapping for different database roles.

Speaker notes

  1. Using a text editor, insert the new entry before the first uncommented directive in pg_hba.conf. local all postgres,student trust

Reload configuration.

  1. Modify the inserted directive in pg_hba.conf by replacing it with the following: local all postgres,student trust local all alice,bob peer

Append the following to the end of the pg_ident.conf file: stmap student alice

  1. For alice and bob roles to share a single mapping, the pg_ident.conf file shall be as follows: stmap student alice stmap student bob

Page 28: 1. Add Roles

Register roles with the permission to log in.

=> CREATE ROLE alice LOGIN;
CREATE ROLE
=> CREATE ROLE bob LOGIN;
CREATE ROLE

2. Limit the use of trust

Edit the contents of pg_hba.conf, allowing the trust method only for postgres and student.

student$ sudo sed -i 's/^local.*all.*all.*trust.*$/local all postgres,student trust\n/'
/etc/postgresql/16/main/pg_hba.conf

This is what we get:

=> SELECT type,database,user_name,address,auth_method,error
FROM pg_hba_file_rules
ORDER BY rule_number;
 type  |   database    |     user_name      |  address  |  auth_method  | error
-------+---------------+--------------------+-----------+---------------+-------
 local | {all}         | {postgres,student} |           | trust         |
 host  | {all}         | {all}              | 127.0.0.1 | scram-sha-256 |
 host  | {all}         | {all}              | ::1       | scram-sha-256 |
 local | {replication} | {all}              |           | trust         |
 host  | {replication} | {all}              | 127.0.0.1 | scram-sha-256 |
 host  | {replication} | {all}              | ::1       | scram-sha-256 |
(6 rows)

Reload the configuration.

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Now, neither alice nor bob can connect.

student$ psql -l -U alice
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  no pg_hba.conf entry for host "[local]", user "alice", database "postgres", no
encryption
student$ psql -l -U bob
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  no pg_hba.conf entry for host "[local]", user "bob", database "postgres", no
encryption

3. Peer Authentication Method

Using a text editor, add another line with the peer authentication method to allow alice and bob to connect.

student$ sudo sed -i '/^local.*all.*postgres,student.*$/alocal all alice,bob peer'
/etc/postgresql/16/main/pg_hba.conf

Contents of pg_hba.conf:

=> SELECT type,database,user_name,address,auth_method,error
FROM pg_hba_file_rules
ORDER BY rule_number;

Page 29: Page 29

 type  |   database    |     user_name      |  address  |  auth_method  | error
-------+---------------+--------------------+-----------+---------------+-------
 local | {all}         | {postgres,student} |           | trust         |
 local | {all}         | {alice,bob}        |           | peer          |
 host  | {all}         | {all}              | 127.0.0.1 | scram-sha-256 |
 host  | {all}         | {all}              | ::1       | scram-sha-256 |
 local | {replication} | {all}              |           | trust         |
 host  | {replication} | {all}              | 127.0.0.1 | scram-sha-256 |
 host  | {replication} | {all}              | ::1       | scram-sha-256 |
(7 rows)

Reload the configuration.

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

The users still cannot log in, but the error message is now different.

student$ psql -l -U alice
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  Peer authentication failed for user "alice"
student$ psql -l -U bob
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  Peer authentication failed for user "bob"

The peer authentication method requires the user names in the OS and roles in PostgreSQL to match. Map the role alice to the OS user student by adding a line to pg_ident.conf. Do not set up the mapping for bob, though.

student$ echo 'stmap student alice' | sudo tee -a /etc/postgresql/16/main/pg_ident.conf
stmap student alice

Add the parameter map to the line to set the mapping.

student$ sudo sed -i 's/peer.*$/peer map=stmap/' /etc/postgresql/16/main/pg_hba.conf

Contents of pg_hba.conf:

=> SELECT type,database,user_name,address,auth_method,options,error
FROM pg_hba_file_rules
ORDER BY rule_number;
 type  |   database    |     user_name      |  address  |  auth_method  |   options   |
error
-------+---------------+--------------------+-----------+---------------+-------------+---
----
 local | {all}         | {postgres,student} |           | trust         |             |
 local | {all}         | {alice,bob}        |           | peer          | {map=stmap} |
 host  | {all}         | {all}              | 127.0.0.1 | scram-sha-256 |             |
 host  | {all}         | {all}              | ::1       | scram-sha-256 |             |
 local | {replication} | {all}              |           | trust         |             |
 host  | {replication} | {all}              | 127.0.0.1 | scram-sha-256 |             |
 host  | {replication} | {all}              | ::1       | scram-sha-256 |             |
(7 rows)

Reload the configuration.

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Now alice can connect to the database and execute commands.

student$ psql -c '\conninfo' -U alice -d student
You are connected to database "student" as user "alice" via socket in
"/var/run/postgresql" at port "5432".

And bob cannot.

student$ psql -c '\conninfo' -U bob -d student

Page 30: 4. One Mapping for Multiple Roles

psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  Peer authentication failed for user "bob"

Allow bob to connect under the same conditions as alice.

student$ echo 'stmap student bob' | sudo tee -a /etc/postgresql/16/main/pg_ident.conf
stmap student bob

Lines added to pg_ident.conf:

student$ sudo tail -n2 /etc/postgresql/16/main/pg_ident.conf
stmap student alice
stmap student bob

Reload the configuration.

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Now both alice and bob can connect.

student$ psql -c '\conninfo' -U alice -d student
You are connected to database "student" as user "alice" via socket in
"/var/run/postgresql" at port "5432".
student$ psql -c '\conninfo' -U bob -d student
You are connected to database "student" as user "bob" via socket in "/var/run/postgresql"
at port "5432".

14 Lesson 14: Backup / Overview

Native Markdown source page: Backup / Overview
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_14_backup_overview.html Archive: DBA1-handouts-16-en.zip PDF pages: 36

Page 1: Backup / Overview

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Logical Backup
  • Physical Backup

Page 3: Logical Backup

What Is Logical Backup

  • Table Backup
  • Database Backup
  • Cluster Backup

Page 4: Logical Backup

SQL commands to restore data from scratch

    • can backup a separate object or database
    • can recover on a cluster running different major version
    • can recover on a different architecture
  • − low speed
  • − can restore to the moment of the backup only

Speaker notes

There are two types of backup: logical and physical.

Logical backup is a set of SQL commands that restores a cluster (or database, or a separate object) from scratch.

Such a backup is, in fact, a plain text file, which gives a certain flexibility. For example, you can make a copy of only those objects that are needed; you can edit the file by changing the names or data types, etc.

In addition, SQL commands can be executed on a different version of the DBMS or on a different architecture. Only compatibility at the command level is required, binary compatibility is not necessary.

However, for a large database, this mechanism is inefficient, since executing the commands (in particular, creation of indexes) will take a long time. Moreover, it is possible to restore the system from such a backup only to the moment at which the backup was made.

https://postgrespro.com/docs/postgresql/16/backup-dump

Page 5: COPY: Table Backup

Backup

  • output of a table contents or a query results into a file, stream or program
  • Restore
    • insertion of rows from a file or input stream into an existing table
  • Server variant Client variant
    • SQL COPY command
    • psql \COPY command
    • the file must be accessible
    • the file must be accessible
    • to the postgres user
    • to the user who has launched psql
    • on the server
    • on the client

Speaker notes

If you want to save only the contents of one table, you can use the COPY command.

The command writes a table (or the result of an arbitrary query) either to a file or to an output stream, or sends it as input to another program. You can specify options such as format (plain text, csv or binary), field separator, NULL string representaion, etc.

The opposite variant of the COPY command reads fields from a file or input stream and inserts them into a table. The table isn’t cleared, the new rows are simply appended to the existing ones.

The COPY command is significantly faster than similar INSERT commands, because the client does not need to access the server repeatedly, and the server does not have to analyze the commands multiple times.

https://postgrespro.com/docs/postgresql/16/sql-copy

In psql, there is a client version of the COPY command with a similar syntax. Unlike the server version, which is an SQL command, the client version is a psql command.

The file name in the SQL command corresponds to a file on the database server. The user running PostgreSQL (usually postgres) must have access to this file. In the client version, the file is accessed on the client, and only the content is transmitted to the server.

https://postgrespro.com/docs/postgresql/16/app-psql

Page 6: COPY

Create a database and a table in it.

=> CREATE DATABASE backup_overview;
CREATE DATABASE
=> \c backup_overview
You are now connected to database "backup_overview" as user "student".
=> CREATE TABLE t(id numeric, s text);
CREATE TABLE
=> INSERT INTO t VALUES (1, 'Hello World!'), (2, ''), (3, NULL);
INSERT 0 3
=> SELECT * FROM t;
 id |      s
----+--------------
  1 | Hello World!
  2 |
  3 |
(3 rows)

Here is how the COPY command outputs the table:

=> COPY t TO STDOUT;
1
Hello World!
2
3
\N
Note

Note Note that an empty string and NULL are different things, despite the output not telling us that. You can input the data in a similar way:

=> TRUNCATE TABLE t;
TRUNCATE TABLE
=> COPY t FROM STDIN;
1
Hi there!
2
3
\N
\.
COPY 3

Let’s check:

=> \pset null '<null>'
Null display is "<null>".
=> SELECT * FROM t;
 id |     s
----+-----------
  1 | Hi there!
  2 |
  3 | <null>
(3 rows)

Page 7: pg_dump: Database Backup

Backup

  • outputs an SQL script or an archive
  • in a special format with a TOC to a stream or file
  • supports parallel execution
  • can define what objects to backup
  • (tables, schemas, only DML or only DDL, etc.)
  • Restore
    • SQL script via psql
    • archive with TOC via pg_restore
    • (can define what objects to restore and supports parallel execution)
    • the new database must be created from template0
    • roles and tablespaces must be created in advance

Speaker notes

The pg_dump utility creates a full-scale database backup. Depending on the options, it provides either an SQL script containing commands that create the required objects, or a file in a special format with a table of contents.

Restoring from an SQL script is as simple as executing it in psql.

https://postgrespro.com/docs/postgresql/16/app-pgdump

Restoring from an archive is done using the pg_restore tool. It reads the file and translates it into regular psql commands. The advantage is that it allows you to specify what objects to restore at the recovery stage, not just at the backup stage. Moreover, this type of backup and restore supports parallel execution.

https://postgrespro.com/docs/postgresql/16/app-pgrestore

The database for restore must be created from the database template0, since all changes made in template1 will also be backed up. In addition, the necessary roles and tablespaces must be created in advance, since these objects belong to the entire cluster. After restore, it is recommended to run the ANALYZE command to collect fresh statistics.

Page 8: pg_dumpall: Cluster Backup

Backup

  • makes a backup of the entire cluster, including roles and tablespaces
  • outputs an SQL script to the console or to a file
  • parallel execution is not supported, but you can dump only the global objects
  • and then use pg_dump
  • Restore
    • via psql

Speaker notes

pg_dumpall creates a backup of the entire cluster, including roles and tablespaces.

Since pg_dumpall requires access to all objects of all databases, it is usually run by the superuser. pg_dumpall connects to each database in the cluster one by one and backups them using pg_dump. In addition, it also stores data related to the cluster as a whole.

The result of pg_dumpall is a script for psql. Other formats are not supported. This means that pg_dumpall does not support parallel execution, which can be a problem for larger clusters. In this case, you can use the –globals-only option to backup only roles and tablespaces, and then backup all the databases using pg_dump.

https://postgrespro.com/docs/postgresql/16/app-pg-dumpall

Page 9: pg_dump Utility

Take a look at pg_dump output in a plain text format. Note the way data from the table is saved. If any changes were made to template1, they will make it into the backup, too. Therefore, when restoring a database, it is best to create one on the target from template0 (the –create option adds the necessary commands automatically).

student$ pg_dump -d backup_overview --create
--
-- PostgreSQL database dump
--
\restrict fWyzTQVdORhS0iq3MfwdQow0Q2Gmy0XG18P2BuznZt3uqsAI7siXWH3OhWbXUKt
-- Dumped from database version 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
-- Dumped by pg_dump version 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: backup_overview; Type: DATABASE; Schema: -; Owner: student
--
CREATE DATABASE backup_overview WITH TEMPLATE = template0 ENCODING = 'UTF8'
LOCALE_PROVIDER = libc LOCALE = 'en_US.UTF-8';
ALTER DATABASE backup_overview OWNER TO student;
\unrestrict fWyzTQVdORhS0iq3MfwdQow0Q2Gmy0XG18P2BuznZt3uqsAI7siXWH3OhWbXUKt
\connect backup_overview
\restrict fWyzTQVdORhS0iq3MfwdQow0Q2Gmy0XG18P2BuznZt3uqsAI7siXWH3OhWbXUKt
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: t; Type: TABLE; Schema: public; Owner: student
--
CREATE TABLE public.t (
    id numeric,
    s text
);
ALTER TABLE public.t OWNER TO student;
--
-- Data for Name: t; Type: TABLE DATA; Schema: public; Owner: student
--
COPY public.t (id, s) FROM stdin;
1
Hi there!

Page 10: Page 10

2
3
\N
\.
--
-- PostgreSQL database dump complete
--
\unrestrict fWyzTQVdORhS0iq3MfwdQow0Q2Gmy0XG18P2BuznZt3uqsAI7siXWH3OhWbXUKt

As an example, let’s copy the table into another database.

=> CREATE DATABASE backup_overview2;
CREATE DATABASE
student$ pg_dump -d backup_overview --table=t | psql -d backup_overview2
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
COPY 3
student$ psql -d backup_overview2
=> SELECT * FROM t;
 id |     s
----+-----------
  1 | Hi there!
  2 |
  3 |
(3 rows)

Page 11: Physical Backup

What Is Physical Backup

  • Cold and Hot Backups
  • Replication Protocol
  • Standalone Backup
  • Continuous WAL Archiving

Page 12: Physical Backup

Crash recovery mechanism is used: copy of data and WAL

    • restore speed
    • can restore a cluster to a certain point in time
  • − cannot restore a separate database, only the cluster as a whole
  • − can restore only on the same architecture and major version

Speaker notes

Physical backup uses the crash recovery mechanism. This requires:

● base backup — a copy of cluster files (data files and system files),

● a set of write-ahead logs needed to restore consistency.

If the file system is already consistent (the backup was made when the server was stopped correctly), then the WALs are not required.

However, with the WAL archive, it is possible to get cluster state at any point in time. This way, the cluster can be restored to the state right before the crash (or at any moment before that, if needed).

High restore speed and the ability to create a backup on the fly without stopping the server make physical backup the main choice for routine backup needs.

https://postgrespro.com/docs/postgresql/16/backup-file

https://postgrespro.com/docs/postgresql/16/continuous-archiving

Page 13: Hot or Cold

  • Cold backup
  • Hot backup
  • the server is off
  • the server was shut
  • the server is
  • Cluster files are
  • backed up when…
  • down incorrectly
  • running
  • special means
  • required
  • WALs are…
  • not required
  • required since the
  • required for the
  • duration of the file
  • last checkpoint
  • copy
  • stored in
  • the file system
  • server
  • must not delete WALs
  • too early

Speaker notes

Physical backup creates a copy of the database cluster files at some point.

If a backup is created while the server is stopped, it’s called “cold”. A cold backup either contains consistent data (if the server was shut down correctly), or contains all the logs necessary for recovery (for example, if the OS has done a data snapshot). This simplifies restore, but requires that the server is stopped.

If a backup is created while the server is running (which requires certain additional actions since you can’t copy files just like that), it is called “hot”. The procedure is more complicated, but can be performed without stopping the server.

For hot backup, the copy of the cluster files will be inconsistent. However, the crash recovery mechanism can also be successfully applied to restore from backup. This will require the WALs for at least the duration of the file copy.

Page 14: Standalone Backup

Base copy + WAL

  • Backup via pg_basebackup
    • connects to the server over the replication protocol
    • performs a checkpoint
    • copies the database cluster files into a specified directory
    • saves all WAL segments generated during the copying process
  • Restore
    • deploy the standalone backup
    • start the server

Speaker notes

Hot backups are created with the pg_basebackup tool.

First, it performs a checkpoint. Then, the cluster files are copied.

All WAL files generated by the server during the time from the checkpoint to the end of file copying are also added to the backup. The resulting backup is called standalone because it contains all the data necessary for recovery.

All you need to restore using a standalone backup is to deploy the backup and start the server. It will use the WALs to restore consistency on startup if necessary, and will be ready to go.

https://postgrespro.com/docs/postgresql/16/app-pgbasebackup

Page 15: Replication Protocol

Protocol

  • receiving the WAL stream
  • backup and replication control commands
  • Served by wal_sender process
  • wal_level = replica
  • Replication slot
    • a server object for receiving WAL records
    • remembers which record was read last
    • WAL segment is not deleted until fully read through the slot

Speaker notes

The replication protocol allows the utility to connect to the server and collect all WAL files generated during file copying. Despite the name, the protocol is used not only for replication (which will be discussed in the next lesson), but also for backup. The protocol can stream WAL records while data files are being copied.

To prevent the server from deleting WAL files too early, the replication slot can be employed, which keeps track of the last WAL record received by the client.

Establishing a connection over the replication protocol requires a certain configuration.

First, the initiating role must have the REPLICATION attribute (or be a superuser). This role must also have the necessary permission in the pg_hba.conf configuration file.

Second, the max_wal_senders parameter must be set sufficiently high. This parameter limits the number of simultaneously running wal_sender processes serving replication protocol connections.

Third, the wal_level parameter, which determines the amount of information in the WAL, must be set to replica.

The default settings already satisfy all these requirements (for a local connection).

https://postgrespro.com/docs/postgresql/16/protocol-replication

Page 16: Standalone Backup

Source slide page 16: Standalone Backup{#source-figure-016 caption=“Source slide page 16: Standalone Backup”}

Speaker notes

The image on the left shows the main server. It processes incoming queries. At the same time, WAL records are formed and the state of the databases changes (first in the buffer cache, then on disk). WAL segments are cyclically overwritten (to be precise, old segments are deleted, since the file names are unique).

At the bottom of the picture is a backup copy (usually located on another server). It contains a base copy of the data and a set of WAL files.

Page 17: Standalone Backup

The default configuration is sufficient for replication protocol:

=> SELECT name, setting
FROM pg_settings
WHERE name IN ('wal_level','max_wal_senders');
      name       | setting
-----------------+---------
 max_wal_senders | 10
 wal_level       | replica
(2 rows)

The local connection permission for the replication protocol is also on in pg_hba.conf by default (not for all package distributions):

=> SELECT type, database, user_name, address, auth_method
FROM pg_hba_file_rules()
WHERE 'replication' = ANY(database);
 type  |   database    | user_name |  address  |  auth_method
-------+---------------+-----------+-----------+---------------
 local | {replication} | {all}     | <null>    | trust
 host  | {replication} | {all}     | 127.0.0.1 | scram-sha-256
 host  | {replication} | {all}     | ::1       | scram-sha-256
(3 rows)

Another database cluster, replica, has been initiated on the port 5433. Ubuntu package installs a tool we can use to verify that the cluster is stopped:

student$ pg_lsclusters
Ver Cluster Port Status Owner    Data directory                 Log file
16  main    5432 online postgres /var/lib/postgresql/16/main
/var/log/postgresql/postgresql-16-main.log
16  replica 5433 down   postgres /var/lib/postgresql/16/replica
/var/log/postgresql/postgresql-16-replica.log

Create a backup. Use the default format (plain):

student$ rm -rf /home/student/tmp/basebackup
student$ pg_basebackup --pgdata=/home/student/tmp/basebackup --checkpoint=fast

The pg_basebackup utility performs a checkpoint immediately upon connecting to the server. By default, dirty buffers are written gradually to avoid I/O peak loads (the process may take up to 4.5 minutes). When using –checkpoint=fast, buffers are written without delays.

Page 18: Restore

Source slide page 18: Restore{#source-figure-018 caption=“Source slide page 18: Restore”}

Speaker notes

During restore, the base backup, including the necessary WAL files, is deployed, for example, on another server (shown on the right).

After the startup, it restores consistency and is ready to go. The system is restored to the point in time when the backup was made. Of course, the main server can go far ahead in the meantime.

Page 19: Restore

Move the new backup into replica cluster directory (after making sure the cluster is stopped):

student$ sudo pg_ctlcluster 16 replica status
pg_ctl: no server running
student$ sudo rm -rf /var/lib/postgresql/16/replica
student$ sudo mv /home/student/tmp/basebackup/ /var/lib/postgresql/16/replica

The cluster files must belong to the postgres user.

student$ sudo chown -R postgres:postgres /var/lib/postgresql/16/replica

Verify the contents:

student$ sudo ls -l /var/lib/postgresql/16/replica
total 344
-rw------- 1 postgres postgres    225 Nov 27 13:52 backup_label
-rw------- 1 postgres postgres 268206 Nov 27 13:52 backup_manifest
drwx------ 8 postgres postgres   4096 Nov 27 13:52 base
drwx------ 2 postgres postgres   4096 Nov 27 13:52 global
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_commit_ts
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_dynshmem
drwx------ 4 postgres postgres   4096 Nov 27 13:52 pg_logical
drwx------ 4 postgres postgres   4096 Nov 27 13:52 pg_multixact
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_notify
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_replslot
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_serial
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_snapshots
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_stat
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_stat_tmp
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_subtrans
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_tblspc
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_twophase
-rw------- 1 postgres postgres      3 Nov 27 13:52 PG_VERSION
drwx------ 3 postgres postgres   4096 Nov 27 13:52 pg_wal
drwx------ 2 postgres postgres   4096 Nov 27 13:52 pg_xact
-rw------- 1 postgres postgres     88 Nov 27 13:52 postgresql.auto.conf

When started, the cluster will begin restore.

student$ sudo pg_ctlcluster 16 replica start

Now both servers run concurrently and independently. Main server:

=> INSERT INTO t VALUES (4, 'Main server');
INSERT 0 1
=> SELECT * FROM t;
 id |      s
----+-------------
  1 | Hi there!
  2 |
  3 | <null>
  4 | Main server
(4 rows)

Server restored from the backup:

student$ psql -p 5433 -d backup_overview
=> INSERT INTO t VALUES (4, 'Backup');
INSERT 0 1
=> SELECT * FROM t;

Page 20: Page 20

 id |     s
----+-----------
  1 | Hi there!
  2 |
  3 |
  4 | Backup
(4 rows)

Page 21: WAL Archive

File archive

  • WAL segments are archived as they are switched
  • controlled by the server
  • archiving happens with a delay
  • Streaming archive
    • a stream of WAL records is continuously recorded into the archive
    • external tools required
    • delays are minimal

Speaker notes

The hot backup concept can be improved upon even further. Since we have a copy of the database cluster files and WALs, then by constantly saving new logs, we will be able to restore the system not only at the time of copying files, but also at any point in time after that.

There are two ways to go about it. The first is to archive old WAL files before deleting them. There are server settings for that. Unfortunately, with this option, a WAL file will not be archived until the server switches to writing to another WAL file.

The second way is to continuously read WAL entries using the replication protocol and write them into the same archive. This way, delays are minimal, but a separate tool has to be set up to receive the stream data and archive it.

Page 22: File WAL Archive

Archiver process

  • Parameters
    • archive_mode = on
    • archive_command
    • shell command to copy a WAL segment
    • to a separate storage
  • Algorithm
    • when switching to a new WAL segment, the archive_command is called
    • if the command is completed with the status 0, the segment is deleted
    • if the command returns anything else (or if the command is not specified),
    • the segment remains until the attempt is successful

Speaker notes

The file WAL archive is managed by the archiver background process.

An arbitrary shell command can be defined as the archive_command parameter to be used for copying. The mechanism itself is enabled by the archive_mode = on.

The algorithm goes as follows. When a WAL segment is filled, the copy command is called. If it is completed with a zero status, the segment can be deleted safely. Otherwise, the segment (and the ones following it) will not be deleted, and the server will periodically try to execute the command until it returns 0.

https://postgrespro.com/docs/postgresql/16/continuous-archiving

Page 23: File WAL Archive

Source slide page 23: File WAL Archive{#source-figure-023 caption=“Source slide page 23: File WAL Archive”}

Speaker notes

This figure shows the main server with continuous archiving set up. Filled WAL segments are copied to a separate archive using the command defined by the archive_command parameter. Usually, the archive is located on a separate server, and it also stores the base backup (or several, from multiple points in time).

Page 24: Streamed WAL Archive

pg\_receivewal utility

  • connects over the replication protocol (can use a replication slot)
  • and stores WAL records stream in segment files
  • the starting position is the beginning of the segment following the last filled
  • segment in the directory,
  • or the start of current segment, if the directory is empty
  • unlike the file archive, records are written continuously
  • parameters have to be reconfigured when changing servers

Speaker notes

Another solution is to use the pg_receivewal utility, which receives WAL records via the stream replication protocol and writes segments to the archive.

pg_receivewal usually runs on a separate “archive” server and connects to the main server with the parameters specified in the command line options. It can (and should) use a replication slot in order to ensure that records are not lost.

pg_receivewal generates files in the same way as the server does, and writes them to the specified directory. Segments that have not yet been filled in are written with the .partial suffix.

When launched, the utility starts archiving from the beginning of the segment, following the newest completed archive segment. If the archive is empty (first run), archiving starts from the beginning of the current segment.

When switching to a new server, pg_receivewal must be stopped and restarted with new parameters.

PostgreSQL does not include built-in tools to run the utility in the background (daemonization) or for automatic startup (as a service). To achieve this, you should use operating system features.

https://postgrespro.com/docs/postgresql/16/app-pgreceivewal

Page 25: Streamed WAL Archive

Source slide page 25: Streamed WAL Archive{#source-figure-025 caption=“Source slide page 25: Streamed WAL Archive”}

Speaker notes

pg_receivewal connects to the server over the stream replication protocol. The connection is handled by a separate wal sender process (this must be taken into account when setting the max_wal_senders parameter).

pg_receivewal saves data without waiting for the entire segment to be received.

Page 26: Base Backup + Archive

Configured continuous WAL archiving

  • Backup via pg_basebackup
    • connects to the server over the replication protocol
    • performs a checkpoint
    • copies the cluster files into a specified directory
    • WAL segments
    • are not required
  • Restore
    • deploy the backup
    • set configuration parameters
    • (command to read WAL from the archive, target recovery point)
    • create a recovery.signal file
    • start the server

Speaker notes

To create a backup with continuous archiving configured, the same pg_basebackup tool is used, but with a different set of parameters. The difference is that the WAL files are not saved to the backup, since they are already in the archive.

Restore is more complicated in this case. In addition to deploying the base backup, some recovery settings must be specified:

● restore_command (inverse of archive_command, it copies files from the archive to the server);

● target restore point.

In addition, a recovery.signal file is needed. If present at server startup, the file tells the server to enter the managed recovery mode (the contents of the file is ignored).

Page 27: Restore

Source slide page 27: Restore{#source-figure-027 caption=“Source slide page 27: Restore”}

Speaker notes

The restore procedure (for example, after main server crash) is performed as follows. A base backup is deployed on another (or the same) server and a recovery.signal file is created. The server starts up and starts reading WAL segments from the archive using restore_command and applying them.

Note that during file archiving, the last incomplete WAL segment at the main server will not be archived. However, the segment can be manually added to the pg_wal directory on the backup server, if this option is available. There may be several such segments in case of archiving failure.

Page 28: Restore

Source slide page 28: Restore{#source-figure-028 caption=“Source slide page 28: Restore”}

Speaker notes

The backup server reads WAL segments from the pg_wal directory and applies them (in the absence of a segment, making an attempt to copy it from the archive), ultimately bringing the state of the databases up to date. The maximum possible loss is the last incomplete WAL segment that has not been archived, and only if it cannot be copied manually for some reason.

By default, all available log entries are applied. If a target restore point is specified, recovery will stop after reaching it.

Page 29: Restore

Source slide page 29: Restore{#source-figure-029 caption=“Source slide page 29: Restore”}

Speaker notes

After that, the backup server goes into normal operation: processing incoming queries, archiving WAL segments, and so on.

The restored server can act as the primary server from now on, but in this case it should be deployed on sufficiently powerful hardware in the first place to avoid performance degradation.

Page 30: Takeaways

Logical backup creates SQL commands to restore the state of database objects

  • copy command, pg_dump and pg_dumpall utilities
  • Physical backup creates a copy of the cluster files + a set of WAL files
    • pg_basebackup utility
  • WAL archive
    • file or stream
    • can restore the system to an arbitrary point in time

Page 31: Practice

  1. Create a database and a table in it with several rows. 2. Make a logical backup of the database using pg_dump. Delete the database and restore it from the backup you made. 3. Make a standalone physical backup of the cluster using pg_basebackup. Modify the table. Restore into a new cluster from the backup you made and verify that the restored database does not contain any of the later changes.

Speaker notes

  1. replica cluster has already been created in the course VM on port 5433. Use this cluster to restore into.

The cluster data directory is /var/lib/postgresql/16/replica.

To connect to it, specify the port number: psql -p 5433

Page 32: 1. Database and Table

=> CREATE DATABASE backup_overview;
CREATE DATABASE
=> \c backup_overview
You are now connected to database "backup_overview" as user "student".
=> CREATE TABLE t(n integer);
CREATE TABLE
=> INSERT INTO t VALUES (1), (2), (3);
INSERT 0 3

2. Logical Backup

Create a backup:

student$ pg_dump -f /home/student/tmp/backup_overview.dump -d backup_overview --create

Delete the database and restore it from the backup:

=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE backup_overview;
DROP DATABASE
student$ psql -f /home/student/tmp/backup_overview.dump
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
CREATE DATABASE
ALTER DATABASE
You are now connected to database "backup_overview" as user "student".
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
COPY 3
=> \c backup_overview
You are now connected to database "backup_overview" as user "student".
=> SELECT * FROM t;

Page 33: 3. Physical Standalone Backup

 n
---
 1
 2
 3
(3 rows)

Creating a backup with a fast checkpoint:

student$ rm -rf /home/student/tmp/backup
student$ pg_basebackup --pgdata=/home/student/tmp/backup --checkpoint=fast

Make sure the second server is stopped and deploy the backup:

student$ sudo pg_ctlcluster 16 replica status
pg_ctl: no server running
student$ sudo rm -rf /var/lib/postgresql/16/replica
student$ sudo mv /home/student/tmp/backup /var/lib/postgresql/16/replica
student$ sudo chown -R postgres:postgres /var/lib/postgresql/16/replica

Change the table:

=> DELETE FROM t;
DELETE 3

Start the server from backup:

student$ sudo pg_ctlcluster 16 replica start
student$ psql -p 5433 -d backup_overview
=> SELECT * FROM t;
 n
---
 1
 2
 3
(3 rows)

Page 34: Practice+

  1. Set up stream archiving on the main cluster using pg_receivewal. 2. Create a standalone backup of the main cluster (without WAL) using pg_basebackup. 3. In the main cluster, create a database and a table in it. 4. Restore the replica cluster from the base backup using the archive. Verify that the database and the table are also restored.

Speaker notes

The replica cluster catalog is /var/lib/postgresql/16/replica.

The current file being written by the pg_receivewal utility has a .partial suffix. Once writing is complete, the file is renamed. When recovering, use the partial file along with the usual segments.

To connect to the replica cluster, specify the port number: psql -p 5433

Page 35: 1. Streaming Archive

Note

Note Note that some commands are executed as the postgres user, and some as student. Create a directory to store the WAL archive:

postgres$ mkdir /var/lib/postgresql/archive

Create a slot to avoid gaps in the archive:

postgres$ pg_receivewal --create-slot --slot=archive

Run the pg_receivewal utility in background. To do that, execute the following command in a separate terminal (or add & at the end of the command in the same terminal).

postgres$ pg_receivewal -D /var/lib/postgresql/archive --slot=archive &
student$ sudo ls -l /var/lib/postgresql/archive
total 16384
-rw------- 1 postgres postgres 16777216 Nov 27 14:02 000000010000000000000001.partial

2. Base Backup without WAL

student$ pg_basebackup --wal-method=none --pgdata=/home/student/tmp/backup --checkpoint=fast
NOTICE:  WAL archiving is not enabled; you must ensure that all required WAL segments are
copied through other means to complete the backup
student$ ls -l /home/student/tmp/backup
total 260
-rw------- 1 student student    225 Nov 27 14:02 backup_label
-rw------- 1 student student 180378 Nov 27 14:02 backup_manifest
drwx------ 6 student student   4096 Nov 27 14:02 base
drwx------ 2 student student   4096 Nov 27 14:02 global
drwx------ 2 student student   4096 Nov 27 14:02 pg_commit_ts
drwx------ 2 student student   4096 Nov 27 14:02 pg_dynshmem
drwx------ 4 student student   4096 Nov 27 14:02 pg_logical
drwx------ 4 student student   4096 Nov 27 14:02 pg_multixact
drwx------ 2 student student   4096 Nov 27 14:02 pg_notify
drwx------ 2 student student   4096 Nov 27 14:02 pg_replslot
drwx------ 2 student student   4096 Nov 27 14:02 pg_serial
drwx------ 2 student student   4096 Nov 27 14:02 pg_snapshots
drwx------ 2 student student   4096 Nov 27 14:02 pg_stat
drwx------ 2 student student   4096 Nov 27 14:02 pg_stat_tmp
drwx------ 2 student student   4096 Nov 27 14:02 pg_subtrans
drwx------ 2 student student   4096 Nov 27 14:02 pg_tblspc
drwx------ 2 student student   4096 Nov 27 14:02 pg_twophase
-rw------- 1 student student      3 Nov 27 14:02 PG_VERSION
drwx------ 3 student student   4096 Nov 27 14:02 pg_wal
drwx------ 2 student student   4096 Nov 27 14:02 pg_xact
-rw------- 1 student student     88 Nov 27 14:02 postgresql.auto.conf

3. New Database and Table

=> CREATE DATABASE backup_overview;
CREATE DATABASE
=> \c backup_overview
You are now connected to database "backup_overview" as user "student".
=> CREATE TABLE t(n integer);
CREATE TABLE
=> INSERT INTO t VALUES (1), (2), (3);
INSERT 0 3

4. Recovery Configuration

Make sure the second server is stopped and push the backup:

student$ sudo pg_ctlcluster 16 replica status

Page 36: Page 36

pg_ctl: no server running
student$ sudo rm -rf /var/lib/postgresql/16/replica
student$ sudo mv /home/student/tmp/backup /var/lib/postgresql/16/replica

Use the partial segment during recovery as well:

student$ echo "restore_command = 'cp /var/lib/postgresql/archive/%f %p || cp
/var/lib/postgresql/archive/%f.partial %p'" | sudo tee
/var/lib/postgresql/16/replica/postgresql.auto.conf
restore_command = 'cp /var/lib/postgresql/archive/%f %p || cp
/var/lib/postgresql/archive/%f.partial %p'
student$ touch /var/lib/postgresql/16/replica/recovery.signal
student$ sudo chown -R postgres:postgres /var/lib/postgresql/16/replica

Start the server and see the result:

student$ sudo pg_ctlcluster 16 replica start
student$ psql -p 5433 -d backup_overview
=> SELECT * FROM t;
 n
---
 1
 2
 3
(3 rows)

Archiving is no longer necessary. Stop the utility and delete the slot to prevent it from interfering with WAL cleanup.

student$ sudo pkill pg_receivewal
postgres$ pg_receivewal --drop-slot --slot=archive

15 Lesson 15: Replication / Overview of Physical Replication

Native Markdown source page: Replication / Overview of Physical Replication
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_15_replica_overview_physical.html Archive: DBA1-handouts-16-en.zip PDF pages: 22

Page 1: Replication / Overview of Physical Replication

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Igor Gnatyuk Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is”, and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Replication Types
  • Physical Replication
  • Log Levels
  • Replication Use Cases
  • Switching to Standby

Page 3: Replication Types

Data synchronization between servers

  • Purposes
    • fault tolerance, high availability
    • scalability
  • Physical replication
    • synchronization at the level of pages and row versions
  • Logical replication
    • synchronization at the level of table rows

Speaker notes

A single database server may not meet the requirements.

A single server is a potential point of failure. Two (or more) servers allow the system to maintain availability in case of a failure (fault tolerance) or, more broadly, in any scenario, such as during scheduled maintenance (high availability).

One server may not be able to handle the load. Scaling up (upgrading server resources) may be inefficient or even impossible. However, the workload can be distributed across multiple servers (scaling).

Database systems can access shared data.

The solution is to have multiple servers managing the same data. Replication refers to the process of synchronizing this data.

Depending on the level at which synchronization occurs, there are two types of replication: physical replication synchronizes changes at the data page level and transaction statuses and logical replication synchronizes changes at the table row level.

Page 4: Physical Replication

Mechanism

  • one server transfers WAL records to another server, and the second server
  • replays the received records
  • Features
    • primary-standby: data flow in one direction only
    • binary server compatibility is required
    • only the cluster as a whole can be replicated

Speaker notes

The idea of physical replication is that one server transfers WAL records to another server, and the second server replays the received records like in crash recovery.

During physical replication, servers have assigned roles: primary and standby. The primary transfers WAL records to standby (in the form of files or a stream of records). The standby applies these records to its data files. The WAL record application is purely mechanical, without “understanding the meaning” of the changes, so binary compatibility between servers is important (the same platform and major PostgreSQL version). Since the WAL is shared across the entire cluster, only the cluster as a whole can be replicated.

Page 5: Physical Replication

Source slide page 5: Physical Replication{#source-figure-005 caption=“Source slide page 5: Physical Replication”}

Speaker notes

To set up replication between two servers, we create a replica from a physical backup of the primary server. Normally, restoring such a backup would create a new independent server. However, when replication is enabled, the standby server operates in continuous recovery mode: it constantly applies new WAL records received from the primary server (handled by the startup process). This way, the replica is constantly maintained in an almost up-to-date state.

There are two ways to deliver WALs from the primary to the standby. The one used more commonly in production is streaming replication.

In this case, the replica (walreceiver process) connects to the primary (walsender process) via the replication protocol and receives the WAL record stream. This minimizes the replica lag and can even eliminate it entirely (in synchronous mode).

If the system is set for continuous archiving, file-based replication is possible. In this case, the replica will lag noticeably as the file archive is updated only when a WAL segment is switched.

In practice, file-based replication is used in addition to streaming replication. If the replica cannot receive the next WAL entry via the replication protocol, it will try to read it from the archive.

https://postgrespro.com/docs/postgresql/16/high-availability

Page 6: WAL Levels

wal\_level parameter

  • minimal < replica
  • crash recovery crash recovery restore from backup, replication

Speaker notes

Since standby only receives the information contained in the WAL, all data necessary for synchronization shall be recorded into the WAL.

The amount of data stored in each WAL record is controlled by the wal_level parameter.

Prior to PostgreSQL 10 the default level was minimal, which guaranteed only crash recovery. Replication cannot function at this level because some changes are directly written to persistent storage for reliability, bypassing WAL.

In PostgreSQL 10+ the default level is replica. At this level all data changes are recorded into the WAL, that enables restoring the system from pg_basebackup hot backups, as well as physical streaming replication.

As backup and replication are highly-demanded features, the default level was switched to replica.

Page 7: Physical Replication Configuration

Setting up streaming replication between two servers. We will focus on the simplest configuration; replication is covered in detail in the DBA3 administrator course. Required configuration parameters to be checked:

=> SELECT name, setting FROM pg_settings
WHERE name in ('wal_level','max_wal_senders');
      name       | setting
-----------------+---------
 max_wal_senders | 10
 wal_level       | replica
(2 rows)

Starting with PostgreSQL 10, default parameters already have appropriate values. Ensure pg_hba.conf allows replication protocol connections:

=> SELECT type, user_name, address, auth_method FROM pg_hba_file_rules
WHERE 'replication' = ANY(database);
 type  | user_name |  address  |  auth_method
-------+-----------+-----------+---------------
 local | {all}     |           | trust
 host  | {all}     | 127.0.0.1 | scram-sha-256
 host  | {all}     | ::1       | scram-sha-256
(3 rows)

The necessary permissions are already in place. Let’s deploy a standby from a physical backup, for this we use the pg_basebackup tool. The target copy directory must be empty or non-existent:

student$ rm -rf /home/student/tmp/backup

The –checkpoint=fast option requests the utility to perform an immediate checkpoint (without delays), while -R adds standby configuration settings:

student$ pg_basebackup --pgdata=/home/student/tmp/backup -R --checkpoint=fast

The utility creates a sample configuration file…

student$ cat /home/student/tmp/backup/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
primary_conninfo = 'user=student passfile=''/home/student/.pgpass''
channel_binding=prefer host=''/var/run/postgresql'' port=5432 sslmode=prefer
sslnegotiation=postgres sslcompression=0 sslcertmode=allow sslsni=1
ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres gssdelegation=0
target_session_attrs=any load_balance_hosts=disable'

… and a signal file that instructs the standby to enter continuous recovery mode:

student$ ls -l /home/student/tmp/backup/*.signal
-rw------- 1 student student 0 Nov 27 13:53 /home/student/tmp/backup/standby.signal

The cluster where we are deploying the standby has been pre-initialized. If the server is running, it must be stopped first:

student$ sudo pg_ctlcluster 16 replica stop
Cluster is not running.

The copy was placed in the home directory of student user, and now we transfer it to the cluster data directory and make postgres user the owner of the files:

student$ sudo rm -rf /var/lib/postgresql/16/replica
student$ sudo mv /home/student/tmp/backup /var/lib/postgresql/16/replica
student$ sudo chown -R postgres: /var/lib/postgresql/16/replica

Now we can start the server:

Page 8: Page 8

student$ sudo pg_ctlcluster 16 replica start

Let’s examine the standby processes.

student$ sudo head -n 1 '/var/lib/postgresql/16/replica/postmaster.pid'
27763
student$ ps -o pid,command --ppid 27763
    PID COMMAND
  27764 postgres: 16/replica: checkpointer
  27765 postgres: 16/replica: background writer
  27766 postgres: 16/replica: startup recovering 000000010000000000000003
  27767 postgres: 16/replica: walreceiver streaming 0/3000060

The walreceiver process receives the WAL stream, and the startup process applies changes. Compare these with the primary processes.

student$ sudo head -n 1 '/var/lib/postgresql/16/main/postmaster.pid'
27329
student$ ps -o pid,command --ppid 27329
    PID COMMAND
  27330 postgres: 16/main: checkpointer
  27331 postgres: 16/main: background writer
  27333 postgres: 16/main: walwriter
  27334 postgres: 16/main: autovacuum launcher
  27335 postgres: 16/main: logical replication launcher
  27378 postgres: 16/main: student student [local] idle
  27768 postgres: 16/main: walsender student [local] streaming 0/3000060

The walsender process sends WAL records to the standby. The state of the replication can be checked on the primary server:

=> SELECT * FROM pg_stat_replication \gx
-[ RECORD 1 ]----+------------------------------
pid              | 27768
usesysid         | 16384
usename          | student
application_name | 16/replica
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2025-11-27 13:53:32.661021+03
backend_xmin     |
state            | streaming
sent_lsn         | 0/3000060
write_lsn        | 0/3000060
flush_lsn        | 0/3000060
replay_lsn       | 0/3000060
write_lag        | 00:00:00.091217
flush_lag        | 00:00:00.09906
replay_lag       | 00:00:00.099478
sync_priority    | 0
sync_state       | async
reply_time       | 2025-11-27 13:53:32.762688+03

*_lsn values indicate which WAL records were sent to the standby, received by it, written to disk and applied. sync_state is synchronous or asynchronous replication (we will explain it in detail later).

Page 9: Standby Usage

Allowed

  • read-only queries (SELECT, COPY TO, cursors)
  • setting server parameters (SET, RESET)
  • transaction management (BEGIN, COMMIT, ROLLBACK…)
  • creating a backup (pg_basebackup)
  • Not allowed
    • any changes (INSERT, UPDATE, DELETE, TRUNCATE, nextval…)
    • locks expecting changes (SELECT FOR UPDATE…)
    • DDL commands (CREATE, DROP…), including creating temporary tables
    • maintenance commands (VACUUM, ANALYZE, REINDEX…)
    • access control (GRANT, REVOKE…)
    • triggers and advisory locks do not work

Speaker notes

By default, the standby operates in the hot standby mode. In this mode, client connections are allowed but restricted to read-only operations. Setting server parameters and transaction management commands will also work. For example, you can start a (read-only) transaction with a specific isolation level.

In addition, the standby can also be used for making backups (taking into account the possible lag behind the primary).

In hot standby mode, no data changes (including sequences), locks, DDL commands, commands such as VACUUM, ANALYZE and REINDEX or access control commands are allowed on the standby. Basically, anything that changes the data in any way is not accepted.

If required, the standby can be run in warm standby mode by setting the parameter hot_standby = off. In this case, client connections will be completely disabled.

https://postgrespro.com/docs/postgresql/16/hot-standby

Page 10: Standby Usage

Run several commands on the primary server:

=> CREATE DATABASE replica_overview_physical;
CREATE DATABASE
=> \c replica_overview_physical
You are now connected to database "replica_overview_physical" as user "student".
=> CREATE TABLE test(id integer PRIMARY KEY, descr text);
CREATE TABLE

Check the standby:

student$ psql -p 5433 -d replica_overview_physical
=> SELECT * FROM test;
 id | descr
----+-------
(0 rows)

Let’s insert a row into the table on the primary server:

=> INSERT INTO test VALUES (1, 'One');
INSERT 0 1
=> SELECT * FROM test;
 id | descr
----+-------
  1 | One
(1 row)

So, replication is working correctly, and queries are executed successfully on the standby. No changes can be made on standby directly:

=> INSERT INTO test VALUES (2, 'Two');
ERROR:  cannot execute INSERT in a read-only transaction

Page 11: Standby Usage

Source slide page 11: Standby Usage{#source-figure-011 caption=“Source slide page 11: Standby Usage”}

data storage reliability

Speaker notes

The replication mechanism offers flexible system design options for a variety of applications. Let’s consider several typical cases and possible solutions.

One of the key objectives is ensuring data storage reliability.

As a reminder, transaction commits can operate in synchronous or asynchronous modes. In synchronous mode, the commit is not completed until the data is safely written to persistent storage. In asynchronous mode, there is a risk of losing some committed data, but commits do not wait for disk writes, improving system performance.

A similar principle is applied to replication: in synchronous mode (synchronous_commit = on), when a standby is present, the commit waits not only for the WAL to be written to disk but also for confirmation that the WAL records have been received by the synchronous standby. This further enhances reliability (ensuring data is not lost even if the primary server fails) but also increases latency, slowing down the system.

There are also intermediate configuration options that do not provide absolute reliability guarantees but still reduce the risk of data loss.

Page 12: Standby Usage

Source slide page 12: Standby Usage{#source-figure-012 caption=“Source slide page 12: Standby Usage”}

long-running analytical queries (reports)

Speaker notes

As mentioned earlier, long-running queries hold back the vacuum horizon, preventing the removal of obsolete row versions. If certain tables are being actively modified during this time, they can grow significantly in size. This is why standbys are often used for long-running analytical queries.

A subtle issue arises when WAL records from the primary server conflict with queries running on the standby. There are two main sources of such records:

  1. The primary server removes row versions that are no longer needed there but are still required by queries on the standby.

  2. Exclusive locks on the primary that are incompatible with queries on the standby.

So, standby for reports is typically configured to accept WAL records from the primary but delay their application if they conflict with running queries. This means the standby’s data may lag behind the primary, but for analytical workloads, this is acceptable.

Page 13: Multiple Stanbys

Source slide page 13: Multiple Stanbys{#source-figure-013 caption=“Source slide page 13: Multiple Stanbys”}

Speaker notes

Multiple standbys can be connected to the primary server to distribute OLTP read workloads.

OLTP queries should not be long-running. This enables effective use of replication protocol feedback between standbys and the primary server. In this case, the primary server maintains awareness of the transaction horizon required by queries on standbys, preventing vacuum from removing needed row versions. Essentially, this feedback mechanism achieves the same result as if all queries were executing locally on the primary server.

However, replication provides only the basic mechanism. External tools (load balancers) are required for automatic workload distribution. It is important to note that data consistency between the primary and standbys is not guaranteed — even in synchronous replication. Applications reading from a single server will, of course, maintain consistency, but consistency is no longer guaranteed when reading from multiple servers simultaneously. Replicas may return either stale data or changes not yet visible on the primary. These topics are discussed in detail in the DBA3 Backup and Replication course.

Page 14: Cascading Replication

Source slide page 14: Cascading Replication{#source-figure-014 caption=“Source slide page 14: Cascading Replication”}

no additional load on the primary and redistribution of network traffic

Speaker notes

Multiple standbys connected to a single primary server will generate additional load on it. Network load should also be considered when transmitting multiple copies of the WAL stream.

To reduce this load, standbys can be arranged in a cascade configuration, where servers relay WAL records to each other in a chain. The further downstream from the primary, the greater potential lag may accumulate in the replicated data.

Note that cascaded synchronous replication is not supported — the primary can only synchronize with directly connected standbys. However, the primary collects feedback from all standbys in the cascade.

Page 15: Delayed Replication

Source slide page 15: Delayed Replication{#source-figure-015 caption=“Source slide page 15: Delayed Replication”}

“time machine” can recover to a specific point in time without WAL archive

Speaker notes

A useful feature is the ability to view data at and recover to an arbitrary point in time. It is particularly useful for recovering from user errors where incorrect actions need to be rolled back.

The regular archive-based point-in-time recovery mechanism can work here, but it requires a lot of preparation and takes a lot of time. And PostgreSQL itself does not allow to make data snapshots for a given moment in the past.

The solution is to have a standby apply WAL records not immediately, but with a certain delay.

In this course, we do not cover the required configurations for each of the provided options. For detailed information, refer to the DBA3 Backup and Replication course.

Page 16: Switching to Standby

Scheduled switchover

  • shutdown of the main server for maintenance without interruption of service
  • manual mode
  • Emergency switchover
    • switch to a standby due to a primary server failure
    • manual mode,
    • but can be automated with external cluster software

Speaker notes

An existing standby can be used to replace the primary server.

There are different reasons for switchover to a backup server. If it is maintenance time on the primary, the switchover can be performed routinely at a convenient time. If it is the primary failure, on the other hand, the switchover has to be performed as quickly as possible to avoid service downtime.

Even in case of a failure, switchover is performed manually unless specialized cluster software is used to monitor server status and initiate the switchover automatically.

Page 17: Switching to a Standby

Source slide page 17: Switching to a Standby{#source-figure-017 caption=“Source slide page 17: Switching to a Standby”}

To switch a standby from recovery to normal mode use the appropriate command.

=> SELECT pg_is_in_recovery(); -- is it a standby?
 pg_is_in_recovery
-------------------
 t
(1 row)
student$ sudo pg_ctlcluster 16 replica promote

Starting with PostgreSQL 12, it can be done with the pg_promote SQL function.

=> SELECT pg_is_in_recovery(); -- let’s check again: is it a standby?
 pg_is_in_recovery
-------------------
 f
(1 row)

We have two completely independent servers running at the same time.

=> INSERT INTO test VALUES (2, 'Two');
INSERT 0 1

It is extremely important to ensure that an application connects to only one of the servers to avoid split-brain scenarios where data becomes irreconcilably divided between servers.

Page 18: Takeaways

Physical replication mechanism works by delivering WAL records to the standby and applying them there

  • streaming WAL records or transferring files
  • Physical replication creates an exact copy of the entire cluster
  • unidirectional, requires binary compatibility
    • core mechanism for solving multiple use cases

Page 19: Practice

  1. Set up physical streaming replication between the two servers in synchronous mode. Verify that replication works as intended. Make sure that when the standby is stopped, commits on the primary are not completed. 2. By default, conflicting WAL records on the standby are delayed for up to 30 seconds. Disable the delay and verify that long-running queries on the standby are canceled if the primary deletes and vacuums required row versions. Then enable feedback and confirm that it prevents cancellations by delaying primary vacuums.

Speaker notes

  1. To do this, set the following parameters on the primary using ALTER SYSTEM:

● synchronous_commit = on

● synchronous_standby_names = ‘“16/replica”’

  1. The max_standby_streaming_delay parameter defines how long the standby will wait for conflicting queries to complete before canceling them. Set it to 0. Enable feedback by setting hot_standby_feedback = on. Apply both settings using ALTER SYSTEM and reload the configuration.

To simulate a long-running query on a small dataset, use pg_sleep() in queries.

Page 20: 1. Synchronous Replication

Deploy the standby as shown in the demonstration:

student$ pg_basebackup --pgdata=/home/student/tmp/backup -R --checkpoint=fast
student$ sudo pg_ctlcluster 16 replica stop
Cluster is not running.
student$ sudo rm -rf /var/lib/postgresql/16/replica
student$ sudo mv /home/student/tmp/backup /var/lib/postgresql/16/replica
student$ sudo chown -R postgres: /var/lib/postgresql/16/replica

Starting the standby.

student$ sudo pg_ctlcluster 16 replica start

Let’s configure synchronous replication on the primary. By default, synchronous mode is enabled, but transaction commit records are only synchronized with the local file system:

=> SHOW synchronous_commit;
 synchronous_commit
--------------------
 on
(1 row)

The synchronization remains unconfigured:

=> SHOW synchronous_standby_names;
 synchronous_standby_names
---------------------------
(1 row)

There can be several standbys, and the primary must know which one to synchronize with. The standby is represented by the name specified in its cluster_name parameter:

student$ psql -p 5433
=> SHOW cluster_name;
 cluster_name
--------------
 16/replica
(1 row)
=> ALTER SYSTEM SET synchronous_standby_names = '"16/replica"';
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)
=> SELECT sync_state FROM pg_stat_replication;
 sync_state
------------
 sync
(1 row)

The standby starts successfully.

=> CREATE DATABASE replica_overview_physical;
CREATE DATABASE

Page 21: Page 21

=> \c replica_overview_physical
You are now connected to database "replica_overview_physical" as user "student".

Now stop the standby…

student$ sudo pg_ctlcluster 16 replica stop

…and attempt to execute a transaction:

=> CREATE TABLE test(n integer);

The operation hangs until the standby restarts and replication is restored:

student$ sudo pg_ctlcluster 16 replica start
CREATE TABLE

2. Conflicting Records

student$ psql -p 5433 -d replica_overview_physical

We disable delayed conflict resolution:

=> ALTER SYSTEM SET max_standby_streaming_delay = 0;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Insert rows into the table:

=> INSERT INTO test(n) SELECT id FROM generate_series(1,10) AS id;
INSERT 0 10

Execute a long-running query on the standby…

=> SELECT pg_sleep(5), count(*) FROM test;

…meanwhile, delete table rows and perform vacuuming on the primary:

=> DELETE FROM test;
DELETE 10
=> VACUUM VERBOSE test;
INFO:  vacuuming "replica_overview_physical.public.test"
INFO:  table "test": truncated 1 to 0 pages
INFO:  finished vacuuming "replica_overview_physical.public.test": index scans: 0
pages: 1 removed, 0 remain, 1 scanned (100.00% of total)
tuples: 10 removed, 0 remain, 0 are dead but not yet removable
removable cutoff: 737, which was 1 XIDs old when operation ended
new relfrozenxid: 737, which is 3 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 1 pages from table (100.00% of total) had 10 dead item identifiers
removed
avg read rate: 0.000 MB/s, avg write rate: 3.199 MB/s
buffer usage: 10 hits, 0 misses, 4 dirtied
WAL usage: 6 records, 1 full page images, 8675 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.01 s
VACUUM

Vacuuming removed all row versions (tuples: 10 removed). The standby query fails with an error:

ERROR:  canceling statement due to conflict with recovery
DETAIL:  User query might have needed to see row versions that must be removed.

Repeat the experiment with feedback enabled.

=> ALTER SYSTEM SET hot_standby_feedback = on;
ALTER SYSTEM
=> SELECT pg_reload_conf();

Page 22: Page 22

 pg_reload_conf
----------------
 t
(1 row)
=> INSERT INTO test(n) SELECT id FROM generate_series(1,10) AS id;
INSERT 0 10
=> SELECT pg_sleep(5), count(*) FROM test;
=> DELETE FROM test;
DELETE 10
=> VACUUM VERBOSE test;
INFO:  vacuuming "replica_overview_physical.public.test"
INFO:  finished vacuuming "replica_overview_physical.public.test": index scans: 0
pages: 0 removed, 1 remain, 1 scanned (100.00% of total)
tuples: 0 removed, 10 remain, 10 are dead but not yet removable
removable cutoff: 738, which was 2 XIDs old when operation ended
new relfrozenxid: 738, which is 1 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers
removed
avg read rate: 0.000 MB/s, avg write rate: 24.881 MB/s
buffer usage: 8 hits, 0 misses, 1 dirtied
WAL usage: 1 records, 0 full page images, 188 bytes
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
VACUUM

Now, the vacuum does not delete the row versions because it is aware of a query running on the standby (10 are dead but not yet removable) and the query is completed successfully:

 pg_sleep | count
----------+-------
          |    10
(1 row)

Results: In the first case (max_standby_streaming_delay), application of WAL records on the standby is delayed. In the second case (hot_standby_feedback), the vacuuming on the primary is delayed. Disable synchronous replication.

=> ALTER SYSTEM RESET synchronous_standby_names;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

16 Lesson 16: Replication / Overview of Logical Replication

Native Markdown source page: Replication / Overview of Logical Replication
Note

This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: dba1_16_replica_overview_logical.html Archive: DBA1-handouts-16-en.zip PDF pages: 23

Page 1: Replication / Overview of Logical Replication

© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Aleksei Beresnev Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova Photo: Oleg Bartunov (Phu Monastery and Bhrikuti Peak, Nepal)

Use of Course Materials

Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.

Feedback

Please send your feedback, comments and suggestions to: edu@postgrespro.ru

Disclaimer

Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is”, and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.

Page 2: Topics

  • Logical Replication
  • WAL Levels
  • Publications and Subscriptions
  • Conflict Detection and Resolution
  • Replica Usage

Page 3: Logical Replication

Publisher

  • reads its own WAL records
  • decodes them into table row changes
  • sends to subscribers
  • Subscriber
    • receives WAL records from the replication stream
    • applies the changes to its own tables
  • Features
    • publication-subscription: data flow is possible in both directions
    • requires protocol-level compatibility
    • can replicate individual tables

Speaker notes

In physical replication only one server (the primary) makes changes and generates WAL records. Other servers (standbys) only read the primary’s WAL records and apply them.

In logical replication all servers operate normally, can modify data, and generate their own WAL records. Any server can publish its changes, while others can subscribe to them. A single server can act as both a publisher and a subscriber, enabling flexible data flows between servers.

The publisher reads its own WAL records, but unlike physical replication, it first decodes them into a logical format that is platform-independent and not tied to specific PostgreSQL version before transmitting to subscribers. Therefore, binary compatibility is not required — only the replication protocol must be understood. It also supports selective replication, allowing only specific tables to be replicated.

https://postgrespro.com/docs/postgresql/16/logical-replication

Page 4: Logical Replication

Source slide page 4: Logical Replication{#source-figure-004 caption=“Source slide page 4: Logical Replication”}

Speaker notes

On the publisher the wal_sender process generates WAL records reflecting changes to published data. On the subscriber the logical replication worker receives the information from the publisher and applies it.

The subscriber can receive changes either directly from the publisher or from the publisher’s standby. In the latter case, the standby’s wal_sender process sends changes to the subscriber. This architecture helps reduce load on the publisher.

https://postgrespro.com/docs/postgresql/16/logical-replication-publication

https://postgrespro.com/docs/postgresql/16/logical-replication-subscription

Page 5: Publication and Subscription

Publication

  • includes one or more database tables
  • can specify columns and row filters
  • processes INSERT, UPDATE, DELETE, TRUNCATE commands
  • sends row-level changes after transaction commit
  • uses a logical replication slot
  • Subscription
    • receives and applies changes
    • can do initial synchronization
    • no parsing, rewriting and planning, just direct execution
    • possible conflicts with local data

Speaker notes

Logical replication uses a publish and subscribe model.

A publication is created on one server and can include several tables in a single database. Starting with version 15, PostgreSQL allows publishing partial table data by specifying column subsets and row filter conditions. Other servers can subscribe to this publication to receive and apply changes to the tables.

Only table row modifications are replicated (not SQL commands). DDL commands are not transmitted, so target tables on the subscriber must be created manually. Initial synchronization of table contents can be performed when a subscription is created.

After transaction commit, information about modified rows is extracted from WAL records on the publisher through logical decoding. The resulting messages are transmitted to subscribers via the replication protocol in a platform-and version-independent format.

Changes are applied without executing SQL commands, avoiding parsing and planning overhead. On the other hand, a single SQL command can result in multiple one-row changes.

https://postgrespro.com/docs/postgresql/16/logical-replication

Page 6: WAL Levels

wal\_level parameter

  • minimal < replica < logical
  • crash recovery crash recovery crash recovery restore from backup, restore from backup, replication replication logical replication

Speaker notes

To enable the publisher to generate messages about changes at the row level, the WAL must include additional information, such as row identifiers involved in modifications and notifications about changes to table definitions and data types. This allows the subscriber to have up-to-date knowledge of the structure of replicated objects in any moment.

This extended level of logging is called logical. Since the default level is replica, logical replication requires changing the wal_level parameter on the publisher.

https://postgrespro.com/docs/postgresql/16/protocol-logical-replication#PRO TOCOL-LOGICAL-MESSAGES-FLOW

Page 7: Logical Replication

Assume the first server contains a table:

=> CREATE DATABASE replica_overview_logical_dba;
CREATE DATABASE
=> \c replica_overview_logical_dba
You are now connected to database "replica_overview_logical_dba" as user "student".
=> CREATE TABLE test(id integer PRIMARY KEY, descr text);
CREATE TABLE

Let’s clone the cluster by creating a standalone backup (as covered in the Physical Replication Overview lesson), but omit the -R option from pg_basebackup call since we need an independent server, not a standby.

student$ rm -rf /home/student/tmp/backup
student$ pg_basebackup --pgdata=/home/student/tmp/backup --checkpoint=fast

If the second server is running, we stop it.

student$ sudo pg_ctlcluster 16 replica stop
Cluster is not running.

Move the backup to the data directory of the second server, changing the owner of the files:

student$ sudo rm -rf /var/lib/postgresql/16/replica
student$ sudo mv /home/student/tmp/backup /var/lib/postgresql/16/replica
student$ sudo chown -R postgres: /var/lib/postgresql/16/replica

Start the second server:

student$ sudo pg_ctlcluster 16 replica start

We got two independent servers, each of them has an empty test table. Let’s add a couple of lines to the table on the first server:

=> INSERT INTO test VALUES (1, 'One'), (2, 'Two');
INSERT 0 2

To establish logical replication between servers, additional information in the WAL of the publishing server is required:

=> ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 main restart

Create a publication on the first server:

student$ psql -d replica_overview_logical_dba
=> CREATE PUBLICATION test_pub FOR TABLE test;
CREATE PUBLICATION
=> \dRp+
                           Publication test_pub
  Owner  | All tables | Inserts | Updates | Deletes | Truncates | Via root
---------+------------+---------+---------+---------+-----------+----------
 student | f          | t       | t       | t       | t         | f
Tables:
    "public.test"

On the second server, subscribe to the publication:

student$ psql -p 5433 -d replica_overview_logical_dba
=> CREATE SUBSCRIPTION test_sub
CONNECTION 'port=5432 user=student dbname=replica_overview_logical_dba'
PUBLICATION test_pub;
NOTICE:  created replication slot "test_sub" on publisher
CREATE SUBSCRIPTION

Page 8: Page 8

=> \dRs
           List of subscriptions
   Name   |  Owner  | Enabled | Publication
----------+---------+---------+-------------
 test_sub | student | t       | {test_pub}
(1 row)

Verify the replication:

=> INSERT INTO test VALUES (3, 'Three');
INSERT 0 1
=> SELECT * FROM test;
 id | descr
----+-------
  1 | One
  2 | Two
  3 | Three
(3 rows)

The following view shows the state of the subscription:

=> SELECT * FROM pg_stat_subscription \gx
-[ RECORD 1 ]---------+------------------------------
subid                 | 24578
subname               | test_sub
pid                   | 29284
leader_pid            |
relid                 |
received_lsn          | 0/3004298
last_msg_send_time    | 2025-11-27 13:54:55.999151+03
last_msg_receipt_time | 2025-11-27 13:54:56.031801+03
latest_end_lsn        | 0/3004298
latest_end_time       | 2025-11-27 13:54:55.999151+03

The logical replication apply worker process has been started (you can see its ID in pg_stat_subscription.pid):

student$ ps -o pid,command --ppid 28969
    PID COMMAND
  28970 postgres: 16/replica: checkpointer
  28971 postgres: 16/replica: background writer
  28975 postgres: 16/replica: walwriter
  28976 postgres: 16/replica: autovacuum launcher
  28977 postgres: 16/replica: logical replication launcher
  29250 postgres: 16/replica: student replica_overview_logical_dba [local] idle
  29284 postgres: 16/replica: logical replication apply worker for subscription 24578

Page 9: Conflicts

Identification modes for modifying and deleting rows

  • primary key columns (default)
  • columns of a specific unique index with the NOT NULL constraint
  • all columns
  • no identification (default for the system catalog)
  • Conflicts: violation of integrity constraints
    • replication is suspended until the conflict is resolved manually

Speaker notes

Inserting new rows is straightforward. Changes and deletions are more complicated. These operations need to somehow identify the old version of the row. By default, primary key columns are used for this, but you can specify other ways (replica identity) when defining a table, i.e. use a unique index or all the table columns. Or you can disable replication for some tables altogether (system catalog tables have it disabled by default).

Since the table on the publisher and the table on the subscriber can be changed independently of each other, conflicts in the form of integrity constraint violations are possible when inserting new row versions. Whenever this happens, the process of applying records is suspended until the conflict is resolved manually.

https://postgrespro.com/docs/postgresql/16/sql-altertable

https://postgrespro.com/docs/postgresql/16/logical-replication-conflicts

https://postgrespro.com/docs/postgresql/16/sql-altersubscription

Page 10: Conflicts

Local modifications on the subscriber are not prohibited. Let’s insert a row into the table on the second server:

=> INSERT INTO test VALUES (4, 'Four (local)');
INSERT 0 1

If we add a row with the same primary key value on the publisher, the subscription will have a conflict when trying to apply the change.

=> INSERT INTO test VALUES (4, 'Four');
INSERT 0 1
=> INSERT INTO test VALUES (5, 'Five');
INSERT 0 1

The subscription is unable to apply the change, replication stops.

=> SELECT * FROM pg_stat_subscription \gx
-[ RECORD 1 ]---------+---------
subid                 | 24578
subname               | test_sub
pid                   |
leader_pid            |
relid                 |
received_lsn          |
last_msg_send_time    |
last_msg_receipt_time |
latest_end_lsn        |
latest_end_time       |
=> SELECT * FROM test;
 id |    descr
----+--------------
  1 | One
  2 | Two
  3 | Three
  4 | Four (local)
(4 rows)

To resolve the conflict, remove the row on the second server and wait a moment…

=> DELETE FROM test WHERE id=4;
DELETE 1
=> SELECT * FROM test;
 id | descr
----+-------
  1 | One
  2 | Two
  3 | Three
  4 | Four
  5 | Five
(5 rows)

Replication continues.

Dropping a Subscription

If replication is no longer needed, the subscription should be deleted, otherwise the publisher will keep the open replication slot.

=> DROP SUBSCRIPTION test_sub;
NOTICE:  dropped replication slot "test_sub" on publisher
DROP SUBSCRIPTION

Page 11: Page 11

Page 12: Restrictions

Not replicated

  • DDL commands
  • sequence values
  • large objects
  • changes to views, materialized views, and foreign tables
  • Not supported
    • automatic conflict resolution

Speaker notes

Logical replication has some fundamental limitations.

DDL commands are not replicated — all schema changes must be copied manually.

Only regular base tables and partitioned tables can be replicated. Other relation types such as views, materialized views, and foreign tables cannot be replicated.

Sequence values are not replicated. This means that if the subscriber inserts rows into a replicated table with a surrogate primary key, conflicts may occur. These conflicts can be avoided by allocating different sequence ranges to each server or by using UUIDs instead of sequences.

There is no built-in conflict resolution mechanism.

These restrictions reduce the applicability of logical replication.

https://postgrespro.com/docs/postgresql/16/logical-replication-restrictions

Page 13: Features

Referential integrity

  • for TRUNCATE operations, the publication must include all tables referenced
  • by foreign keys
  • Persistent connection is mandatory
    • inactive replication slots prevent WAL segment removal and hold back the
    • vacuum horizon
  • Potential issues
    • bulk data changes
    • changes made by long-running transactions

Speaker notes

To properly replicate TRUNCATE commands, the publication should include all tables that have foreign key references to the truncated table.

The connection between the publisher and the subscriber must remain stable. If interrupted, the replication slot becomes inactive, forcing the server to retain WAL segments until reconnection. Inactive slots also hold back the vacuum horizon.

Changes are replicated row-by-row, so SQL commands affecting many rows on the publisher create significant subscriber load.

The default configuration handles long-running transactions poorly — they increase publisher load since changes are only sent after transaction commit. The subscription’s streaming parameter mitigates this by getting row change without delay, either buffering changes in temporary files or applying changes immediately when background process is available.

Page 14: 1. Consolidation

Source slide page 14: 1. Consolidation{#source-figure-014 caption=“Source slide page 14: 1. Consolidation”}

receipt and consolidation of data from regional servers

Speaker notes

Let’s discuss some logical replication use cases.

Suppose there are several regional branches, each of which runs on its own PostgreSQL server. The goal is to consolidate some data on a central server.

First, publications of the necessary data are created on regional servers. The central server subscribes to these publications. The received data can be processed using triggers on the central server (for example, unifying the data format).

Inverted, the setup allows, for example, to transfer reference information from the central server to regional ones.

The business logic may apply additional constraints on the system. In some use cases, scheduled batch data transfers may be preferable.

The image shows two WAL receiving processes running on the central server, one for each subscription.

Page 15: 2. Server Update

Source slide page 15: 2. Server Update{#source-figure-015 caption=“Source slide page 15: 2. Server Update”}

updating the major version without interruption of service

Speaker notes

Case: update the major version on the server without interrupting the service.

Two major versions are not binary compatible, so physical replication will not work. However, logical replication can solve the problem.

As usual, external tools are required to switch users between servers.

First, a new server is created with the desired PostgreSQL version.

Page 16: 2. Server Update

Source slide page 16: 2. Server Update{#source-figure-016 caption=“Source slide page 16: 2. Server Update”}

updating the major version without interruption of service

Speaker notes

Then, logical replication of all required databases is set up between the servers, and the data is synchronized. This is possible because logical replication does not require binary compatibility between servers.

Page 17: 2. Server Update

Source slide page 17: 2. Server Update{#source-figure-017 caption=“Source slide page 17: 2. Server Update”}

updating the major version without interruption of service

Speaker notes

Clients are then switched to the new server while the old one is terminated.

In practice, the process of updating major server version using logical replication is much more complicated and difficult. It is discussed in more detail in the Server Update lesson of the DBA2 course.

Page 18: 3. Primary-Primary

Source slide page 18: 3. Primary-Primary{#source-figure-018 caption=“Source slide page 18: 3. Primary-Primary”}

a cluster where multiple servers can modify data

Speaker notes

Case: provide reliable data storage on multiple servers with the ability to write the data on any server (particularly useful for geo-distributed systems).

This can be achieved through bidirectional logical replication, synchronizing changes for the same tables between servers in both directions.

PostgreSQL 16 introduced bidirectional replication, where tables can be both published and subscribed to on different servers simultaneously.

This requires that the applications working with the cluster are built with certain considerations in mind in order to avoid conflicts when modifying data in the same table. For example, to use globally unique identifiers or to ensure that different servers work with different ranges of keys.

Keep in mind that the primary-primary setup with logical replication will not support global distributed transactions and thus cannot guarantee full data consistency between servers. In addition, PostgreSQL does not offer any tools for automatic failure processing, adding nodes to the cluster or removing nodes from it, etc. These tasks must be solved by external means.

Page 19: Takeaways

Logical replication streams individual row changes

  • multidirectional
    • requires protocol-level compatibility
  • Publish and subscribe model
  • All current restrictions must be considered

Page 20: Practice

  1. Set up logical replication of an arbitrary table to another table on the same server. 2. Set up bidirectional logical replication of the same table between two different servers.

Speaker notes

  1. If you try to perform standard operations, the CREATE SUBSCRIPTION command will hang. Read carefully the documentation:

https://postgrespro.com/docs/postgresql/16/sql-createsubscription

  1. Clone the second server from a backup, as shown in demonstration earlier.

When creating subscriptions on both servers, specify the following parameters: copy_data = false, origin = none.

Page 21: 1. Replication on a Single Server

Test table:

=> CREATE DATABASE replica_overview_logical_dba;
CREATE DATABASE
=> \c replica_overview_logical_dba
You are now connected to database "replica_overview_logical_dba" as user "student".
=> CREATE TABLE test (
    id int
);
CREATE TABLE

A copy of the database with the table:

=> \c student
You are now connected to database "student" as user "student".
=> CREATE DATABASE replica_overview_logical_dba2 TEMPLATE replica_overview_logical_dba;
CREATE DATABASE

Set the required WAL level:

=> ALTER SYSTEM SET wal_level = logical;
ALTER SYSTEM
=> \q
student$ sudo pg_ctlcluster 16 main restart

Connect to the databases:

student$ psql replica_overview_logical_dba
student$ psql replica_overview_logical_dba2

Create a publication in the first database:

=> CREATE PUBLICATION test FOR TABLE test;
CREATE PUBLICATION

By default, the CREATE SUBSCRIPTION command creates a replication slot, which waits for all active transactions (at the time of slot creation) to complete before proceeding. One such transaction is the CREATE SUBSCRIPTION command itself, causing it to block indefinitely. This issue can be resolved by manually creating a replication slot and specifying its name when creating the subscription:

=> SELECT pg_create_logical_replication_slot('test_slot','pgoutput');
 pg_create_logical_replication_slot
------------------------------------
 (test_slot,0/21819E8)
(1 row)
=> CREATE SUBSCRIPTION test
CONNECTION 'user=student dbname=replica_overview_logical_dba'
PUBLICATION test WITH (slot_name = test_slot, create_slot = false);
CREATE SUBSCRIPTION

Verification:

=> INSERT INTO test SELECT * FROM generate_series(1,100);
INSERT 0 100
=> SELECT count(*) FROM test;

Page 22: Page 22

 count
-------
   100
(1 row)

Replication is working. Delete the publication, subscription, and the second database.

=> DROP PUBLICATION test;
DROP PUBLICATION
=> DROP SUBSCRIPTION test;
NOTICE:  dropped replication slot "test_slot" on publisher
DROP SUBSCRIPTION
=> DROP DATABASE replica_overview_logical_dba2 (FORCE);
DROP DATABASE

2. Bidirectional Replication

We clone the server using a backup:

student$ pg_basebackup --pgdata=/home/student/tmp/backup --checkpoint=fast

Make sure the second server is stopped and push the backup:

student$ sudo pg_ctlcluster 16 replica stop
Cluster is not running.
student$ sudo rm -rf /var/lib/postgresql/16/replica
student$ sudo mv /home/student/tmp/backup /var/lib/postgresql/16/replica
student$ sudo chown -R postgres: /var/lib/postgresql/16/replica

Start the second server:

student$ sudo pg_ctlcluster 16 replica start

The database with the table and the log level setting were also cloned:

student$ psql -p 5433 replica_overview_logical_dba
=> SHOW wal_level;
 wal_level
-----------
 logical
(1 row)
=> SELECT count(*) FROM test;
 count
-------
   100
(1 row)

We publish the table on both servers:

=> CREATE PUBLICATION test FOR TABLE test;
CREATE PUBLICATION
=> CREATE PUBLICATION test FOR TABLE test;
CREATE PUBLICATION

Subscribe:

=> CREATE SUBSCRIPTION test
CONNECTION 'port=5433 user=student dbname=replica_overview_logical_dba'
PUBLICATION test WITH (copy_data = false, origin = none);
NOTICE:  created replication slot "test" on publisher
CREATE SUBSCRIPTION

Page 23: Page 23

=> CREATE SUBSCRIPTION test
CONNECTION 'port=5432 user=student dbname=replica_overview_logical_dba'
PUBLICATION test WITH (copy_data = false, origin = none);
NOTICE:  created replication slot "test" on publisher
CREATE SUBSCRIPTION

Modify table rows:

=> UPDATE test SET id = id + 100;
ERROR:  cannot update table "test" because it does not have a replica identity and
publishes updates
HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.

For changes and deletions to be replicated properly, row identification must be configured. By default, rows are identified by their primary key.

=> ALTER TABLE test ADD PRIMARY KEY (id);
ALTER TABLE
=> ALTER TABLE test ADD PRIMARY KEY (id);
ALTER TABLE

Retry the operation:

=> UPDATE test SET id = id + 100;
UPDATE 100
=> UPDATE test SET id = id + 100;
UPDATE 100

Check the Result:

=> SELECT min(id), max(id) FROM test;
 min | max
-----+-----
 201 | 300
(1 row)
=> SELECT min(id), max(id) FROM test;
 min | max
-----+-----
 201 | 300
(1 row)