Postgres Performance e Tunning
Postgres Performance e Tunning
Performance
Tuning
Strategies, best practices, and tips
from Percona PostgreSQL experts
Contents
Introduction 3
Parameters 4
• PostgreSQL Parameters: Scope and Priority Users Should Know �����������������������������������������������������������������������������������������������������������������������������������5
• It’s All About Replication Lag in PostgreSQL ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� 13
• Optimizing Database Parameters for Maximum Efficiency ������������������������������������������������������������������������������������������������������������������������������������������������ 16
Query optimization 21
• Query Optimization with Python and PgBouncer �������������������������������������������������������������������������������������������������������������������������������������������������������������������������22
• Improve PostgreSQL Query Performance Insights with pg_stat_monitor ���������������������������������������������������������������������������������������������������������28
Monitoring 32
• PostgreSQL Checkpoints, Buffers, and WAL Usage with Percona Monitoring and Management ������������������������������������������������33
• Understand Your PostgreSQL Workloads Better with pg_stat_monitor ��������������������������������������������������������������������������������������������������������������� 40
• Monitoring PostgreSQL Databases Using PMM ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ 45
Indexing 54
• PostgreSQL Indexes Can Hurt You: Negative Effects and the Costs Involved �������������������������������������������������������������������������������������������������� 55
• Useful Queries For PostgreSQL Index Maintenance ������������������������������������������������������������������������������������������������������������������������������������������������������������������� 59
Vaccuming 64
• PostgreSQL Vacuuming Command to Optimize Database Performance ������������������������������������������������������������������������� 65
• Why Custom Scheduled Jobs for Vacuuming and Fine-grained Autovacuum Tuning Are Unavoidable �������������������� 73
• Overcoming VACUUM WRAPAROUND �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������78
Partitioning 84
• PostgreSQL Partitioning Using Traditional Methods ������������������������������������������������������������������������������������������������������������������������������������������������������������������ 85
• Partitioning in PostgreSQL With pg_partman (Serial-Based & Trigger-Based) ������������������������������������������������������������������������������������������� 94
Schema design 100
• Diffing PostgreSQL Schema Changes ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������101
PostGIS 112
• Using PostGIS To Enable Better Performance in PostgreSQL ���������������������������������������������������������������������������������������������������������������������������������������������113
Conclusion 129
2
PostgreSQL Performance Tuning
Strategies, best practices, and tips from Percona PostgreSQL experts
3
Parameters
Our first chapter looks at optimizing parameters in PostgreSQL,
highlighting how even minor adjustments can lead to substantial
improvements. Sourced from the seasoned expertise of Percona’s
PostgreSQL professionals, this advice will be your starting point
for parameter tuning to enhance your PostgreSQL database’s
efficiency, stability, and speed.
4
PostgreSQL Parameters:
Scope and Priority Users Should Know
By Jobin Augustine
5
1. Compile time parameter settings
These are the set of parameters that are set at the time of compilation. This acts as the default value for
PostgreSQL. We can check these values in the boot_val field of pg_settings.
These compile time settings have the least priority and can be overridden in any other levels. However, some
of these parameters cannot be modified by any other means. Changing these values at compile time is not
intended for common use. If a PostgreSQL user wants to change these values, they need to recompile the
PostgreSQL from the source code. Some are exposed through the configure command line option.
--with-segsize=<SEGSIZE> This sets table segment size in GB. The default is 1GB. This means PostgreSQL
creates a new file in the data directory as the table size exceeds 1GB.
--with-wal-blocksize=<BLOCKSIZE> sets WAL block size in kB, and the default is 8kB.
Most of the parameters have compile time defaults. That is why we can start running PostgreSQL by
specifying a very minimal number of parameter values.
Parameters can also be specified at the data directory initialization time. Some of these parameters cannot
be changed by other means or are difficult to change.
For example, the wal_segment_size, which determines the WAL segment file, is such a parameter. PostgreSQL
generates WAL segment files of 16MB by default, which can be specified only at initialization. This is the level
at which decisions on whether to use data_checksums need to be taken. This can be changed later using
the pg_checksums utility, but that will be a painful exercise on a big database.
The default character encoding and locale settings can be specified at this level. But this can also be
specified at the subsequent levels. You may refer to initdb options for more information:
[Link]
Those parameters taken from the specific data directory initialization, which overrides the built-in
parameters, can be checked like this:
This override includes some of the calculated auto-tune values for that environment.
6
3. PostgreSQL parameters set by environment variables
PostgreSQL executables, including the postmaster, are honoring many environment variables. But they are
generally used by client tools. The most common parameter used by the PostgreSQL server (postmaster) will
be PGDATA, which sets the parameter data_directory. These parameters can be specified by the service
managers like systemd.
$ cat /usr/lib/systemd/system/[Link]
...
Environment=PGDATA=/var/lib/pgsql/14/data/
…
Environment=PG_OOM_ADJUST_FILE=/proc/self/oom_score_adj
Environment=PG_OOM_ADJUST_VALUE=0
$ export PGDATA=/home/postgres/data
$ export PGPORT=5434
$ pg_ctl start
waiting for server to start....2023-08-04 06:53:09.637 UTC [5787] LOG: starting
PostgreSQL 15.3 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat
4.8.5-44), 64-bit
2023-08-04 06:53:09.637 UTC [5787] LOG: listening on IPv6 address “::1”, port 5434
2023-08-04 06:53:09.637 UTC [5787] LOG: listening on IPv4 address “[Link]”, port 5434
2023-08-04 06:53:09.639 UTC [5787] LOG: listening on Unix socket “/tmp/.[Link].5434”
4. Configuration files
Probably, this is the method every novice user will be aware of. The fundamental configuration file
is [Link], and it is the most common place to have global settings. PostgreSQL looks for a
configuration file in the PGDATA by default, but an alternate location can be specified using the command
line parameter config_file of postmaster. The parameter specifications can be split into multiple files and
directories because Postgresql supports include and include_dir directives in the configuration files. So, there
can be nested/cascaded configurations.
PostgreSQL rereads all its configuration files if it receives a SIGHUP signal. If the same parameter is set in
multiple locations, the last to read will be will be considered. Among all configuration files, [Link].
conf gets the highest priority because that is the file to read the last. That is where all “ALTER SYSTEM SET/
RESET” commands keep the information.
7
5. Command line argument to postmaster
The postmaster, aka Postgres, has a feature to set parameters as command-line arguments (it has features
to get the values also). This is one of the most reliable methods used by many of the external tools to
manage PostgreSQL service. For example, the high availability solution Patroni passes some of the most
critical parameters as a command line argument. Here is how the Postgres process with command-line
options looks in a Patroni environment:
/usr/pgsql-14/bin/postgres -D /var/lib/pgsql/14/data
--config-file=/var/lib/pgsql/14/data/[Link]
--listen_addresses=[Link] --port=5432
--cluster_name=kc_primary_cluster --wal_level=replica
--hot_standby=on --max_connections=100 --max_wal_senders=5
--max_prepared_transactions=0
--max_locks_per_transaction=64 --track_commit_timestamp=off
--max_replication_slots=10 --max_worker_processes=8
--wal_log_hints=on
So Patroni can ensure that there is no local configuration mistake that can adversely affect the availability
and stability of the cluster. However, changing this is possible only at the server startup. Obviously, this has
higher precedence over the values from the configuration files. The scope will be at the instance level. This
answers many of the Patroni user’s questions on why they cannot directly change some of the parameters in
the parameter file. PostgreSQL users can check those parameters which came as command line arguments
like:
Parameters specifications up to this level can have “postmaster” context. The concept of “context” is
discussed in the next section.
8
6. Database level setting
All options discussed so far have a global scope. Meaning they are applicable for the entire instance.
But there could be reasons why a PostgreSQL user wants to change that at a specific database level. For
example, one of the databases might be handling an OLTP workload where query parallelism may not be
really needed and may have an adverse impact. But another database might be an OLAP system.
At this stage, we should recollect another concept called the “context” of a parameter. For example, the
network port at which PostgreSQL listens cannot be changed at the individual database level. A change
of such parameters requires a PostgreSQL restart. So, we say that the context of parameter “ port” is
postmaster. A change to such parameters requires the postmaster — the main process of the PostgreSQL —
to restart.
The max_connections is something the postmaster specifies at the global (instance) level, requiring a
restart.
Other sets of parameters need to be communicated through postmaster only, even though they can be
changed without restarting the server. That context is called sighup. Because we can signal the postmaster,
and it will re-read such parameters and propagate the same to all its child processes, changing them at the
database level will be prevented.
9
7. User-level settings
Each user can have their preferred parameter settings so that all sessions created by that user will have that
setting in place. Please remember that this user-level setting has a higher preference than database-level
settings. Users can check their user-level settings like this:
PostgreSQL allows us to have parameter settings that are applicable when a particular user/role connects to
a particular database.
For example:
Setting at this level has even higher priority than everything mentioned before.
There is an option to specify parameters while making a new connection. It can be passed to PostgreSQL as
part of the connection string.
For example, I want to connect to the database to perform some bulk data loading and manipulation (ETL),
and I don’t want to wait for any WAL writing. If there is any crash in between, I am okay with performing the
ETL again. So, I am going to request a connection with synchronous_commit off.
10
10. Session-level setting
Each session can decide on the settings for that session at that point in time or execution. The sessions are
allowed to modify this session-level setting as and when required.
A good use case is that, suppose we are going to rebuild a big index. We know that it is going to use
considerable maintenance_work_mem. Setting this at the session level simplifies our life without affecting
other sessions.
PostgreSQL allows us to specify parameters at a very small scope, like transactions. Here is an example of
discouraging sequential scans in a particular transaction block.
postgres=# BEGIN;
BEGIN
postgres=*# SET LOCAL enable_seqscan=off;
SET
I prefer transaction-level settings because the changes are very local to the transaction and will be reverted
once the transaction is completed. This is the most preferred place to set the work_mem to minimize the
impact.
11
12. Object-level settings
PostgreSQL allows us to specify the parameter specific to a program block, like a PL/pgSQL function. So, the
setting goes as part of the function definition.
Here is an example of the function definition to test the function-level settings.
PostgreSQL parameter specification is very flexible and powerful, so understanding the scope, context, and
priority is important for every user. A rule of thumb could be the broader the scope, the lower the priority.
12
It’s All About Replication Lag in
PostgreSQL
By Naveed Shaikh
13
What is replication lag?
Replication lag is the time between when data is written to the primary database and when it is replicated
to the standby databases. In PostgreSQL, replication lag can occur due to various reasons such as network
latency, slow disk I/O, long-running transactions, etc.
Replication lag can have serious consequences in high-availability systems where standby databases are
used for failover. If the replication lag is too high, it can result in data loss when failover occurs.
The most common approach is to run a query referencing this view in the primary node.
(1 row)
postgres=#
Network latency: Network latency is the delay caused by the time it takes for data to travel between the pri-
mary and standby databases.
Various factors, such as the distance between the databases, network congestion, etc., can cause this delay:
Slow disk I/O: Slow disk I/O can be caused by various factors such as disk fragmentation, insufficient disk
space, etc. Slow disk I/O can delay writing data to the standby databases.
Long-running transactions: Long-running transactions can cause replication lag because the changes
made by these transactions are not replicated until the transaction is committed.
A poor configuration, like setting low numbers of max_wal_senders while processing huge numbers of
transaction requests.
Sometimes, the server recycles old WAL segments before the backup can finish and cannot find the WAL
segment from the primary.
Usually, this is also due to the checkpointing behavior where WAL segments are rotated or recycled.
There are several ways to mitigate replication lag in PostgreSQL, such as:
14
Increasing the network bandwidth: Increasing the network bandwidth between the primary and standby
databases can help reduce replication lag caused by network latency.
Using asynchronous replication: Asynchronous replication can help reduce replication lag by allowing the
standby databases to lag behind the primary database.
This means the standby databases do not have to wait for the primary database to commit transactions
before replicating the data.
Tuning PostgreSQL configuration parameters: Tuning the PostgreSQL configuration parameters, such as
wal_buffers, max_wal_senders, etc., can help improve replication performance and reduce replication lag.
Monitoring replication lag: Monitoring replication lag can help identify the cause of the lag and take appro-
priate actions to mitigate it.
PostgreSQL provides several tools for monitoring replication lag, such as pg_stat_replication, pg_wal_re-
ceiver_stats, etc.
Conclusion
Replication lag is a common issue in PostgreSQL that can seriously affect high-availability systems.
Understanding the causes of replication lag and taking appropriate measures to mitigate it can help ensure
the availability and reliability of the database system.
By increasing network bandwidth, using asynchronous replication, tuning PostgreSQL configuration param-
eters, and monitoring replication lag, administrators can mitigate replication lag and ensure a more stable
and reliable database environment.
15
Optimizing Database Parameters for
Maximum Efficiency
By Ibrar Ahmed
16
What is PostgreSQL performance tuning?
PostgreSQL performance optimization aims to improve the efficiency of a PostgreSQL database system by
adjusting configurations and implementing best practices to identify and resolve bottlenecks, improve query
speed, and maximize database throughput and responsiveness.
1. Configuration parameter tuning involves altering variables such as memory allocation, disk I/O
settings, and concurrent connections based on specific hardware and requirements.
2. Query optimization: Analyzing query execution plans, identifying slow queries, and optimizing
them through appropriate indexing techniques, query rewriting, or utilizing advanced features like
partial indexes or materialized views.
3. Hardware optimization: You need to ensure that the CPU, memory, and storage components
meet the performance requirements of the database workload.
4. Statistics and monitoring: By enabling and analyzing performance statistics and utilizing moni-
toring tools such as Percona Monitoring and Management, you can identify bottlenecks and track
query performance over time.
5. Index tuning: PostgreSQL offers various types of indexes, including B-tree, hash, and generalized
inverted indexes (GIN/GiST). Selecting the appropriate index type, creating composite indexes,
and regularly analyzing and reindexing the database can substantially improve query speed.
6. Schema design: Evaluating the database schema design and making adjustments such as
partitioning large tables, eliminating redundant data, and denormalizing tables for frequently
accessed information can improve performance.
7. Connection pooling: Minimizing connection overhead and improving response times for fre-
quently accessed data by implementing mechanisms for connection pooling and caching strat-
egies.
8. Load balancing and replication: Scaling the database system horizontally by distributing the
workload across multiple servers using techniques like connection pooling, read replicas, or im-
plementing a primary-replica replication setup.
9. Memory management: Since PostgreSQL’s performance relies heavily on efficient memory us-
age, monitoring and adjusting key memory-related configuration parameters like shared_buf-
fers, work_mem, and effective_cache_size can significantly impact performance.
The performance of a PostgreSQL database has a significant impact on the overall effectiveness of an appli-
cation. Here’s why it’s crucial for businesses to possess a high-performing database:
Responsiveness: Imagine waiting forever for the data you need to show up immediately. Nobody has time
for that! Slow queries and data retrieval can lead to frustrating delays that impact the user experience. A
well-performing database ensures seamless interactions, quick response times, and satisfies user expecta-
tions.
Throughput: The throughput of an application is directly influenced by the speed at which the database can
process and serve queries. A high-performance database significantly decreases query execution time, em-
powering the application to handle more concurrent requests and deliver data faster.
Scalability: As an application expands and needs to handle more data and user loads, the database must
scale accordingly. A well-performing PostgreSQL database can effectively manage increased workloads,
ensuring the application remains responsive and performs well even when under heavy usage.
Efficient Resource Utilization: Enabling the effective utilization of system resources like CPU, memory, and disk
I/O can optimize your PostgreSQL database while maintaining functionality. This not only results in cost sav-
17
ings by minimizing hardware requirements but also has the potential to decrease cloud expenses.
Data Integrity: This is crucial for ensuring a reliable and efficient database. By incorporating features like
ACID compliance, transaction management, and strong error handling, the database can safeguard against
data corruption or loss. This is particularly important for businesses that rely on precise and consistent data
for their decision-making and operational needs.
Competitive Advantage: User retention rates and customer satisfaction can make or break any business. A
high-performing database that consistently ensures excellent application performance can give businesses
a competitive advantage.
Bear in mind that while optimizing PostgreSQL server configuration improves performance, a database
developer must also be diligent when writing queries for the application. Suppose queries perform full table
scans where an index could be used or perform heavy joins or expensive aggregate operations. In that case,
the system can still perform poorly even if the database parameters are tuned. It is essential to pay attention
to performance when writing database queries.
Nevertheless, database parameters are very important, too, so let’s look at the eight that have the most sig-
nificant potential to improve performance.
Below are some PostgreSQL parameters that can be adjusted for improved performance based on your
system and specific workload.
shared_buffer
PostgreSQL uses its own buffer and also uses kernel-buffered IO. That means data is stored in memory twice,
first in the PostgreSQL buffer and then in the kernel buffer. Unlike other databases, PostgreSQL does not pro-
vide direct IO. This is called double buffering. The PostgreSQL buffer is called shared_buffer, which is the most
effective tunable parameter for most operating systems. This parameter sets how much dedicated memory
will be used by PostgreSQL for the cache.
The default value of shared_buffer is set very low, and you will not benefit much from that. It’s low because
certain machines and operating systems do not support higher values. But in most modern machines, you
need to increase this value for optimal performance.
The recommended value is 25% of your total machine RAM. Try some lower and higher values because, in
some cases, we achieve good performance with a setting over 25%. The configuration really depends on
your machine and the working data set. If your working set of data can easily fit into your RAM, then you
might want to increase the shared_buffer value to contain your entire database so that the whole working
set of data can reside in the cache. That said, you obviously do not want to reserve all RAM for PostgreSQL.
In production environments, it is observed that a large value for shared_buffer gives excellent performance,
though you should always benchmark to find the right balance.
18
Note: Be careful; some kernels do not allow a bigger value. Specifically, in Windows, there is no use of a high-
er value.
wal_buffers
PostgreSQL writes its WAL (write-ahead log) record into the buffers, which are then flushed to disk. The default
size of the buffer, defined by wal_buffers, is 16MB, but if you have many concurrent connections, a higher val-
ue can give better performance.
effective_cache_size
The effective_cache_size provides an estimate of the memory available for disk caching. It is just a guide-
line, not the exact allocated memory or cache size. It does not allocate actual memory but tells the optimizer
the amount of cache available in the kernel. If the value of this is set too low, the query planner can decide
not to use some indexes, even if they’d be helpful. Therefore, setting a large value is always beneficial.
work_mem
This configuration is used for complex sorting. If you have to do complex sorting, increase the value of work_
mem for good results. In-memory sorts are much faster than sorts spilling to disk. Setting a very high value
can cause a memory bottleneck for your deployment environment because this parameter is per-user sort
operation. Therefore, if you have many users trying to execute sort operations, then the system will allocate
work_mem * total sort operations for all users. Setting this parameter globally can cause very high memory
usage. So, it is highly recommended to modify this at the session level.
The initial query’s sort node has an estimated cost of 514431.86. A cost is an arbitrary
unit of computation. For the above query, we have a work_mem of only 2MB. For testing
purposes, let’s increase this to 256MB and see if there is any impact on cost.
19
maintenance_work_mem
maintenance_work_mem is a memory setting used for maintenance tasks. The default value is 64MB. Set-
ting a large value helps in tasks like VACUUM, RESTORE, CREATE INDEX, ADD FOREIGN KEY, and ALTER TABLE.
postgres=# CHECKPOINT;
postgres=# SET maintenance_work_mem to ‘10MB’;
postgres=# CHECKPOINT;
postgres=# set maintenance_work_mem to ‘256MB’;
The index creation time is 170091.371ms when maintenance_work_mem is set to only 10MB, but that is re-
duced to 111274.903 ms when we increase maintenance_work_mem setting to 256MB.
synchronous_commit
This is used to enforce that the commit will wait for WAL to be written on disk before returning a success sta-
tus to the client. This is a trade-off between performance and reliability. If your application is designed such
that performance is more important than reliability, then turn off synchronous_commit. This means there
will be a time gap between the success status and a guaranteed write-to-disk. In the case of a server crash,
data might be lost even though the client received a success message on commit. In this case, a transac-
tion commits very quickly because it will not wait for a WAL file to be flushed, but reliability is compromised.
checkpoint_timeout, checkpoint_completion_target
PostgreSQL writes changes into WAL. The checkpoint process flushes the data into the data files. This activity
is done when CHECKPOINT occurs. This is an expensive operation and can cause a huge amount of IO. This
whole process involves expensive disk read/write operations. Users can always issue CHECKPOINT whenev-
er necessary or automate the system by PostgreSQL’s parameters checkpoint_timeout and checkpoint_
completion_target.
The checkpoint_timeout parameter is used to set the time between WAL checkpoints. Setting this too low
decreases crash recovery time, as more data is written to disk, but it also hurts performance since every
checkpoint consumes valuable system resources. The checkpoint_completion_target is the fraction of time
between checkpoints for checkpoint completion. A high frequency of checkpoints can impact performance.
For smooth checkpointing, checkpoint_timeout must be a low value. Otherwise, the OS will accumulate all
the dirty pages until the ratio is met and then go for a big flush.
20
Query optimization
In the pursuit of peak database performance, the optimization of
queries is a vital area of focus. In this chapter, our experts discuss
some techniques and tools for query optimization in PostgreSQL,
ensuring your database operates optimally.
21
Query Optimization with Python and
PgBouncer
By Mario García
Here, you will learn how to install and configure PgBouncer with
Python to implement a connection pool for your application.
22
PgBouncer
PgBouncer is a PostgreSQL connection pooler. Any target application can be connected to PgBouncer as if it
were a PostgreSQL server, and PgBouncer will create a connection to the actual server, or it will reuse one of
its existing connections.
The aim of PgBouncer is to lower the performance impact of opening new connections to PostgreSQL.
Installation
If you’re an Ubuntu user, you can install PgBouncer from the repositories:
If not available in the repositories, you can follow the instructions below for both Debian and Ubuntu as
mentioned in the Scaleway documentation
23
Configuration
After installing PgBouncer, edit the configuration files, as stated in the Scaleway documentation.
You may also want to set listen_addr to * if you want to listen to TCP connections on all addresses or set a
list of IP addresses.
From this article by Abdullah Alger, the settings max_client_conn and default_pool_size, the former refer to
the number of applications that will make connections, and the latter is how many server connections per
database. The defaults are set at 100 and 20, respectively.
“username” “password”
3. Add the IP address of the PgBouncer server to the PostgreSQL pg_hba.conf file
By default, PgBouncer comes with a trust authentication method. The trust method can be used in a devel-
opment environment but is not recommended for production. For production, HBA authentication is recom-
mended.
4. After configuring PgBouncer, restart both the PostgreSQL and PgBouncer services
24
Python
Requirements
Dependencies
Make sure all the dependencies are installed before creating the Python script that will generate the data
for your Project. You can create a [Link] file with the following content:
tqdm
faker
psycopg2
name: percona
dependencies:
- python=3.10
- tqdm
- faker
- psycopg2
You can change the Python version as this script has been proven to work with these versions of Python: 3.7,
3.8, 3.9, 3.10, and 3.11.
Or run the following statement to configure the project environment when using Anaconda:
Database
Now that you have the dependencies installed, you must create a database named company.
Log into PostgreSQL:
$ sudo su postgres
$ psql
25
And create the employee’s table:
Inserting data
Now it’s time to create the Python script that will generate the data and insert it into the database.
26
At first, the multiprocessing pool is created and configured to use all available CPU cores minus one. Each
core will call the insert_data() function.
On each call to the function, a connection to the database will be established through the default port
(6432) of PgBouncer, meaning that the application will open a number of connections equal to num_cores,
a variable that contains the number of CPU cores being used.
Then, the data will be generated with Faker and inserted into the database by executing the corresponding
SQL statements.
In a CPU with 16 cores, the number of records inserted into the database on each call to the function will
equal 60,000 divided by 15, that is 4,000 SQL statements executed.
This way, you can modify and optimize the script by configuring a connection pool with PgBouncer.
27
Improve PostgreSQL Query Performance
Insights with pg_stat_monitor
By Ibrar Ahmed
Hamid Akhtar
28
Some useful extensions
Currently, you may be relying on a number of extensions to understand how a query behaves, the time
taken in planning and execution phases, min/max/meantime values, index hits, query plan, and client
application details. Here are some extensions that you might already be very familiar with.
pg_stat_activity
This view is available by default with PostgreSQL. It provides one row per server process along with current
activity and query text.
In case you’d like to learn more about it, hop over to the official PostgreSQL documentation here.
pg_stat_statements
This extension is part of the contrib packages provided with the PostgreSQL server. However, you’d have
to create the extension manually. It’s a query-wise aggregation of statistical data with min/max/mean/
standard deviation for execution and planning times and various useful information and query text.
You can read more about pg_stat_statements at the official PostgreSQL documentation site.
auto_explain
Another useful extension is provided by the PostgreSQL server. It dumps query plans in the server log for any
query exceeding a time threshold specified by a GUC
(Grand Unified Configuration).
pg_stat_monitor
While all previously mentioned views/extensions are great in their own right, one needs to manually
combine client/connection information from pg_stat_activity, statistical data from pg_stat_statements,
and query plan from auto_analyze to complete the dataset to understand query performance patterns.
The feature set has been growing over the past year, with it providing, in a single view, all performance-
related information that you may need to debug a low performant query. For more information about the
extension, see our GitHub repository, or for user-specific documentation, see our user guide.
Feature set
• Time Interval Grouping: Instead of supplying one set of ever-increasing counts, pg_stat_monitor
computes stats for a configured number of time intervals and time buckets. This allows for much better
data accuracy, especially in the case of high-resolution or unreliable networks.
29
This allows you to drill down into the performance of queries coming from particular client addresses and
applications, which we at Percona have found to be very valuable in a number of cases.
• Capture actual parameters in the queries: pg_stat_monitor allows you to choose if you want to see
queries with placeholders for parameters or actual query examples.
• Query plan: Each SQL is now accompanied by its actual plan that was constructed for its execution.
Also, we found having query parameter values is very helpful, as you can run EXPLAIN on it or easily play
with modifying the query to make it run better and make communication about the query clearer when
discussing with other DBAs and application developers.
• Tables Access Statistics for a Statement: This allows us to easily identify all queries that accessed a
given table. This set is at par with the information provided by the pg_stat_statements.
• Histogram: Visual representation is very helpful when it can help identify issues. With the help of the
histogram function, you can now view a timing/calling data histogram in response to an SQL query. And
yes, it even works in PostgreSQL.
SELECT * FROM histogram(0, ‘F44CD1B4B33A47AF’) AS a(range TEXT, freq INT, bar TEXT);
range | freq | bar
--------------------+------+--------------------------------
(0 - 3)} | 2 | ■■■■■■■■■■■■■■■■■■■■■■■■
(3 - 10)} | 0 |
(10 - 31)} | 1 | ■■■■■■■■■■■■
(31 - 100)} | 0 |
(100 - 316)} | 0 |
(316 - 1000)} | 0 |
(1000 - 3162)} | 0 |
(3162 - 10000)} | 0 |
(10000 - 31622)} | 0 |
(31622 - 100000)} | 0 |
(10 rows)
• Functions: It may come as a surprise, but we understand that functions may internally execute
statements!!! To help ease the tracking and analysis, pg_stat_monitor now provides a column
that specifically helps keep track of the top query for a statement so that you can backtrack to the
originating function.
• Relation Names: Relations used in a query are available in the “relations” column in the pg_stat_
monitor view. This reduces work at your and makes analysis simpler and quicker.
• Query Types: With query classification as SELECT, INSERT, UPDATE, or DELETE, analysis becomes simpler. It’s
another effort reduced at your end and another simplification by pg_stat_monitor.
30
SELECT bucket, substr(query,0, 50) AS query, cmd_type FROM pg_stat_monitor WHERE elevel = 0;
bucket | query | cmd_type
--------+---------------------------------------------------+----------
4 | END |
4 | SELECT abalance FROM pgbench_accounts WHERE aid = | SELECT
4 | vacuum pgbench_branches |
4 | select count(*) from pgbench_branches | SELECT
4 | UPDATE pgbench_accounts SET abalance = abalance + | UPDATE
4 | truncate pgbench_history |
4 | INSERT INTO pgbench_history (tid, bid, aid, delta | INSERT
• Query metadata: Google’s Sqlcommenter is a useful tool that, in a way, bridges that gap between ORM
libraries and understanding database performance. And we support it. So, you can now put any key
value data in the comments in /* … */ syntax (see Sqlcommenter documentation for details) in your
SQL statements, and the information will be parsed by pg_stat_monitor and made available in the
comments column in pg_stat_monitor view.
• Logging Error and Warning: As seen in different monitoring/statics collector tools, most tools/extensions
only monitor successful queries. But in many cases, monitoring ERROR, WARNING, and LOG gives
meaningful information to debug the issue. pg_stat_monitor not only monitors the ERROR/WARNINGS/
LOG but also collects the statistics about these queries. In PostgreSQL queries with ERROR/WARNING,
there is an error level (elevel), SQL Code (sqlcode), and an error message is attached. Pg_stat_monitor
collects all this information along with its aggregates.
The pg_stat_monitor extension has evolved and has become very feature-rich. We do not doubt its
usefulness for DBAs, performance engineers, application developers, and anyone who needs to look at
query performance. We believe it can help save many hours and help identify unexpected query behaviors.
pg_stat_monitor is available on GitHub.
31
Monitoring
Effective monitoring is key to ensuring your PostgreSQL database
performs at its best. With insights from Percona’s PostgreSQL
experts, this chapter guides you through the essential strategies for
understanding and fine-tuning your database system.
Whether you are looking for ways to monitor in real time, analyze
long-term trends, or drill down into specific performance issues, the
insights provided here will equip you with the knowledge to deploy
effective monitoring solutions.
32
PostgreSQL Checkpoints, Buffers, and
WAL Usage with Percona Monitoring
and Management
By Agustín Gallego
33
Creating a custom query collector
Note: This step will not be needed with soon-to-come postgresql_exporter PMM versions!
The first step is to include a new query to gather data on WAL usage because this is not yet collected by
default on the latest version of PMM. However, do note that only three graphs will use these custom metrics,
so if you want to try out the dashboard without doing this customization, it’s also possible for you to do it (at
the expense of having a bit less data).
For this, it’s as easy as executing the following (in the PMM client node that is monitoring the Postgres
servers):
cd /usr/local/percona/pmm2/collectors/custom-queries/postgresql/high-resolution/
cat<<EOF >[Link]
# ######################################################
# This query gets information related to WAL LSN numbers in bytes,
# which can be used to monitor the write workload.
# Returns a COUNTER: [Link]
# ######################################################
pg_wal_lsn:
query: “SELECT pg_current_wal_lsn() - ‘0/0’ AS bytes, pg_walfile_name(pg_current_wal_lsn()) as
file, pg_current_wal_insert_lsn() - ‘0/0’ AS insert_bytes, pg_current_wal_flush_lsn() - ‘0/0’ AS
flush_bytes”
metrics:
- bytes:
usage: “COUNTER”
description: “WAL LSN (log sequence number) in bytes.”
- file:
usage: “COUNTER”
description: “WAL file name being written to.”
- insert_bytes:
usage: “COUNTER”
description: “WAL insert LSN (log sequence number) in bytes.”
- flush_bytes:
usage: “COUNTER”
description: “WAL flush LSN (log sequence number) in bytes.”
EOF
chown pmm-agent:pmm-agent [Link]
chmod 660 [Link]
pg_wal_lsn_bytes
pg_wal_lsn_file
pg_wal_lsn_insert_bytes
pg_wal_lsn_flush_bytes
34
Importing the custom PMM dashboard
To view these new metrics, we will import a new custom PMM dashboard that will provide this information
in a structured and easy-to-understand way. This can be done in two easy steps (since I’ve uploaded it to
Grafana Labs).
35
After this, you’ll be taken to the dashboard, which I generally “star” for easy access later on:
The dashboard is divided into three main areas, with relevant configurations at the top and graphs following
them. The main idea is to easily detect what the server is doing at any point in time so we can better
understand how it reacts to the workload and tune accordingly. These graphs and panels show changes in
usage and workload patterns and configuration changes, which can help during root cause analysis and
performance reviews.
Checkpointing section
Here, we will find everything related to checkpointing: when are checkpoints started (and due to what) and
when they finish. Checkpointing has a lot of impact on the write throughput/utilization by Postgres, so it’s
important to make sure that it’s not being triggered before needed.
In the following example, we can see how at the beginning, there were many instances of forced
checkpointing, not only because the checkpointer was running more often than checkpoint_timeout
seconds but because of the kind of metric we can see in the Checkpoints graph (requested vs. scheduled).
We can’t see it yet because the WAL graphs are at the bottom, but this was fixed after we increased the
value for max_wal_size.
36
Additionally, after the metrics refresh, we can see that the Checkpoint Timeout stat panel shows another
change closer to the end of our currently selected time range:
This means that we have also changed the checkpoint_timeout value. This was done only after our workload
was more stable and there were no more forced checkpoints being issued.
Buffers section
This section holds configurations and metrics related to reads to and writes from the shared buffers. Again,
demonstrating it by example, let’s see how much of an impact correctly sizing the shared_buffers has on our
test workload. It’s set at 128Mb by default, which is hardly enough for any serious workload and will mean that
we should see a lot of churn in both reads and writes until we increase it. The only downside is that modifying
shared_buffers needs a restart, so before doing that, we can increase bgwriter_lru_maxpages (which
doesn’t need a restart) to avoid the writer stopping each round and get a bit more performance out of it at
the expense of I/O.
We can clearly see how, at first, there were constant reads into the shared buffers, and the background
writer was doing most of the writes. Additionally, we can see the positive impact on increasing bgwriter_lru_
maxpages because that process is not being throttled anymore. After we increased shared_buffers and
restarted Postgres, the reads decreased to almost having to read nothing from the page cache or disks, and
the writes are no longer done by the background writer but by the checkpointer.
37
WAL usage section
The last section pertains to write-ahead logs, another source of potential performance bottlenecks. Note
that (for now) this is the only section that needs data from the custom query collector we added at the
beginning, so you can use the rest of the dashboard even if you decide not to include it. PMM already collects
the configurations, so we will only miss data from the graphs.
The WAL Writes Per Checkpoint Timeout graph will group the number of bytes written in chunks of
checkpoint_timeout seconds so we can clearly see the expected max_wal_size value. If the chunks
(shown as orange bars) exceed the max_wal_size (shown as a red line), it means that there will be forced
checkpointing. We can easily dimension max_wal_size knowing that it should be larger than any orange
bar, and we can tell when exactly it was changed, following changes in the red line. The WAL Writes graph
uses the same metric but is shown as a rate, which can help us pinpoint heavy write times more granularly.
Lastly, the WAL Locations graph is included for completeness and shows the different locations for WAL
pointers (such as insertion and flush locations), which, according to the documentation, are mainly used for
debugging purposes.
Most graphs contain additional information and links on either the configuration variables or the metrics they
show, so they are useful in case we need a bit of help interpreting them. Just hover the mouse over the “i”
icon on each of them:
38
Getting even more data
Starting from PostgreSQL 15, we have a new view on WAL metrics: pg_stat_wal.
The metrics used in this custom collector and dashboard will work for older versions, but in the future, it will
be nice to see what data we can extract from this new view and how we can use it to tune our servers.
Conclusion
This is another example of how tunable and powerful PMM is. Not only because we can add our custom
metrics easily but also because it already collects many metrics we can use out of the box. Additionally, it’s
easy to share new PMM dashboards via the Grafana Labs page by simply publishing it and sharing the ID
number. With all this new information, we can better tune our PostgreSQL instances!
39
Understand Your PostgreSQL Workloads
Better with pg_stat_monitor
By Matt Yonkovit
Matt was the Head of Open Source Strategy at Percona until 2022,
overseeing the company’s community strategy and work around open
source.
Percona has long been associated with pushing the limits and
understanding the nuances involved in running database systems
at scale, so building a tool that helps get us there brings a bit
more insight and details around query performance and scale on
PostgreSQL systems fits with our history. So what the hell does
pg_stat_monitor do, and why should you care? Excellent question!
40
Currently, the de facto standard for collecting and reviewing query metrics is pg_stat_statements. This
extension collects query metrics and allows you to go back and see which queries have impacted your sys-
tem. Querying the extension would yield something like this:
postgres=# dx
List of installed extensions
-[ RECORD 1 ]-----------------------------------------------------------------------
Name | pg_stat_statements
Version | 1.8
Schema | public
Description | track planning and execution statistics of all SQL statements executed
-[ RECORD 2 ]-----------------------------------------------------------------------
Name | plpgsql
Version | 1.0
Schema | pg_catalog
Description | PL/pgSQL procedural language
postgres=# x
Expanded display is on.
postgres=# select * from pg_stat_statements;
-[ RECORD 2 ]-------+--------------------------------------------------------
userid | 16384
dbid | 16608
queryid | -7945632213382375966
query | select ai_myid, imdb_id, year, title, json_column from movies_normalized_meta
where ai_myid = $1
plans | 0
total_plan_time | 0
min_plan_time | 0
max_plan_time | 0
mean_plan_time | 0
stddev_plan_time | 0
calls | 61559
total_exec_time | 27326.783784999938
min_exec_time | 0.062153
max_exec_time | 268.55287599999997
mean_exec_time | 0.44391208084927075
stddev_exec_time | 2.522740928486301
rows | 61559
shared_blks_hit | 719441
shared_blks_read | 1031
shared_blks_dirtied | 0
shared_blks_written | 0
local_blks_hit | 0
local_blks_read | 0
local_blks_dirtied | 0
local_blks_written | 0
temp_blks_read | 0
temp_blks_written | 0
blk_read_time | 0
blk_write_time | 0
wal_records | 6
wal_fpi | 0
wal_bytes | 336
You can see here that this particular statement has been executed 61,559 times, with a total time of 27,326
Milliseconds, for a mean time of 0.44 MS.
You can also get metrics on if this statement is writing data, generating wal, etc. This is valuable to help find
what statement may be missing cache and hitting the disk, or which statements may be blowing up your
wal logs.
41
While this data is great, it could be better. Specifically, it’s hard to determine if problems are worsening or
improving. Also, what if that particular query that executed 61K times runs in .01ms 60K times and 1000 ms 1K
times? Collecting enough data here to make better, more targeted decisions around optimization is need-
ed. This is where pg_stat_monitor can help.
First, let me show you the output from one of the collected queries (note I am only selecting a single bucket;
more on that in a second):
postgres=#
postgres=# x
Expanded display is on.
postgres=# select * from pg_stat_monitor ;
-[ RECORD 1 ]-------+---------
bucket | 3
bucket_start_time | 2022-04-27 20:13:00
userid | movie_json_user
datname | movie_json_test
client_ip | [Link]
queryid | 82650C255980E05
top_queryid |
query | select ai_myid, imdb_id, year, title, json_column from movies_normalized_meta where
ai_myid = $1
comments |
planid |
query_plan |
top_query |
application_name |
relations | {public.movies_normalized_meta}
cmd_type | 1
cmd_type_text | SELECT
elevel | 0
sqlcode |
message |
calls | 18636
total_exec_time | 9022.0356
min_exec_time | 0.055
max_exec_time | 60.7575
mean_exec_time | 0.4841
stddev_exec_time | 1.568
rows_retrieved | 18636
plans_calls | 0
total_plan_time | 0
min_plan_time | 0
max_plan_time | 0
mean_plan_time | 0
stddev_plan_time | 0
shared_blks_hit | 215919
shared_blks_read | 1
shared_blks_dirtied | 39
shared_blks_written | 0
local_blks_hit | 0
local_blks_read | 0
local_blks_dirtied | 0
local_blks_written | 0
temp_blks_read | 0
temp_blks_written | 0
blk_read_time | 0
blk_write_time | 0
resp_calls | {17946,629,55,6,0,0,0,0,0,0}
cpu_user_time | 3168.0737
cpu_sys_time | 1673.599
wal_records | 9
wal_fpi | 0
wal_bytes | 528
state_code | 3
state | FINISHED
42
You can see there is a lot of extra data. Let’s view these side by side:
[Link]
monitor/REL1_0_STABLE/USER_GUIDE.
html
43
However, I want to highlight a few of the new metrics and capabilities I am most excited about. For me, the
most interesting is the ability to collect histogram data. This enables you to see if queries that deviate from
the normal. One of the key things our support engineers are always looking at is how is the P99 latency, and
this helps with that. You can see Percona Monitoring and Management take advantage of these features
here:
With the histograms enabled, I can see and help track down where queries and performance deviate from
the normal.
Additionally, you will notice the inclusion of CPU time. Why is this important? Query timings include things
like waiting on disk and network resources. If you have a system with a CPU bottleneck, the queries taking
the longest time may or may not be the offender.
Finally, you can configure pg_stat_monitor to store explain plans from previously run queries. This is in-
credibly useful when plans change over time, and you are trying to recreate what took place an hour or two
ago.
Gaining additional insights and understanding your workload is critical, and pg_stat_monitor can help you
do both. pg_stat_monitor enables end-to-end traceability, aggregated stats across configurable time
windows, and query-wise execution time. Still, PMM visualizes this and gives the user more insight into Post-
greSQL behavior.
Want to try this out for yourself? The instructions are available here:
[Link]
itories
44
Monitoring PostgreSQL Databases
Using PMM
By Avinash Vallarapu
Avinash joined Percona in May 2018 and was PostgreSQL Tech Lead -
Global Services until 2021. Before joining Percona, Avi worked as a Database
Architect at OpenSCG for two years and as a DBA Lead at Dell for 10 Years in
Database technologies.
If you already know how to create a PMM Server, please skip the PMM
server setup and proceed to the PostgreSQL client setup.
45
Using the PMM docker image to create a PMM server
PMM is a client-server architecture where clients are the PostgreSQL, MySQL, or MongoDB databases and the
server is the PMM Server. We see a list of metrics on the Grafana dashboard by connecting to the PMM serv-
er on the UI. To demonstrate this setup, I have created two virtual machines, one of them the PMM Server
and the second server the PostgreSQL database server.
[Link] is my PMM-Server
[Link] is my PG 11 Server
Step one:
On the PMM Server, install and start docker.
Step two:
Pull the pmm-server docker image. I am using the latest PMM2 docker image for this setup.
You see a docker image of size 1.48 GB downloaded after the above step.
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
[Link]/percona/pmm-server 2 cd30e7343bb1 2 weeks ago 1.48 GB
Step three:
Create a container for persistent PMM data.
$ docker create
-v /srv
--name pmm-data
percona/pmm-server:2 /bin/true
Step four:
Create and launch the PMM Server. In the following step, you can see that we are binding the port 80 of the
container to the port 80 of the host machine, likewise for port 443.
$ docker run -d
-p 80:80
-p 443:443
--volumes-from pmm-data
--name pmm-server
--restart always
percona/pmm-server:2
46
At this stage, you can modify certain settings, such as the memory you wish to allocate to the container or
the CPU share, etc. You can also see more such configurable options using docker run --help. The following
is just an example of how you can modify the above step with some memory or CPU allocations.
$ docker run -d
-p 80:80
-p 443:443
--volumes-from pmm-data
--name pmm-server
--cpu-shares 100
--memory 1024m
--restart always
percona/pmm-server:2
You can list the containers started for validation using docker ps.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bb6043082d3b percona/pmm-server:2 “/opt/[Link]” About a minute ago Up About a
minute [Link]:80->80/tcp, [Link]:443->443/tcp pmm-server
Step five:
You can now see the PMM Server Dashboard in the browser using the Host IP address. For my setup, the PMM
Server’s IP Address is [Link]. As soon as you put the IP in the browser, you will be asked to enter the
credentials as seen in the image below. The default user and password are both: admin
47
Installing PMM client on a remote PostgreSQL server
I have a PostgreSQL 11.5 Server running on [Link]. The following steps demonstrate how we can install
and configure the PMM client to enable monitoring from the PMM server ( [Link]).
Before proceeding further, you must ensure that ports 80 and 443 are enabled on the PMM server for the PG
11 Server to connect. In order to test that, I have used telnet to validate whether ports 80 and 443 are open
on the PMM Server for the pg11 server.
[root@pg11]$ hostname -I
[Link]
Step six:
There are very few steps you need to perform on the PostgreSQL server to enable it as a client for PMM
server. The first step is to install the PMM Client on the PostgreSQL Database server as follows. Based on the
current PMM release, I am installing pmm2-client today. But this may change once we have a new PMM
release.
Step seven:
The next step is to connect the client (PostgreSQL server) to the PMM Server. We could use pmm-admin
config to achieve that. Following is a simple syntax that you could use in general.
The following are the flags and other options I could use with my setup.
flags : --server-insecure-tls
--server-url=[Link]
(--server-url should contain the PMM Server Host information)
node-address : [Link]
(My PostgreSQL Server)
node-type : generic
(As I am running my PostgreSQL database on a Virtual Machine but not on a Container,
it is generic.)
node-name : pg-client
(Can be any nodename you could use to uniquely identify this database server on your
PMM Server Dashboard)
48
So, the final syntax for my setup looks like the below. We can run this command as root or by using the sudo
command.
Syntax: 7a
Syntax: 7b
You could also use a simple syntax such as following without node-address, node-type, node-name :
But when you use such a simple syntax as above, node-address, node-type, node-name are defaulted to
certain values. Suppose the defaults are incorrect due to your server configuration. In that case, you may
better pass these details explicitly like I have done in the syntax: 7a. To validate whether the defaults are
correct, you can simply use # pmm-admin config --help. In the following log, you see that the node-ad-
dress defaults to [Link], which is incorrect for my setup. It should be [Link].
Flags:
-h, --help Show context-sensitive help (also try --help-long and
--help-man)
--version Show application version
...
...
...
Args:
[<node-address>] Node address (autodetected default: [Link])
Below is an example where the default settings were perfect because I had configured my database server
the right way.
49
# pmm-admin config --help
usage: pmm-admin config [<flags>] [<node-address>] [<node-type>] [<node-name>]
Flags:
-h, --help Show context-sensitive help (also try --help-long and
--help-man)
...
...
Args:
[<node-address>] Node address (autodetected default: [Link])
[<node-type>] Node type, one of: generic, container (default: generic)
[<node-name>] Node name (autodetected default: pg-client)
Using steps 6 and 7a, I finished installing the PMM client on the PostgreSQL server and connected it to the
PMM Server. If the above steps are successful, you should see the client listed under Nodes, as seen in the
following image. Else, something went wrong.
In order to monitor your PostgreSQL server using PMM, you need to create a user *using*, which the data-
base stats can be collected by the PMM agent. However, starting from PostgreSQL 10, you do not need to
grant SUPERUSER or use SECURITY DEFINER (to avoid granting SUPERUSER). You can simply grant the role
pg_monitor to a user (monitoring user).
Assuming that your PostgreSQL Version is ten or higher, you can use the following steps.
Step one:
Create a Postgres user that can be used for monitoring. You could choose any username; pmm_user in the
following command is just an example.
Step two:
Grant pg_monitor role to the pmm_user.
50
$ psql -c “GRANT pg_monitor to pmm_user”
Step three:
If you are not using localhost, but using the IP address of the PostgreSQL server while enabling monitoring
in the next steps, you should ensure to add appropriate entries to enable connections from the IP and the
pmm_user in the pg_hba.conf file.
In the above step, replace [Link] with the appropriate PostgreSQL Server’s IP address.
Step four:
Validate whether you are able to connect as pmm_user to the Postgres database from the Postgres server
itself.
Using PMM, we can monitor several metrics in PostgreSQL, such as database connections, locks, checkpoint
stats, transactions, temp usage, etc. However, you could additionally enable Query Analytics to look at the
query performance and understand the queries that need some tuning. Let us see how we can simply en-
able PostgreSQL monitoring with and without QAN.
Without QAN
Step one:
To start monitoring PostgreSQL, we could simply use pmm-admin to add PostgreSQL. It accepts additional
arguments such as the service name and PostgreSQL address and port. As we are talking about enabling
monitoring without QAN, we could use the flag: --query-source=none to disable QAN.
Step two:
Once you have enabled monitoring, you could validate the same using pmm-admin list.
51
# pmm-admin list
Service type Service name Address and port Service ID
PostgreSQL postgres [Link]:5432 /service_id/b2ca71cf-a2a4-48e3-
9c5b-6ecd1a596aea
You can now access the PostgreSQL Dashboards and see several metrics being monitored.
With QAN
With PMM2, an additional step is needed to enable QAN. You should create a database with the same name
as the monitoring user ( pmm_user here). And then, you should create the extension pg_stat_statements
in that database. This behavior is going to change on the next release so that you can avoid creating the
database.
Step one:
Create the database with the same name as the monitoring user. Create the extension pg_stat_state-
ments in the database.
Step two:
If shared_preload_libraries has not been set to pg_stat_statements, we need to set it and restart Post-
greSQL.
Step three:
In the previous steps, we used the flag: --query-source=none to disable QAN. To enable QAN, you could just
remove this flag and use pmm-admin to add PostgreSQL without the flag.
52
# pmm-admin add postgresql --username=pmm_user --password=secret postgres
[Link]:5432
PostgreSQL Service added.
Service ID : /service_id/24efa8b2-02c2-4a39-8543-d5fd54314f73
Service name: postgres
Step four:
Once the above step is completed, you could validate the same again using pmm-admin list. But this time,
you should see an additional service: qan-postgresql-pgstatements-agent.
# pmm-admin list
Service type Service name Address and port Service ID
PostgreSQL postgres [Link]:5432 /service_id/24efa8b2-02c2-4a39-
8543-d5fd54314f73
After this step, you can see the Queries and their statistics captured on the Query Analytics Dashboard.
53
Indexing
PostgreSQL indexing is a feature that allows PostgreSQL to quickly
retrieve data without having to scan every row in a table. Indexes
are created on one or more columns of a table and provide a fast
path to locate rows matching a query condition. They are essential
for optimizing search operations, especially in large databases, by
reducing the amount of data that needs to be examined.
54
PostgreSQL Indexes Can Hurt You:
Negative Effects and the Costs Involved
By Jobin Augustine
At a very basic level, we should remember that indexes are not free
of cost. The benefits come with a cost in terms of performance and
resource consumption. The following is the list of ten problems/
overheads that the excessive use of indexes can cause. This post
is about PostgreSQL, but most of the problems also apply to other
database systems.
55
1. Indexes penalize the transactions
We might see an improvement in the performance of a SELECT statement after adding an index. But we
should not forget that the performance gains come with a cost to the transactions on the same table. Con-
ceptually, every DML on a table needs to update all the indexes of the table. Even though there are a lot of
optimizations for reducing the write amplification, it is a considerable overhead.
For example, assume there are five indexes on a table; every INSERT into the table will result in an INSERT of
the index record on those five indexes. Logically, five index pages will also be updated. So effectively, the
overhead is 5x.
2. Memory usage
Index pages must be in memory, regardless of whether any query uses them, because they need to get up-
dated by transactions. Effectively, the memory available for pages of the table gets less. The more indexes,
the more the requirement of memory for effective caching. If we don’t increase the available memory, this
starts hurting the entire performance of the system.
Unlike INSERTS new records into tables, rows are less likely to be inserted into the same page. Indexes like
B-Tree indexes are known to cause more random writes.
Due to random writes and reads, indexes need more pages to be in the cache. Cache requirements for in-
dexes are generally much higher than associated tables.
5. WAL generation
In addition to WAL records of the table updates, there will also be WAL records for indexes. This helps in crash
recovery and replication. If you are using any wait event analysis tools/scripts like pg_gather, the overhead
of the WAL generation will be clearly visible. The actual impact depends on the index type.
This is a synthetic test case, but if WAL-related wait events appear as any of the top wait events, it is a mat-
ter of concern for a transaction system, and we should take every step to address it.
Not only will WAL records be generated, but we will have more pages dirtied as well. As the index pages get
dirtied, they must be written back to files, leading to more I/O again—the “DataFileWrite” wait event, as seen
in the previous screenshot.
Another side effect is indexes increase the total Active-Dataset size. By “Active dataset,” I mean the tables
56
and indexes which are frequently queried and used. As the size of the active dataset increases, the cache
becomes less and less efficient. Less-effective cache results in more datafile read, so read I/O is increased.
This is in addition to the read I/O required to bring the additional index pages from storage for specific que-
ries.
Again the pg_gather report of another system with mainly select queries shows this problem. As the Ac-
tive-Dataset increases, PostgreSQL has no choice but to bring the pages from storage.
A more significant percentage of “DataFileRead” sustaining for a longer duration indicates that the Ac-
tive-Dataset is much bigger, which is not cachable.
7. Impact on VACUUM/AUTOVACUUM
The overhead is not only for inserting or updating index pages, as discussed in the previous points. There is
overhead in maintaining it since the indexes also need cleanups of old tuple references.
I have seen cases where autovacuum workers on a single table run for a very long duration because of
the size of the table and, most importantly, the excessive number of indexes on the table. In fact, it is wide-
spread that users see their autovacuum worker is “stuck” for hours without showing any progress for a lon-
ger duration. This happens because the index cleanup by the autovacuum is the opaque stage of autovac-
uum and is not visible through views like pg_stat_progress_vacuum other than the vacuum phase, which
is indicated as vacuuming indexes.
Indexes can get bloated and become less efficient over time. Periodic index maintenance (REINDEX) might
be needed in many systems.
Tunnel vision is the loss of the field view. The user may be concentrating on a particular SQL statement in an
attempt to “tune” and decide on creating indexes. By creating an index for tuning a query, we are shifting
more system resources to that query. Then, it may give more performance to that particular statement by
penalizing others.
But as we keep creating more and more indexes for tuning other queries, the resources will shift again to-
wards other queries. This leads to a situation where the effort to tune every query penalizes every other que-
ry. Ultimately, everyone will be hurt, and only losers will be in this war. Someone trying to tune should consid-
er how every part of the system can co-exist (maximizing business value) rather than absolute maximum
performance for a particular query.
57
9. Greater storage requirement
I see cases where indexes take more storage than tablets almost every day.
This may sound too silly for those with more money to spend on storage, but we should remember that this
has a cascading effect. The total database size grows to a multiple of the actual data. So obviously, back-
ups take more time, storage, and network resources, and then the same backup can put more load on the
host machine. This would also increase the time to restore a backup and recover it. Bigger databases affect
many things, including more time to build standby instances.
I am not just talking about rarely occurring index-related bugs like silent index corruption of PostgreSQL 14
or index corruption due to glibc collation change, which keeps popping up now and then and affects many
environments even today. Over decades of working with databases, I have observed that index corruptions
are reported more frequently. (I hope anyone involved in PostgreSQL for years and who has seen hundreds
of cases will agree with me). As we increase the number of indexes, we increase the probability.
A set of critical questions should accompany new index considerations: Is it essential to have this index, or
is it necessary to speed up the query at the cost of more index? Is there a way to rewrite the query to get a
better performance? Is it ok to discard the small gains and live without an index?
Existing indexes also require a critical review over a period of time. All unused indexes (those indexes with
idx_scan as zero in pg_stat_user_indexes) should be considered for dropping. Scripts like the one from
pgexperts can help to do more analysis.
PostgreSQL 16 has one more column in pg_stat_user_indexes / pg_stat_all_indexes with the name last_
idx_scan, which can tell us when was the last time the index was used (timestamp). This will help us to take
a well-informed look at all the indexes in the system.
Summary
The summary in simple words: Indexes are not cheap. There is a cost, and the cost can be manifold. In-
dexes are not always good, and sequential scans are not always bad, either. My humble advice is to avoid
looking for improving individual queries as the first step because it is a slippery slope. A top-down approach
to tuning the system yields better results, starting from tuning the Host machine, Operating System, Post-
greSQL parameter, Schema, etc. An objective “cost-benefit analysis” is important before creating an index.
58
Useful Queries For PostgreSQL Index
Maintenance
By Ibrar Ahmed
59
Step one: Sometimes, you need to see how many indexes your table has. This query will show the sche-
ma-qualified table name and its index names.
Step two: As we all know, an index is a performance feature, but along with that, it is also used to ensure
uniqueness. But to ensure the uniqueness, we need a separate type of index called a unique index. To check
whether an index is unique, pg_index has a column named “indisunique” to identify the uniqueness of the
index.
Step three: There is a pretty simple way to get the size of the index of PostgreSQL. Here is a query to list the
PostgreSQL with size.
SELECT pg_size_pretty(pg_relation_size(‘pgbench_accounts_index’));
pg_size_pretty
----------------
132 MB
(1 row)
60
Step four: Here is a list of the indexes with total table size and size of the index, which is very useful for com-
paring your table size with its corresponding indexes. It’s very good to know the size of your table, index, and
the total size of the table.
pg_relation_size: Function gives the size of relation. It is used to get the size of the table/index.
pg_total_relation_size: This is a special function that gives the total size of the table along with its all index-
es.
Step five: Get the query of the index. This query will show the index creation query.
Step six: If your index becomes corrupted or bloated, you need to build that index again. At the same time,
you don’t want to block the operation on your table, so this REINDEX CONCURRENTLY command is your
choice.
61
Step seven: PostgreSQL has many index methods like BTree, Hash, BRIN, GIST, and GIN. Sometimes, we want
to create some specific index on a column but cannot do that. PostgreSQL has limitations; some indexes
cannot be created on some data types and operators, which makes sense, too. For example, the Hash index
can only be used for equal operators. Here is a query to get the list of the supported data types for a partic-
ular index.
SELECT amname,
opfname
FROM pg_opfamily,
pg_am
WHERE opfmethod = pg_am.oid
AND amname = ‘btree’;
amname | opfname
--------+--------------------
btree | array_ops
btree | bit_ops
btree | bool_ops
…
Step eight: This query will find the unused indexes. If index_scans is 0 or close to 0, then you can drop those
indexes. But be careful, as maybe those indexes are for unique purposes.
62
Step nine: A query is used to find a duplicate index. In this example, pgbench_accounts has two of the
same indexes. There is no need to have multiple same indexes with a different name on a table. As we
already discussed, in case of update/insert, all the indexes get updated along with the actual table, which
hurts the performance.
(1 row)
Conclusion
PostgreSQL has catalog tables to store the index information; therefore, we can write as many queries as
needed. This shows some basic queries and shows how to use the catalog tables to write the queries.
63
Vaccuming
Executing the vacuum command in PostgreSQL is an important
maintenance operation designed to enhance database
performance and free up disk space. It’s vital to avoid the buildup of
redundant data, often referred to as “dead tuples” or “dead rows,”
which can degrade query efficiency, but its benefits are maximized
only when it is correctly configured and diligently monitored.
For this chapter, we have compiled a few insights from experts into
vacuum strategies, including the refinement of manual vacuuming
and the automation offered by autovacuum, to provide you with
the knowledge to fine-tune vacuum settings to suit your specific
database needs.
64
PostgreSQL Vacuuming Command to
Optimize Database Performance
By Ibrar Ahmed
65
Multi-version concurrency control (MVCC)
To maintain consistency and prevent data loss due to concurrent updates, PostgreSQL employs multi-
version concurrency control (MVCC). PostgreSQL and other database management systems use MVCC
to ensure consistent reads and prevent data loss from concurrent updates. PostgreSQL is achieved by
storing multiple versions of each row within the database, allowing transactions to access a consistent data
snapshot.
In PostgreSQL databases, each row within a single table is assigned a transaction ID referred to as an “xmin.”
This ID signifies the transaction that inserted the row. When a row is updated or deleted, it is not immediately
removed from the table. Instead, a new version of the row is inserted with a new transaction ID, while the old
version is marked as “dead” with a transaction ID called an “xmax.”
When a transaction reads a row, it utilizes the xmin and xmax values to determine whether the row is visible
to the transaction. If the xmin value is greater than the transaction’s “snapshot” (a record of the transaction
IDs that were in progress when the transaction began), the row is not visible to the transaction. If the xmax
value equals the transaction’s ID, the row has been deleted by the transaction and is also not visible. In all
other cases, the row is visible to the transaction.
This allows transactions to access a consistent data snapshot, as they can only see rows committed when
the transaction began. It also prevents data loss due to concurrent updates, as conflicting updates result in
inserting new row versions rather than overwriting the existing data.
Although MVCC incurs some overhead in terms of storage and performance due to the need to maintain
multiple versions of each row, it is a crucial feature of PostgreSQL and other database systems that support
concurrent updates.
This allows multiple versions of each row to be stored, enabling transactions to access a consistent data
snapshot. However, this can result in the accumulation of dead tuples as rows are updated or deleted.
PostgreSQL vacuuming
The vacuum process in PostgreSQL helps maintain the database’s performance and disk space efficiency
by removing rows that are no longer needed.
These rows accumulate because PostgreSQL uses MVCC to allow multiple transactions to access the same
data simultaneously without conflicts. During the vacuum process, tables and indexes are scanned, and
these dead tuples are removed, helping to reclaim space and improve query performance. It is essential to
run a vacuum periodically to keep the database running smoothly. MVCC stores multiple versions of each
row, so dead tuples are not immediately removed when a row is updated or deleted.
Vacuum Full
The VACUUM FULL command in PostgreSQL is a tool used to reclaim disk space occupied by deleted or
obsolete data. Unlike the regular VACUUM command, it performs a more thorough cleanup by physically
rearranging the data on the disk. This ensures that all dead tuples are completely removed, resulting in a
more compact storage structure and improved query performance.
VACUUM FULL requires exclusive access to the table, so while the command runs, other transactions cannot
read from or write to the table, potentially impacting database availability. It’s important to know this is a
resource-intensive operation that may cause database disruptions and longer execution times.
Verbose
The term “Verbose” is used when referring to Postgres log output. Normally, PostgreSQL logs only the barest
of information when it’s first set up, i.e., STARTUP, SHUTDOWN, ERROR, and FATAL messages. Log output
66
verbosity increases substantially when the logging parameters are set to record additional information
relating to client access details and Postgres background processes such as CHECKPOINTs and VACUUM.
The most “verbose” setting is when each and every SQL statement is logged, which can increase the
Postgres log size, generating GB of messages in a matter of a few minutes.
The vacuum process removes dead tuples and updates statistics used by the query planner to more
accurately estimate the number of rows returned by a query and choose the most efficient execution plan.
There are two types of vacuum in PostgreSQL: VACUUM and ANALYZE. VACUUM removes dead tuples and
updates statistics, while ANALYZE only updates statistics. It is generally recommended to run both VACUUM
and ANALYZE together.
The vacuum command can be initiated manually using SQL commands or automated using the
autovacuum background process, which runs based on configurable thresholds such as the number of
dead tuples or live rows in a table. In PostgreSQL 15, the vacuum process has been optimized to make it
more efficient and faster to vacuum large tables. It includes improvements such as the ability to vacuum
multiple partitions of the same table in parallel, vacuum indexes concurrently, and skip vacuuming indexes
unaffected by an update.
From a technical perspective, the vacuum process in PostgreSQL 15 involves several components. The
vacuum daemon (autovacuum) initiates vacuum operations based on configurable thresholds. The
vacuum worker executes the actual vacuum operation, scanning the table or index and removing dead
tuples while updating statistics.
Autovacuum
Autovacuum, which automates routine vacuum maintenance, is enabled by default in PostgreSQL and can
be configured using several parameters in [Link] file. PostgreSQL has several settings related to
vacuum that can be configured to control how the vacuum process runs. You can find the following settings
in [Link] file and include:
• autovacuum: This setting enables or disables the autovacuum background process. By default,
autovacuum is enabled.
• autovacuum_analyze_threshold: This setting determines the minimum number of live rows that
must be present in a table before it is analyzed. The default value is 50.
67
Here is an example of configuring some of the vacuum parameters in the [Link] file:
autovacuum = on
autovacuum_vacuum_threshold = 100
autovacuum_analyze_threshold = 100
autovacuum_vacuum_scale_factor = 0.5
autovacuum_analyze_scale_factor = 0.2
autovacuum_vacuum_cost_delay = 50
autovacuum_vacuum_cost_limit = 500
In this example, autovacuum is enabled, and the thresholds for vacuum and analyze are set to 100. The
scale factors for vacuum and analyze are set to 0.5 and 0.2, respectively, which means that a vacuum will
be triggered when there are 50 dead rows per 1,000 live rows in the table (0.5 x 100), and an analyze will be
triggered when there are 20 live rows per 1,000 rows in the table (0.2 x 100). The vacuum cost delay is set to
50 milliseconds, and the vacuum cost limit is set to 500 rows. This means that the autovacuum will wait 50
milliseconds before starting a vacuum operation and will vacuum a maximum of 500 rows at a time.
It is important to configure these settings to ensure that vacuum and analyze properly are running
effectively and not causing too much load on the database. It is also a good idea to monitor the activity of
the autovacuum and manually vacuum tables that are not adequately maintained by the autovacuum.
Autovacuum can also be configured on a per-table basis using the autovacuum_vacuum_cost_delay and
autovacuum_vacuum_cost_limit parameters in the table’s storage parameters. These parameters control
how aggressively autovacuum vacuums the table, with a lower cost delay causing the vacuum to run more
frequently and a higher cost limit allowing more rows to be vacuumed at once.
Parallel vacuuming
Parallel vacuum is a feature in PostgreSQL that allows the vacuum process to be run concurrently on
multiple cores or processors for the same table, improving the performance of the vacuum operation. This
can be especially useful for vacuuming large tables, as it allows the vacuum process to use multiple CPUs
to scan and process the table in parallel.
The parallel vacuum was introduced in PostgreSQL 13 as an experimental feature and made generally
available in PostgreSQL 14. To use a parallel vacuum, you need to set the “max_parallel_workers_per_
gather” parameter in the [Link] configuration file to a value greater than one. To enable parallel
processing, specify the “PARALLEL” option when running a vacuum or analyze command.
For example, to run a parallel vacuum on a table named “foo_table,” you can use the following command:
You can also specify the “PARALLEL” option when running a vacuum or analyze command on an entire
schema or database:
Note: Keep in mind that parallel vacuum can increase the load on the database server, as it requires
multiple CPU cores to be used simultaneously. You should carefully monitor the performance of your
PostgreSQL database when using a parallel vacuum and adjust the “max_parallel_workers_per_gather”
parameter as needed to find the optimal setting for your workload.
68
Transaction wraparound
Transaction wraparound is a phenomenon that can occur in PostgreSQL when the maximum transaction
ID (TXID) has been reached, and the system wraps around to reuse old transaction IDs. This can cause
problems if there are still rows in the database with a higher transaction ID than the current maximum, as
they will be considered “dead” and removed by the vacuum process.
To prevent transaction wraparound from causing problems, it is important to run the vacuum process
and remove dead rows regularly. You can use the following query to check for tables at risk of transaction
wraparound.
69
Vacuum statistics
To view the vacuum history for all tables in the current schema:
This query retrieves vacuum statistics for all tables in the “public” schema in the current database.
This query can help monitor the status of the vacuum process and identify tables that may need to be
vacuumed or analyzed. For example, if a table has many dead rows or has not been vacuumed or analyzed
recently. In that case, it may be worth running a manual vacuum or analyzing operations to improve the
performance of the database.
Download our free e-book, “The 6 Common Causes of Poor Database Performance,” to see the most
common database performance issues and get expert insights on how to fix them.
SELECT
[Link] as schema_name,
[Link] as table_name,
[Link] as row_count,
[Link] as page_count,
s.n_dead_tup as dead_row_count,
s.last_vacuum,
s.last_autovacuum,
s.last_analyze,
s.last_autoanalyze
FROM pg_class c
JOIN pg_namespace n ON [Link] = [Link]
LEFT JOIN pg_stat_user_tables s ON [Link] = [Link]
WHERE [Link] = ‘r’ AND [Link] = ‘public’;
-[ RECORD 1 ]----+------------------------------
schema_name | public
table_name | pgbench_accounts
row_count | 9.999965e+06
page_count | 163935
dead_row_count | 41705
last_vacuum | 2022-12-25 16:00:36.231734+00
last_autovacuum |
last_analyze | 2022-12-25 16:00:18.90299+00
last_autoanalyze |
-[ RECORD 2 ]----+------------------------------
schema_name | public
table_name | pgbench_branches
row_count | 100
page_count | 1
dead_row_count | 41
last_vacuum | 2022-12-25 16:00:44.722317+00
last_autovacuum |
last_analyze | 2022-12-25 16:00:16.254529+00
last_autoanalyze | 2022-12-25 16:01:45.957663+00
70
To view the list of tables that have been modified since the last vacuum:
SELECT
[Link] as schema_name,
[Link] as table_or_index_name,
[Link] as table_or_index,
[Link] as row_count,
s.last_vacuum,
s.last_autovacuum,
s.last_analyze,
s.last_autoanalyze
FROM pg_class c
JOIN pg_namespace n ON [Link] = [Link]
LEFT JOIN pg_stat_user_tables s ON [Link] = [Link]
WHERE ([Link] = ‘r’ or [Link] = ‘i’)
AND (s.last_vacuum < s.last_autovacuum OR s.last_vacuum < s.last_analyze);
To view the list of tables and indexes that have a high number of dead rows:
SELECT
[Link] as schema_name,
[Link] as table_name,
[Link] as row_count,
s.n_dead_tup as dead_row_count
FROM pg_class c
JOIN pg_namespace n ON [Link] = [Link]
LEFT JOIN pg_stat_user_tables s ON [Link] = [Link]
WHERE [Link] = ‘r’ AND s.n_dead_tup > 0;
SELECT
[Link] as schema_name,
[Link] as index_name,
s.n_dead_tup as dead_row_count
FROM pg_class c
JOIN pg_namespace n ON [Link] = [Link]
LEFT JOIN pg_stat_user_tables s ON [Link] = [Link]
71
You can write the above query for all the databases using dblink. This will give information about all the
databases.
SELECT *
FROM dblink(‘host=<host> port=<port> dbname=<database> user=<username>
password=<password>’,
‘SELECT
[Link] as schema_name,
[Link] as table_or_index_name,
[Link] as table_or_index,
[Link] as row_count,
s.last_vacuum,
s.last_autovacuum,
s.last_analyze,
s.last_autoanalyze
FROM pg_class c
JOIN pg_namespace n ON [Link] = [Link]
LEFT JOIN pg_stat_user_tables s ON [Link] = [Link]
WHERE ([Link] = ‘’r’’ or [Link] = ‘’i’’)
AND (s.last_vacuum < s.last_autovacuum OR s.last_vacuum < s.last_analyze)’) AS t(
schema_name text,
table_or_index_name text,
table_or_index char(1),
row_count bigint,
last_vacuum timestamp,
last_autovacuum timestamp,
last_analyze timestamp,
last_autoanalyze timestamp
);
Note: You need to replace the placeholders <host>, <port>, <database>, <username>, and <password> with
the actual values for your server.
Conclusion
It is important to properly configure the autovacuum to avoid overloading the database with too many
vacuums. It is also a good idea to monitor the activity of the autovacuum and manually vacuum tables that
are not adequately maintained by the autovacuum.
In summary, PostgreSQL vacuuming is an essential maintenance task in PostgreSQL that helps to reclaim
space and improve performance. It is generally recommended to run the vacuum regularly, either manually
using the VACUUM SQL command or automatically using the autovacuum background process. This helps
to ensure that the database remains efficient and free of dead rows and reduces the risk of transaction
wraparound. Autovacuum is a convenient way to automate this process, but it is important to configure and
monitor it to ensure it runs effectively and adequately.
72
Why Custom Scheduled Jobs for
Vacuuming and Fine-grained
Autovacuum Tuning Are Unavoidable
By Jobin Augustine
But the autovacuum is far from a perfect fit for many environments.
While working with many customer environments, we keep seeing
cases where the built-in logic is insufficient. As I keep doing fixes for
many Percona customers, I thought of noting down important points
for everyone.
73
Most common problems/limitations
• autovacuum_vacuum_scale_factor
• autovacuum_vacuum_threshold
• autovacuum_analyze_scale_factor
• autovacuum_analyze_threshold
• autovacuum_vacuum_insert_scale_factor
• autovacuum_vacuum_insert_threshold
• Basically, all these parameters are based on the DML operations performed in the table. The first four
parameters are based on UPDATEs and DELETEs on the table because they cause the “dead” tuples. The
5th and 6th parameters are obviously based on INSERTs.
Considering only the DML activities for triggering autovacuum can only solve part of the problem
because autovacuum has a lot more things to do than just handling dead tuples. The most important
and crucial thing is the FREEZE operation / Table age. So, it is quite normal to see tables get aged until
they hit the autovacuum_freeze_max_age limit, triggering the aggressive autovacuum to prevent the
wraparound. Subsequently, they are heavy on the system and cause other side effects at unexpected
times, which is not what we want to see in production systems.
• Candidate tables are ordered alphabetically. This is not something very widely discussed. This has
another serious side effect: few tables always get priority, and few others never get a chance to get
vacuumed. They starve until they reach the limit autovacuum_freeze_max_age. Again, the aggressive
autovacuum kicks in with aggression and side effects.
• Tables become candidates for autovacuum during peak hours. The autovacuum settings are based
on scale factors/thresholds and Indirectly based on DML Operations. The chance that the table crosses
these limits is high when there is a high number of transactions on the table – which is the peak hours.
Effectively, it gets kicked in at the very wrong time.
• No consideration for long starving tables. It is very common to see a few tables become candidates
for vacuuming too frequently and occupy all workers repeatedly. Meanwhile, other tables down the list
of candidature remain unvacumed for a long time. There is no way to give some weightage for tables
which are not considered for long.
• No way to control the throttle of autovacuum workers dynamically. This is probably the worst, even if
an informed DBA wants to adjust the autovacuum_vacuum_cost_limit based on need or time window
and signal the PostgreSQL.
For example:
• This has no effect on the currently running autovacuum workers. Only the next worker starting will
consider this setting. So this can not be used for addressing the problem.
• The attempt by DBAs to tune parameters often backfires. After seeing the aged tables and starving
tables, desperate DBAs keep aggressive settings and a higher number of workers. Many times, this
pushes the system way beyond its limit because everything gets in the wrong time with high aggression
when the system has already a high number of active sessions. Multiplied by the maintenance_work_
mem allocations by workers. System performance suffers to a great extent. The worst I have seen is
74
autovacuum workers occupying up to 50% of server resources.
• Autovacuum during the active time window defeats its own purpose. The autovacuum worker will
be referring to an old xid/snapshot if it takes time to complete during the high activity window. So
effectively, it won’t be cleaning the dead tuples generated during the same duration, which is against
the very purpose of autovacuum
• Many Starved tables reach the wraparound prevention condition at the same time.
It is very common to see that the tables that are starved for a longer duration of autovacuum reach
autovacuum_freeze_max_age and wraparound prevention aggressive vacuum get triggered
concurrently, making the situation much worse.
Due to such ineffectiveness, we keep seeing DBAs tending to disable the autovacuum altogether and
invite a bigger set of problems and even outages. At the very least, my request to anyone who is new to
PostgreSQL is to please never try to turn off the autovacuum. That’s not the way to address autovacuum-
related issues.
Tuning Autovacuum
Tuning autovacuum is obviously the first line of action. Autovacuum settings can be adjusted at the global
level or at the table level.
autovacuum_max_workers controls how many workers will be working at a time on different tables.
By default, autovacuum_vacuum_cost_limit will be disabled (-1), which means the value of the other
parameter vacuum_cost_limit will be in effect. So, the very first thing suggested is to set a value for
autovacuum_vacuum_cost_limit, which will help us control the autovacuum workers alone.
One common mistake I see across many installations is that autovacuum_max_workers is set to a
very high value, like 15, assuming that this makes the autovacuum run faster. Please remember that
autovacuum_vacuum_cost_limit is divided among all workers. So the higher the number of workers, each
worker runs slower. And slower workers mean ineffective cleanup as mentioned above. Moreover, each of
them can occupy up to maintenance_work_mem. In general, the default value of autovacuum_max_
workers, which is 3, will be sufficient. Please consider increasing it only if it is an absolute necessity.
Table-level settings
The blanket setting at the Instance level might not work great for at least a few tables. These outliers need
special treatment, and tuning the settings at table level might become unavoidable. I would start with those
tables which become candidates too frequently for autovacuum.
PostgreSQL with a log_autovacuum_min_duration setting gives great details of those tables that are
frequently becoming candidates, and those autovacuum runs that took considerable time and effort.
Personally, I prefer this as the starting point. A summary of autovacuum runs can be obtained by comparing
the autovacuum_count of pg_stat_all_tables taken in two different timestamps also. We need to consider
the HOT (Heap Only Tuple) updates and fillfactor. Hot update information can be analyzed using the n_
tup_hot_upd of the same view (pg_stat_all_tables). Tuning this can bring down the vacuum requirements
drastically. Equipped with all this information analysis, specific table-level settings can be adjusted. For
example:
75
alter table t1 set (autovacuum_vacuum_scale_factor=0.0, autovacuum_vacuum_
threshold=130000, autovacuum_analyze_scale_factor=0.0, autovacuum_analyze_
threshold=630000, autovacuum_enabled=true, fillfactor=82);
Tools/scripts like pg_gather can be handy to assess the autovacuum frequency and suggest settings like
fillfactor and table level autovacuum settings.
Our aim is not to disable the autovacuum but to supplement the autovacuum with our knowledge about
the system. It need not be complex at all. The simplest we can have is to run a ‘VACUUM FREEZE’ on tables
which are having maximum age for itself or its TOAST.
For example, we can have [Link] file with the following content:
The query gets 100 aged tables that are not currently undergoing autovacuum and run a “VACUUM FREEZE”
on them. (The gexec at the end executes the query output.)
This can be scheduled using corn for a low activity window like:
If there are multiple low-impact windows, all of them can be made use of using multiple schedules.
A copy of the above SQL script is available on GitHub for easy download. Practically, we have seen that the
supplementary, scheduled vacuum jobs, based on the table age approach, have the following positive
effects.
• The chance of those tables becoming candidates again during the peak times is drastically
reduced.
• Able to achieve very effective utilization of server resources during the off-peak times for the
vacuum and freeze operation.
• Since the candidature was selected based on totally different criteria (age of table) than the default
(scale factor and threshold), the chance of a few tables starving forever is eliminated. Moreover, that
removes the possibility of the same table becoming a candidate for vacuum again and again.
• In customer/user environments, the wraparound prevention autovacuum is almost never reported
again.
76
Summary
It is not rare to see systems where autovacuum remains untuned or poor settings are used from the
instance level to the table level. Just want to summarize that:
• Default settings may not work great in most of the systems. Repeated autovacuum runs on a few
tables while other tables starve for autovacuum is very common.
• Poor settings can result in autovacuum workers taking a considerable part of the server resources
with little gain.
• Autovacuum has the natural tendency to start at the wrong time when a system undergoes heavy
transactions.
• Practically, a scheduled vacuum job becomes necessary for those systems that undergo heavy
transactions and, have a large number of transaction tables, and are expected to have spikes and
peak time periods of load.
Clear analysis and tuning are important. It is always highly recommended to have a custom vacuum job
that takes up your knowledge about the specific system and time windows of the least impact.
77
Overcoming VACUUM WRAPAROUND
By Robert Bernier
78
Transaction ID Wraparound can be caused by a combination of one or more of the following circumstances:
Transaction WRAPAROUND can cause a spontaneous shutdown of the Postgres database server in order to
protect the integrity of the data.
PostgreSQL, at any one time, has a number of transactions that are tracked by a unique ID. Every so often,
that number reaches the upper limit that can be registered, for example, 200 million transactions, which
is the default and is then renumbered. But if the number of unique transaction IDs goes to its maximum
transactions limit, known as TXID Wraparound, Postgres will force a shutdown in order to protect the data.
• Four billion transactions, 2^32, is the integer upper limit for the datatype used in Postgres.
• Two billion transactions, 2^31, is the upper limit that PostgreSQL permits before forcing a shutdown.
• Ten million transactions before the upper limit is reached, WARNING messages consisting of a
countdown will be logged.
• One million transactions before the upper limit is reached, PostgreSQL goes to READ-ONLY mode.
Warning Signs
In the case of the autovacuum daemon falling behind across the entire data cluster, review your monitoring
solution in order to identify the trend of these metrics:
• IO wait increases
• CPU load increases
• SQL performance decreases
• Reviewing the internal Postgres monitoring metrics and confirming tables are being vacuumed.
• Reviewing the Postgres logs, look for an overabundance of canceled autovacuum worker processes.
• Reviewing the view “pg_stat_activity” and looking for a query string – PREVENTING TRANSACTION ID
WRAPAROUND -. Actually, this is a normal message. But one should not see autovacuum running
solely for the purposes of mitigating WRAPAROUND.
Here are example error messages that you can find in the Postgres logs when threatened by a shutdown
due to WRAPAROUND:
#
# When less than 10 million transactions remain before shutdown
#
WARNING: database “mydb” must be vacuumed within 177009986 transactions
HINT: To avoid a database shutdown, execute a database-wide VACUUM in “mydb”.
79
Here’s a set of queries that will help you determine if WRAPAROUND is a risk:
--
-- Database query for transaction age per database
-- and as a percentage of maximum permitted transactions
--
SELECT datname,
age(datfrozenxid),
(age(datfrozenxid)::numeric/1000000000*100)::numeric(4,2) as “% WRAPAROUND RISK”
FROM pg_database ORDER BY 2 DESC;
--
-- Database query for transaction age per table
--
SELECT
[Link]::regclass as table_name,
greatest(age([Link]),age([Link])) as “TXID age”,
(greatest(age([Link]),age([Link]))::numeric/1000000000*100)::numeric(4,2) as “%
WRAPAROUND RISK”
FROM pg_class c
LEFT JOIN pg_class t ON [Link] = [Link]
WHERE [Link] IN (‘r’, ‘m’)
ORDER BY 2 DESC;
--
-- Other vacuum runtime parameters of interest
-- returning TXID age
--
SELECT name, setting
FROM pg_settings
WHERE name ~ ‘vacuum’
AND name ~’_age$’
ORDER BY 1 ASC;
First and foremost, make certain all tables are regularly vacuumed. A correctly configured autovacuum
process takes care of this without it ever becoming an issue. Otherwise, you will need to consider a manual
VACUUM strategy.
The following are merely suggestions since each situation is highly subjective.
If you have the time, run the following invocation of vacuumdb. The value for option ‘-j’ can vary from a
couple to a value equal to the number of CPUs on the host. The option ‘-a’ will process each database in
alphabetical order.
vacuumdb -F -a -z -j 10 -v
Consider a bash script targeting individual databases if you see one is more urgent than another:
vacuumdb -z -j 10 -v <mydatabase>
80
Immediate Action: Approaching Wraparound at < 10 Million Transactions
The following is the set of actions to take when TRANSACTION WRAPAROUND is imminent. Remember, you are
in a race against time.
You must vacuum the entire data cluster before the remaining available transaction id drops to one million
transactions.
Action
• The task is to vacuum the databases as quickly as possible.
• The tool of choice is the CLI “vacuumdb”.
• Use as many threads as reasonable.
• Run VACUUM in verbose mode and log the output.
• Monitor log output for anomalous messages, i.e., vacuum fails, etc.
• Run “vacuumdb” against individual databases and, if necessary, individual tables.
• Avoid using the option ‘-a’.
Scripts
Here’s a pair of example scripts that you can use as a starting point when developing your own mitigation
protocol.
Method
The secret sauce is xargs, which enables one to utilize as many CPUs as reasonably possible. The following
pair of bash scripts invoke vacuumdb against a series of tables. Of course, there’s more than one way to do
this.
Script one generates a list of tables in a selected database and calls script two, which executes the VACUUM
on each of those tables individually.
81
SCRIPT ONE (go1_highspeed_vacuum.sh)
#!/bin/bash
#
# INVOCATION
# EX: ./go1_highspeed_vacuum.sh
#
########################################################
# EDIT AS REQUIRED
export CPU=4
export PAGER=less PGUSER=postgres PGPASSWORD=mypassword PGDATABASE=db01 PGOPTIONS=’-c statement_
timeout=0’
########################################################
SQL1=”
with a as (select [Link]::regclass as table_name,
greatest(age([Link]),age([Link]))
from pg_class c
left join pg_class t on [Link] = [Link]
where [Link] in (‘r’, ‘m’)
order by 2 desc)
select table_name from a
“
#!/bin/bash
########################################################
# EDIT AS REQUIRED
export PAGER=less PGUSER=postgres PGPASSWORD=mypassword PGOPTIONS=’-c statement_timeout=0’
export DB=$1
########################################################
Tips
• Be prepared to execute vacuumdb against the databases in REVERSE alphabetical order to avoid
clashing with the autovacuum worker processes, which vacuums in FORWARD alphabetical order.
• Query table “pg_stat_activity”.
• Always monitor where the autovacuum processes are working.
• Avoid working on the same table that the autovacuum workers are currently processing.
• Use the autovacuum workers as an indicator of what databases remain to be processed.
• Kill active autovacuum workers when in conflict with a manual vacuum in order to speed things up.
Immediate action: When PostgreSQL service has shut down due to transaction wraparound
One recovers from a forced shutdown due to transaction ID wraparound by performing a cluster-wide
82
vacuum in single-user mode:
Log in to the host, and as a UNIX user, “Postgres” executes an invocation that is something similar:
I would suggest scripting the vacuum process because you’ll need to log in to each database to perform
the VACUUM.
#
# it is understood the database list has
TXID Wraparound is one of the scariest scenarios that can occur. Thankfully, this is an extremely rare
incident and only occurs when systems are either under extremely heavy load or have been neglected.
83
Partitioning
PostgreSQL partitioning is a technique used to enhance the
performance and manageability of large tables by splitting them
into smaller, more manageable pieces called partitions. This
approach allows for more efficient data access and manipulation,
especially in scenarios involving large volumes of data. Through
partitioning, databases can achieve optimized query performance,
thanks to partition pruning, and facilitate easier data management
activities like archival and purging by allowing these tasks to be
executed on individual partitions.
84
PostgreSQL Partitioning Using
Traditional Methods
By Neha Korukula
Neha has been a Jr. PostgreSQL DBA in Managed Services at Percona since
2022 with extensive experience on installation, user configuration, setting
up high availability, performance tuning, and migrations with minimal /zero
downtime.
85
Methods of built-in PostgreSQL partition techniques
• Range partitioning
• List partitioning
• Hash partitioning
• Bulk operations like data loads and deletes can be performed using the partition feature of ATTACH
and DETACH effectively.
• The exact point at which a table will benefit from partitioning depends on the application. However, a
rule of thumb is that the size of the table should exceed the physical memory of the database server.
• As data grows, sub-partitions can be created, which enhances the performance, and old partitions
can be deleted either by making them standalone or dropping them entirely.
Benefits of partitioning
• Query performance for DDL and DML operations can be improved in certain situations, particularly
when most of the heavily accessed rows of the table are in a single partition or a small number of
partitions, which is explained below.
• When queries or updates access a large percentage of a single partition, performance can be
improved by using a sequential scan of that partition instead of using an index, which would require
random-access reads scattered across the whole table.
• Dropping the partition table or truncating the partition table can be done using DROP TABLE and
TRUNCATE TABLE, respectively, reducing the load through DELETE operations.
Range partitioning
Database partition that is based on a specific range of columns with data like dates and Numeric values.
Here, as an example, I created a table with range partitioning and partition tables for each quarter on a
Date column.
CREATE TABLE employees (id INT NOt NULL , fname VARCHAR(20) , lname VARCHAR (20) , dob DATE NOT NULL ,
joined DATE NOT NULL) PARTITION BY RANGE (joined);
CREATE TABLE employees_q1 PARTITION of employees for VALUES FROM (‘2022-01-01’) to (‘2022-04-01’);
CREATE TABLE employees_q2 PARTITION of employees for VALUES FROM (‘2022-04-01’) to (‘2022-07-01’);
CREATE TABLE employees_q3 PARTITION of employees for VALUES FROM (‘2022-07-01’) to (‘2022-10-01’);
CREATE TABLE employees_q4 PARTITION of employees for VALUES FROM (‘2022-10-01’) to (‘2023-01-01’);
d+ employees
Partitioned table “[Link]”
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
-------+-----------------------+-----------+----------+---------+----------+-------------+--------------+-----
id | integer | | not null | | plain | | |
fname | character varying(20) | | | | extended | | |
lname | character varying(20) | | | | extended | | |
dob | date | | not null | | plain | | |
joined | date | | not null | | plain | | |
86
Inserted some random data for entries with 365 days a year.
INSERT INTO employees (id ,fname,lname,dob ,joined) VALUES ( generate_series(1, 365) ,(array[‘Os-
wald’, ‘Henry’, ‘Bob’, ‘Vennie’])[floor(random() * 4 + 1)],
(array[‘Leo’, ‘Jack’, ‘Den’, ‘Daisy’ ,’Woody’])[floor(random() * 5 + 1)], ‘1995-01-01’::date +
trunc(random() * 366 * 3)::int,
generate_series(‘1/1/2022’::date, ‘12/31/2022’::date, ‘1 day’));
Here, I created a table without a partition and inserted the same data, similar to the partitioned table.
A query plan is seen better for DDL operations when performed on data with a single partition or fewer
partitions.
CREATE TABLE employees_nopartition (id INT NOt NULL , fname VARCHAR(20) , lname VARCHAR (20) , dob
DATE NOT NULL , joined DATE NOT NULL) ;
EXPLAIN select * from employees_nopartition where joined >= ‘2022-05-12’ and joined < ‘2022-
06-10’;
QUERY PLAN
------------------------------------------------------------------------------
Seq Scan on employees_nopartition (cost=0.00..8.47 rows=29 width=22)
Filter: ((joined >= ‘2022-05-12’::date) AND (joined < ‘2022-06-10’::date))
(2 rows)
Here we can see a better query plan when data is fetched from the partitioned table than data fetched
from the non-partitioned table.
EXPLAIN select * from employees where joined >= ‘2022-05-12’ and joined < ‘2022-06-10’;
QUERY PLAN
------------------------------------------------------------------------------
Seq Scan on employees_q2 (cost=0.00..2.37 rows=29 width=22)
Filter: ((joined >= ‘2022-05-12’::date) AND (joined < ‘2022-06-10’::date))
(2 rows)
87
List partitioning
Database partition that is based on key value(s) or discrete values and partition can also be done with the
expression of the column like (RANGE BY LIST(expression)), which is explained below.
For example, I created a table with a list partition and a few list-partitioned tables and inserted some
random data with 1,000 rows.
CREATE TABLE sales (id INT NOT NULL , branch VARCHAR(3),type text, Amount int ) PARTITION BY LIST
(branch);
INSERT into sales (id , branch ,type , amount ) VALUES ( generate_series(1, 1000) , (array[‘HYD’,
‘BLR’, ‘DEL’, ‘TPT’])[floor(random() * 4 + 1)] ,
(array[‘Laptops’, ‘Printers’, ‘Hardisks’, ‘Desktops’ ,’Monitors’])[floor(random() * 5 + 1)], (ran-
dom()*200000)::int );
d+ sales
Partitioned table “[Link]”
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
--------+----------------------+-----------+----------+---------+----------+--------------+--------
-----
id | integer | | not null | | plain | |
branch | character varying(3) | | | | extended | |
type | text | | | | extended | |
amount | integer | | | | plain | |
Partition key: LIST (branch)
Partitions: blr_sales FOR VALUES IN (‘BLR’),
del_sales FOR VALUES IN (‘DEL’),
hyd_sales FOR VALUES IN (‘HYD’),
tpt_sales FOR VALUES IN (‘TPT’)
88
List partitioning using expression
For example, I created a table with list partitioning using the expression of a column.
CREATE TABLE donors (id INT NOt NULL , name VARCHAR(20) , bloodgroup VARCHAR (15) , last_donated
DATE ,
contact_num VARCHAR(10)) PARTITION BY LIST (left(upper(bloodgroup),3));
d+ donors
Partitioned table “[Link]”
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
--------------+-----------------------+-----------+----------+---------+----------+-------------+-
-------------+-------------
id | integer | | not null | | plain | | |
name | character varying(20) | | | | extended | | |
bloodgroup | character varying(15) | | | | extended | | |
last_donated | date | | | | plain | | |
contact_num | character varying(10) | | | | extended | | |
Partition key: LIST (“left”(upper((bloodgroup)::text), 3))
Partitions: a_negative FOR VALUES IN (‘A- ‘),
a_positive FOR VALUES IN (‘A+ ‘),
ab_negative FOR VALUES IN (‘AB-’),
ab_positive FOR VALUES IN (‘AB+’),
b_negative FOR VALUES IN (‘B- ‘),
b_positive FOR VALUES IN (‘B+ ‘),
o_negative FOR VALUES IN (‘O- ‘),
o_positive FOR VALUES IN (‘O+ ‘)
INSERT INTO donors (id , name , bloodgroup , last_donated , contact_num) VALUES (generate_series(1,
100) ,’user_’ || trunc(random()*100) ,
(array[‘A+ group’, ‘A- group’, ‘O- group’, ‘O+ group’,’AB+ group’,’AB- group’,’B+ group’,’B-
group’])[floor(random() * 8 + 1)] , ‘2022-01-01’::date + trunc(random() * 366 * 1)::int,
CAST(1000000000 + floor(random() * 9000000000) AS bigint));
89
List partitioned data with expression distributed among its partitions is seen below:
SELECT a_negative , a_positive , ab_negative , ab_positive , b_negative ,b_positive ,o_negative , o_positive , total_
cnt from
( SELECT COUNT(*) FROM a_negative ) AS a_negative, ( SELECT COUNT(*) FROM a_positive ) AS a_positive,
( SELECT COUNT(*) FROM ab_negative ) AS ab_negative, ( SELECT COUNT(*) FROM ab_positive ) AS ab_positive ,
( SELECT COUNT(*) FROM b_negative ) AS b_negative, ( SELECT COUNT(*) FROM b_positive ) AS b_positive ,
( SELECT COUNT(*) FROM o_positive ) AS o_positive , ( SELECT COUNT(*) FROM o_negative ) AS o_negative,
( SELECT COUNT(*) FROM donors ) AS total_cnt;
a_negative | a_positive | ab_negative | ab_positive | b_negative | b_positive | o_negative | o_positive | total_cnt
------------+------------+-------------+-------------+------------+------------+------------+------------+-----------
(9) | (19) | (10) | (12) | (12) | (10) | (18) | (10) | (100)
(1 row)
Here is an example shown with the table, which is created without partitions and inserted the same data
similar to that of the partitioned table.
Below, I created a table without a partition and inserted some random data with 1,000 rows to show query
performance.
CREATE TABLE sales_nopartition (id INT NOT NULL , branch VARCHAR(3),type text, Amount int );
INSERT into sales_nopartition (id , branch ,type , amount ) VALUES ( generate_series(1, 1000) ,
(array[‘HYD’, ‘BLR’, ‘DEL’, ‘TPT’])[floor(random() * 4 + 1)] ,
(array[‘Laptops’, ‘Printers’, ‘Hardisks’, ‘Desktops’ ,’Monitors’])[floor(random() * 5 + 1)], (ran-
dom()*200000)::int );
EXPLAIN update sales_nopartition set type = ‘Smart Watches’ where branch = ‘HYD’;
QUERY PLAN
---------------------------------------------------------------------------
Update on sales_nopartition (cost=0.00..19.50 rows=229 width=50)
-> Seq Scan on sales_nopartition (cost=0.00..19.50 rows=229 width=50)
Filter: ((branch)::text = ‘HYD’::text)
(3 rows)
EXPLAIN update sales set type = ‘Smart Watches’ where branch = ‘HYD’;
QUERY PLAN
------------------------------------------------------------------
Update on sales (cost=0.00..5.10 rows=248 width=50)
Update on hyd_sales
-> Seq Scan on hyd_sales (cost=0.00..5.10 rows=248 width=50)
Filter: ((branch)::text = ‘HYD’::text)
(4 rows)
90
DELETE query performance
The above examples show the performance of DELETE and UPDATE operations with data fetched from a
single partitioned table having a better query plan than the one with no partitions.
Hash partitioning
Hash partitioning table is defined as the table partitioned by specifying a modulus and a remainder for
each partition.
• Each partition will hold the rows for which the hash value of the partition key divided by the specified
modulus will produce the specified remainder.
• Hash partitioning is best used when each partition is on different table spaces residing on separate
physical disks, so the IO is equally divided by more devices.
For example, I created a table with hash partitioning and a few partitioned tables with modulus five.
CREATE TABLE students ( id int NOT NULL, name varchar(30) NOT NULL , course varchar(100) ,joined
date ) PARTITION BY hash(id);
CREATE TABLE student_0 PARTITION OF students FOR VALUES WITH (MODULUS 5,REMAINDER 0);
CREATE TABLE student_1 PARTITION OF students FOR VALUES WITH (MODULUS 5,REMAINDER 1);
CREATE TABLE student_2 PARTITION OF students FOR VALUES WITH (MODULUS 5,REMAINDER 2);
CREATE TABLE student_3 PARTITION OF students FOR VALUES WITH (MODULUS 5,REMAINDER 3);
CREATE TABLE student_4 PARTITION OF students FOR VALUES WITH (MODULUS 5,REMAINDER 4);
91
The table structure looks like the one below with five created partitions:
d+ students
Partitioned table “[Link]”
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
--------+------------------------+-----------+----------+---------+----------+--------------+-------------
id | integer | | not null | | plain | |
name | character varying(30) | | not null | | extended | |
course | character varying(100) | | | | extended | |
joined | date | | | | plain | |
Partition key: HASH (id)
Partitions: student_0 FOR VALUES WITH (modulus 5, remainder 0),
student_1 FOR VALUES WITH (modulus 5, remainder 1),
student_2 FOR VALUES WITH (modulus 5, remainder 2),
student_3 FOR VALUES WITH (modulus 5, remainder 3),
student_4 FOR VALUES WITH (modulus 5, remainder 4)
INSERT into students (id , name , course ,joined ) VALUES (generate_series(1, 100000) , ‘student_’
|| trunc(random()*1000) ,
(array[‘Finance & Accounts’, ‘Business Statistics’, ‘Environmental Science’])[floor(random() * 3
+ 1)],’2019-01-01’::date + trunc(random() * 366 * 3)::int);
We see below the hash partitioned data among its partitioned tables.
• The primary benefit is to ensure an even distribution of data among a predetermined number of
partitions.
• Hash keys are used effectively and efficiently in cases where ranges are not applicable, like
employee number, product ID, etc.
For this purpose, we use default partitions on range and list partitioned tables.
For both range and list partitions, data can be stored temporarily, which is out-of-range, by creating a
default partition and later creating an appropriate partition.
Hash-partitioned tables may not have a default partition, as the creation of a default partition for hash
partitioning does not make any sense and is not needed.
Here, we see what happens when I try to insert data for which a partition doesn’t exist and how the default
partition helps in this case.
92
INSERT into sales VALUES ( 1001 , ‘MYS’ , ‘Scanners’ , 190000);
ERROR: no partition of relation “sales” found for row
DETAIL: Partition key of the failing row contains (branch) = (MYS).
So the data we inserted is sent to the default partition, and partitions can be created later based on the
data in the default table and available partitions.
Conclusion
Here, we discussed default partitioning techniques in PostgreSQL using single columns, and we can also
create multi-column partitioning. PostgreSQL Partition Manager(pg_partman) can also be used for creating
and managing partitions effectively.
93
Partitioning in PostgreSQL With pg_partman
(Serial-Based & Trigger-Based)
By Neha Korukula
Neha has been a Jr. PostgreSQL DBA in Managed Services at Percona since
2022 with extensive experience on installation, user configuration, setting
up high availability, performance tuning, and migrations with minimal /zero
downtime.
94
Partitioning by range without template
You can create partitions based on a serial-based column using pg_partman. For example:
Step one: Create a table with native partitioning type by range using serial-typed column.
partman=# CREATE TABLE students (id INT PRIMARY KEY , fname VARCHAR(20) , lname VARCHAR (20) , dob
DATE NOT NULL ,joined DATE )
PARTITION BY RANGE (id);
CREATE TABLE
Step two: Create parent to create initial child partitioned tables without template table.
The structure of the parent table with its partitions created is as follows.
partman=# d+ students
Partitioned table “[Link]”
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats
target | Description
--------+-----------------------+-----------+----------+---------+----------+-------------+---
id | integer | | not null | | plain |
fname | character varying(20) | | | | extended |
lname | character varying(20) | | | | extended |
dob | date | | not null | | plain |
joined | date | | | | plain |
Partition key: RANGE (id)
Indexes:
“students_pkey” PRIMARY KEY, btree (id)
Partitions: students_p100000 FOR VALUES FROM (100000) TO (200000),
students_p200000 FOR VALUES FROM (200000) TO (300000),
students_p300000 FOR VALUES FROM (300000) TO (400000),
students_default DEFAULT
95
Here, we insert 1,000,000 rows to view data moving into partitioned tables.
partman=# INSERT INTO students (id ,fname,lname,dob ,joined) VALUES ( generate_series(1, 1000000)
,(array[‘Oswald’, ‘Henry’, ‘Bob’, ‘Vennie’])[floor(random() * 4 + 1)],
(array[‘Leo’, ‘Jack’, ‘Den’, ‘Daisy’ ,’Woody’])[floor(random() * 5 + 1)], ‘1995-01-01’::date +
trunc(random() * 366 * 3)::int,
generate_series(‘1/1/2022’::date, ‘08/31/2023’::date, ‘1 day’));
INSERT 0 1000000
Using functions, we can create child partitions and move the data from default to child tables.
Here, we see that, while performing run_maintenance_proc to create needed partitions for existing data in
default tables, it’s a violation of rows as it helps to create new partitions needed as per the data but not to
insert data to new partitions. So, we can use the function partition_data_proc to move the data.
96
Here, we actively move the data from the default to the newly created partitioned table.
Creating the parent table with the partman option (more about parent table creation options) allows us
to create trigger-based partitions using pg_partman’s method of partitioning, which has less read-write
performance compared to native/declarative partitioning.
Step one: Create a table with partman-type range partitioning using serial-typed columns and insert
random data of 5,000 rows.
partman=# CREATE TABLE donors (id INT PRIMARY KEY , name VARCHAR(20) , bloodgroup VARCHAR (15) ,
last_donated DATE NOT NULL, contact_num VARCHAR(10));
CREATE TABLE
partman=# INSERT INTO donors (id , name , bloodgroup , last_donated , contact_num) VALUES (gener-
ate_series(1, 5000) ,’user_’ || trunc(random()*100) ,
(array[‘A+ group’, ‘A- group’, ‘O- group’, ‘O+ group’,’AB+ group’,’AB- group’,’B+ group’,’B-
group’])[floor(random() * 8 + 1)] , ‘2022-01-01’::date + trunc(random() * 366 * 1)::int,
CAST(1000000000 + floor(random() * 9000000000) AS bigint));
INSERT 0 5000
Step two: Create parent to create initial child partitioned tables without template table.
97
The table structure has been created with child tables based on the default premake value of four on
partman.part_config and a trigger named donors_part_trig.
partman=# d+ donors
Table “[Link]”
Column | Type | Collation | Nullable | Default | Storage | Compression |
Stats target | Description
--------------+-----------------------+-----------+----------+---------+----------+-----------
id | integer | | not null | | plain |
name | character varying(20) | | | | extended |
bloodgroup | character varying(15) | | | | extended |
last_donated | date | | not null | | plain |
contact_num | character varying(10) | | | | extended |
Indexes:
“donors_pkey” PRIMARY KEY, btree (id)
Triggers:
donors_part_trig BEFORE INSERT ON donors FOR EACH ROW EXECUTE FUNCTION donors_part_trig_func()
Child tables: donors_p1000,donors_p2000,donors_p3000,donors_p4000, donors_p5000,
donors_p6000,donors_p7000, donors_p8000,donors_p9000
Access method: heap
Consequently, upon inserting new data into the table, we notice that if the partition exists, the data
seamlessly moves into the respective partitions; however, if the partition doesn’t exist, the data remains
within the main table.
partman=# INSERT INTO donors (id , name , bloodgroup , last_donated , contact_num) VALUES (gener-
ate_series(5001, 7000) ,’user_’ || trunc(random()*100) ,
(array[‘A+ group’, ‘A- group’, ‘O- group’, ‘O+ group’,’AB+ group’,’AB- group’,’B+ group’,’B-
group’])[floor(random() * 8 + 1)] , ‘2022-01-01’::date + trunc(random() * 366 * 1)::int,
CAST(1000000000 + floor(random() * 9000000000) AS bigint));
INSERT 0 0
98
In this scenario, inserting data where the partition doesn’t exist results in directing the data to the main table.
Subsequently, manual movement of this data occurs in batches to the newly created partitions.
SELECT donors_p1000,donors_p2000,donors_p3000,donors_p4000,donors_p5000,
donors_p6000,donors_p7000,donors_p8000,donors_p9000,donors_totalcnt from
( SELECT COUNT(*) FROM donors_p1000 ) AS donors_p1000, ( SELECT COUNT(*) FROM donors_p2000) AS donors_p2000, (
SELECT COUNT(*) FROM donors_p3000 ) AS donors_p3000,
( SELECT COUNT(*) FROM donors_p4000 ) AS donors_p4000,( SELECT COUNT(*) FROM donors_p5000) AS donors_p5000,(
SELECT COUNT(*) FROM donors_p6000 ) AS donors_p6000,
( SELECT COUNT(*) FROM donors_p7000 ) AS donors_p7000,( SELECT COUNT(*) FROM donors_p8000) AS donors_p8000, (
SELECT COUNT(*) FROM donors_p9000) AS donors_p9000,
( SELECT COUNT(*) FROM donors ) AS donors_totalcnt ;
donors_p1000 | donors_p2000 | donors_p3000 | donors_p4000 | donors_p5000 | donors_p6000 | donors_p7000 | do-
nors_p8000 |
donors_p9000 | donors_totalcnt
--------------+--------------+--------------+--------------+--------------+--------------+--------------+------
--------+--------------+-----------------
(1000) | (1000) | (1000) | (1000) | (1000) | (1000) | (0) | (0)
|
(0) | (6000)
(1 row)
partman=# INSERT INTO donors (id , name , bloodgroup , last_donated , contact_num) VALUES (generate_se-
ries(14001, 15000) ,’user_’ || trunc(random()*100) ,
(array[‘A+ group’, ‘A- group’, ‘O- group’, ‘O+ group’,’AB+ group’,’AB- group’,’B+ group’,’B- group’])[floor(ran-
dom() * 8 + 1)] , ‘2022-01-01’::date + trunc(random() * 366 * 1)::int,
CAST(1000000000 + floor(random() * 9000000000) AS bigint));
INSERT 0 1000
partman=# CALL partman.partition_data_proc(‘[Link]’);
NOTICE: Batch: 1, Rows moved: 999
NOTICE: Batch: 2, Rows moved: 1
NOTICE: Total rows moved: 1000
NOTICE: Ensure to VACUUM ANALYZE the parent (and source table if used) after partitioning data
CALL
partman=# VACUUM ANALYZE donors;
VACUUM
Employing functions to proactively create partitioned tables and migrate data enables the proactive
planning of partitions ahead of inserts, preventing data accumulation in default partitions or the main table.
99
Schema design
Schema design in PostgreSQL focuses on optimizing database
architecture for efficient data storage, retrieval, and processing
and involves setting up tables, defining relationships, choosing
appropriate data types, and implementing constraints to maintain
data integrity and enhance performance. Tailoring the design to
the application’s specific needs is crucial, and in this chapter, we
highlight some of the approaches recommended by our experts to
craft efficient and robust database schemas in PostgreSQL
100
Diffing PostgreSQL Schema Changes
By Robert Bernier
101
Using logical dump manifests
The easiest way to identify changes between schemas from one database to another is to compare
schema dump manifests.
The following example demonstrates an approach one can take looking for differences between schema on
different databases:
EXAMPLE:
-- db01: version 1
create table t1 (
c1 int,
c2 text,
c4 date
);
-- db02: version 2
create table t1 (
c1 serial primary key,
c2 varchar(256),
c3 date default now()
);
# generate manifests
pg_restore -l [Link] > db01_manifest.ini
pg_restore -l [Link] > db02_manifest.ini
102
This snippet demonstrates looking for differences by comparing the md5 checksums:
# EX 1: generate checksums
md5sum
<(tail +16 db01_manifest.ini | cut -d ‘ ‘ -f 4-)
<(tail +16 db02_manifest.ini | cut -d ‘ ‘ -f 4-)
# output
$ 9d76c028259f2d8bed966308c256943e /dev/fd/63
$ ba124f9410ea623085c237dc4398388a /dev/fd/62
This next snippet diffs the differences between the two manifests, identifying only those objects and
attributes that have changed. Notice that redundant information, the first 16 lines, are skipped:
# EX 2: perform diff
diff
> <(tail +16 db01_manifest.ini | cut -d ‘ ‘ -f 4-)
> <(tail +16 db02_manifest.ini | cut -d ‘ ‘ -f 4-)
This resultant diff shows the changes made between the two schemas:
1a2,3
> SEQUENCE public t1_c1_seq postgres
> SEQUENCE OWNED BY public t1_c1_seq postgres
2a5,12
> SEQUENCE public t2_c1_seq postgres
> SEQUENCE OWNED BY public t2_c1_seq postgres
> DEFAULT public t1 c1 postgres
> DEFAULT public t2 c1 postgres
> CONSTRAINT public t1 t1_pkey postgres
> CONSTRAINT public t2 t2_pkey postgres
> INDEX public t2_c5_idx postgres
> FK CONSTRAINT public t2 t2_c5_fkey postgres
The good news is that there are a number of existing tools that can reconcile differences between a
proposed schema design and the target schema:
• Commercial offerings can differentiate schema between databases in an elegant and efficient
manner. Researching, ala Google, yields the most popular technologies one can use.
• In regards to open source solutions, there are a number of projects capable of diffing Postgres
database schemas.
103
Working with the apgdiff extension
Apgdiff can be found in the Postgres community repository. It compares two schema dump files and
creates an SQL output file that is, for the most part, suitable for upgrades of old schemata:
Package: apgdiff
Version: 2.7.0-1.pgdg18.04+1
Architecture: all
Maintainer: Debian PostgreSQL Maintainers <team+postgresql@[Link]>
Installed-Size: 173
Depends: default-jre-headless | java2-runtime-headless
Homepage: [Link]
Priority: optional
Section: database
Filename: pool/main/a/apgdiff/apgdiff_2.7.0-1.pgdg18.04+1_all.deb
Size: 154800
SHA256: 9a83fcf54aed00e1a28c3d00eabe1c166977af1e26e91035e15f88b5215b181b
SHA1: ea713acb55898f07374dadd1bebb09ec2fa4b589
MD5sum: e70a97903cb23b8df8a887da4c54e945
The following example demonstrates how one can update differences between the development
environment and the production database schema using apgdiff.
EXAMPLE:
There’s more you can accomplish with these simple approaches. By incorporating variations of these, one
can create fairly sophisticated shell scripts with little code and, with a little luck, not that much effort.
104
Load balancing/
connection pooling
As your database expands, managing and optimizing its
performance can become increasingly difficult. This is where tools
like Pgpool-II, PgBouncer, and HAProxy can come into play. In this
chapter, we highlight their use cases and benefits and demonstrate
using a combination of PgBouncer and HAProxy in order to scale
PostgreSQL.
105
Pgpool-II Use Cases and Benefits
By Kai Wagner
106
Pgpool-II
This is where the pgpool-II comes in. Pgpool-II is a powerful tool that can help you manage and optimize
the performance of your PostgreSQL database cluster. Let’s explore the benefits of using pgpool-II for
PostgreSQL.
1. Load balancing
One of the primary benefits of using pgpool-II is its ability to distribute incoming client connections across
multiple PostgreSQL servers, allowing you to balance the load and increase the capacity of your database
cluster. This means that as your application grows and more clients connect to the database, pgpool-II can
distribute the load across multiple servers, preventing any one server from becoming overwhelmed.
2. Connection pooling
Pgpool-II can maintain a pool of idle database connections that can be reused by multiple clients, reducing
the overhead of establishing new connections and improving application performance. This is particularly
useful for applications that frequently open and close database connections, as it reduces the time it takes
to establish a connection.
3. Query caching
Pgpool-II can cache frequently used queries in memory, reducing the load on your PostgreSQL servers and
improving response times. This means that when a query is executed, pgpool-II can check the cache first
to see if the results are already available rather than sending the query to the database server. This can
significantly improve query response times and reduce the load on your database servers.
4. High availability
Pgpool-II can automatically redirect client connections to a backup server when a PostgreSQL server
becomes unavailable. Furthermore, the online recovery feature allows new standby nodes to be added
seamlessly. Moreover, Pgpool’s watchdog module ensures that if one node goes down, another node
takes over its place, resulting in minimal to no interruption for client applications. With automatic failover,
online recovery, and watchdog, your application can continue functioning without interruption or manual
intervention, even if one or more database servers or pgpool-II servers go down.
5. Online recovery
Pgpool-II allows you to perform online recovery of a failed PostgreSQL server without interrupting service,
minimizing downtime and ensuring data integrity. This means that if a server fails, you can perform recovery
operations without interrupting the service, allowing your application to continue operating as normal.
In conclusion, the pgpool-II extension provides a range of powerful features that can help you optimize
the performance, availability, and scalability of your PostgreSQL database cluster. Whether you need load
balancing, connection pooling, query caching, high availability, parallel query execution, or online recovery,
pgpool-II has you covered.
By using pgpool-II, you can improve the reliability and performance of your PostgreSQL database cluster,
ensuring that your application continues to function smoothly as your database grows. Percona fully
supports Pgpool-II; you can download the latest builds from our repository.
107
Scaling PostgreSQL using Connection
Poolers and Load Balancers for an
Enterprise Grade environment
By Avinash Vallarapu
Avinash joined Percona in May 2018 and was PostgreSQL Tech Lead -
Global Services until 2021. Before joining Percona, Avi worked as a Database
Architect at OpenSCG for two years and as a DBA Lead at Dell for 10 Years in
Database technologies.
Jobin Augustine
108
Why connection pooling?
When it comes to improving the efficiency of a database workload, one of the first places we start looking
at is the list of slow queries; if the most popular ones can be optimized to run faster, then we can easily
gain some overall performance back. Arguably, we may look next at the number and frequency of client
connections: is the workload composed of a high number of very frequent but short-lived connections? Or
are clients connections of a more moderate number and tend to stick around for longer?
If we consider the first scenario further–a high number of short-lived connections–and that each
connection spawns a new OS process, the server may hit a practical limit as to the number of transactions—
or connections—it can manage per second, considering the hardware available and the workload being
processed. Remember that PostgreSQL is process-based, as opposed to thread-based, which is itself an
expensive operation in terms of resources, both CPU and memory.
A possible remedy for this would be the use of a connection pooler, acting as a mediator between the
application and the database. The connection pooler keeps a number of connections permanently opened
with the database and receives and manages all incoming requests from clients itself, allowing them to
temporarily use one of the connections it already has established with PostgreSQL. This removes the burden
of creating a new process each time a client establishes a connection with PostgreSQL and allows it to
employ the resources that it would otherwise use for this to serve more requests (or complete them faster).
Rule of thumb?
A general rule of thumb that we often hear is that you may need a connection pooler once you reach
around 350 concurrent connections. However, the actual threshold is highly dependent on your database
traffic and server configuration: as we found out recently, you may need one much sooner.
You may implement connection pooling using your native application connection pooler (if there is one
available) or through an external connection pooler such as PgBouncer and pgPool-II. For the solution
we have built, which we demonstrated in our webinar on October 10, we have used PgBouncer as our
connection pooler.
PgBouncer
PgBouncer is a lightweight (thread-based) connection pooler that has been widely used in PostgreSQL
based environments. It “understands” the PostgreSQL connection protocol and has been a stable project for
over a decade.
PgBouncer allows you to configure the pool of connections to operate in three distinct modes: session,
statement, and transaction. Unless you have a good reason to reserve a connection in the pool to a single
user for the duration of its session or are operating with single-statements exclusively, transaction mode is
the one you should investigate.
A feature that is central to our enterprise-grade solution is that you can add multiple connection strings
using unique alias names (referred to as database names). This allows greater flexibility when mediating
connections with multiple database servers. We can then have an alias named “master_db” that will route
connections to a master/primary server and another alias named “slave_db” that will route connections to
a slave/standby server.
Scaling up
Once efficiency is taken care of, we can then start working on increasing the resources or computing
power available to process database requests. Scaling vertically means, in short, upgrading the server
to more and faster cores, memory, and storage. It’s a simple approach but one that reaches a practical
limitation rather quickly. It is not in line with other requirements of an enterprise-grade solution, such as
high availability. The alternative is scaling horizontally. As briefly introduced above, a common way for
implementing horizontal scalability is to redirect reads to standby servers (replicas) with the help of a proxy,
which can also act as a load balancer, such as HAProxy. We’ll be discussing these ideas further here and
showcase their integration in our webinar.
109
HAProxy
HAProxy is a popular open source TCP/HTTP load balancer that can distribute the workload across multiple
servers. It can be leveraged in a PostgreSQL replication cluster that has been built using streaming
replication. When you build replication using the streaming replication method, standby replicas are open
for reads. With the help of HAProxy, you can efficiently utilize the computing power of all database servers,
distributing read requests among the available replicas using algorithms such as Least Connection and
Round Robin.
The following diagram represents a simplified part of the architecture that composes the enterprise-grade
solution we’ve designed, where we employ PgBouncer and HAProxy to scale our PostgreSQL cluster:
Our PgBouncer contains two database (alias) names, one for redirecting writes to the master and another
for balancing reads across standby replicas, as discussed above. Here is how the database section looks in
the [Link], PgBouncer’s main configuration file:
[databases]
master = host=haproxy port=5002 dbname=postgres
slave = host=haproxy port=5003 dbname=postgres
Notice that both database entries redirect their connections to the HAProxy server, but each to a different
port. The HAProxy, in turn, is configured to route the connections in functions of the incoming port they reach.
Considering the above pgBouncer config file as a reference, writes (master connections) are redirected to
port 5002, and reads (slave connections) to port 5003. Here is how the HAProxy config file looks:
110
# Connections to port 5002
listen Master
bind *:5002
option tcp-check
tcp-check send GET\ / HTTP/1.0\r\n
tcp-check send HOST\r\n
tcp-check send \r\n
tcp-check expect string “role”:\ “master”
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server pg0 pg0:5432 maxconn 100 check port 8008
server pg1 pg1:5432 maxconn 100 check port 8008
server pg2 pg2:5432 maxconn 100 check port 8008
As seen above:
• connections to port 5002 are sent to server with role: “master”
• connections to port 5003 are sent to server with role: “replica”
HAProxy relies on Patroni to determine the role of the PostgreSQL server. Patroni is being used here for cluster
management and automatic failover. By using Patroni’s REST API (on port 8008 in this scenario), we can
obtain the role of a given PostgreSQL server. The example below shows this in practice, the IP addresses
denoting the PostgreSQL servers in this setup:
HAProxy can thus rely on Patroni’s REST API to redirect connections from the master alias in PgBouncer to a
server with role master. Similarly, HAProxy uses server role information to redirect connections from a slave
alias to one of the servers with role replica using the appropriate load balancer algorithm.
This way, we ensure that the application uses the advantage of a connection pooler to leverage
connections to the database and also of the load balancer, which distributes the read load to multiple
database servers as configured.
There are many other open source connection poolers and load balancers available to build a similar
setup. You can choose the one that best suits your environment—just make sure to test your custom
solution appropriately before bringing it to production.
111
PostGIS
This chapter takes a look into PostGIS, a powerful open source
tool that extends the capabilities of PostgreSQL by introducing
support for storing, indexing, and querying geospatial data.
Recommended by our expert, this section focuses on utilizing
PostGIS to unlock superior performance within PostgreSQL
environments.
112
Using PostGIS To Enable Better
Performance in PostgreSQL
By Robert Bernier
113
What is PostGIS?
PostGIS is a PostgreSQL extension that adds GIS capabilities to this RDBMS. Its popularity stems not only
from being “free” but also from the fact that it’s considered to be among the leading GIS implementations
in the world today. Virtually every major front-end application provides the hooks for a PostGIS, PostgreSQL-
enabled back-end.
The PostGIS project, which is BSD-licensed, began back in 2001. It turns our vanilla-flavored Postgres into a
spatial database and includes spatial datatypes (geometry, geography), spatial indexes (r-tree, quad-tree,
kd-tree), and spatial functions.
Working with GIS normally requires several layers of technology of Geo-Spatial Software, for example:
PostGIS offers a solution to provide geospatial data capabilities in PostgreSQL for indexing, storing, and
querying geographical data.
Spatial Indexing
Spatial indexing accelerates the querying and retrieval of geospatial data in PostgreSQL, improving
response times for applications and analytics. It uses various spatial indexing techniques, such as R-tree
and GiST (Generalized Search Tree), to organize data based on their spatial relationships, allowing the
database to quickly narrow down the search space.
PostGIS offers a wide array of geospatial functions for PostgreSQL, allowing users to perform complex spatial
operations such as distance and area computations, geometric alterations, and buffering. Optimized
spatial queries, like finding nearby points or intersecting polygons, use these functions to retrieve geospatial
data efficiently.
PostGIS supports spatial data types like Point, LineString, and Polygon in 2D and 3D. These allow efficient
spatial data storage and retrieval due to their optimized indexing mechanisms. They enable spatial
indexing, enabling faster query processing by organizing data based on geometric properties.
114
Querying The Spatial Database
It’s amazing the kinds of answers one can get by a single, well-composed, and yet standard query by
asking such simple questions as:
• How far is it from here to there?
• What’s the closest point between two meandering streets?
• Is a street found in a certain zip code?
• How many homes are susceptible to flooding?
Here are a few example queries using a PostGIS-powered database. Refer here for more community-
related shapefiles for Kamloops, British Columbia:
--
-- EXAMPLE 1: What is the total length of all roads in British Columbia?
--
SELECT sum(ST_Length(the_geom))/1000 AS km_roads FROM bc_roads;
km_roads
------------------
70842.1243039643
--
-- EXAMPLE 2: How large is the city of Prince George, British Columbia?
--
SELECT ST_Area(the_geom)/10000 AS hectares
FROM bc_municipality
WHERE name = ‘PRINCE GEORGE’;
hectares
------------------
32657.9103824927
--
-- EXAMPLE 3: What is the largest municipality in the province of British Columbia?
--
SELECT name,
ST_Area(the_geom)/10000 AS hectares
FROM bc_municipality
ORDER BY hectares DESC
LIMIT 1;
name | hectares
---------------+-----------------
TUMBLER RIDGE | 155020.025561
--
-- EXAMPLE 4: What is the length of roads fully contained within each municipality?
--
SELECT [Link],
sum(ST_Length(r.the_geom))/1000 as roads_km
FROM bc_roads AS r,
bc_municipality AS m
WHERE ST_Contains(m.the_geom,r.the_geom)
GROUP BY [Link]
ORDER BY roads_km;
name | roads_km
----------------------------+------------------
SURREY | 1539.47553551242
VANCOUVER | 1450.33093486576
LANGLEY DISTRICT | 833.793392535662
BURNABY | 773.769091404338
PRINCE GEORGE | 694.375543691
115
PostGIS Use Cases
PostGIS extends PostgreSQL’s capabilities to handle geospatial data for solutions in real-world scenarios.
From mapping to visualizations to logistics, PostGIS helps users to make informed decisions based on
spatial insights.
Location-Based Services
PostGIS plays a role in location-based applications by enabling spatial data storage and analysis. For
instance, a grocery delivery service could utilize PostGIS to optimize delivery for certain areas. A query that
finds available drivers within a certain distance, facilitated by PostGIS’s spatial indexing, would significantly
enhance the platform’s responsiveness.
Geospatial Analytics
PostGIS utilizes geospatial data analysis by offering advanced tools for operations such as identifying the
nearest points or calculating areas within polygons. The functions are optimized through spatial indexing
for quicker and better queries of complex geospatial analyses, improving overall performance.
PostGIS improves map rendering and visualization through efficient geospatial data handling. It allows for
quickly retrieving map data to ensure smooth and responsive displays.
Basic Installation
The following installation instructions assume one is using PostgreSQL version 12 on Linux/CENTOS-7,
although any major version of Postgres and OS can be used.
#
# REDHAT/CENTOS DERIVATIVES, USING COMMUNITY POSTGRES
#
yum update -y
yum install -y postgis30_12 postgis30_12-client
#
# CREATE THE DATACLUSTER
#
/usr/pgsql-12/bin/postgresql-12-setup initdb
#
# SERVICE STARTUP
#
systemctl start postgresql-12
#
# INSTALLING THE POSTGIS EXTENSION
# Execute as UNIX account postgres
#
createdb mydatabase;
psql -c ‘create extension postgis’ mydatabase
116
Complete Installation for PostGIS Extension
The following provides a more complete installation of all PostGIS capabilities. Refer to the PostGIS
documentation for more information.
--
-- if you want to install raster support
--
CREATE EXTENSION postgis_raster;
--
-- if you want to install topology support
--
CREATE EXTENSION postgis_topology;
--
-- if you want to install sfcgal support
--
CREATE EXTENSION postgis_sfcgal;
--
-- if you want to install tiger geocoder
--
CREATE EXTENSION fuzzystrmatch
CREATE EXTENSION postgis_tiger_geocoder;
--
-- if you want to install pcre, add the address standardizer extension
--
CREATE EXTENSION address_standardizer;
--
-- list of all installed extensions
--
mydatabase=# select * from pg_available_extensions where name ~ ‘^postgis’;
name | default_version | installed_version | comment
--------------------------+-----------------+-------------------+----------------------------------
-----------------------------------
postgis-3 | 3.0.1 | | PostGIS geometry, geography, and
raster spatial types and functions
postgis_raster-3 | 3.0.1 | | PostGIS raster types and
functions
postgis_sfcgal-3 | 3.0.1 | | PostGIS SFCGAL functions
postgis_tiger_geocoder-3 | 3.0.1 | | PostGIS tiger geocoder and
reverse geocoder
postgis_topology-3 | 3.0.1 | | PostGIS topology spatial types
and functions
postgis | 3.0.1 | 3.0.1 | PostGIS geometry, geography, and
raster spatial types and functions
postgis_raster | 3.0.1 | 3.0.1 | PostGIS raster types and
functions
postgis_sfcgal | 3.0.1 | 3.0.1 | PostGIS SFCGAL functions
postgis_tiger_geocoder | 3.0.1 | 3.0.1 | PostGIS tiger geocoder and
reverse geocoder
postgis_topology | 3.0.1 | | PostGIS topology spatial types
and functions
117
Working With Shapefiles
There are literally hundreds of terabytes available online. Ironically, the most precious data one can get is
free to download because it’s been generated by governments from all over the world for the public good.
Now, it’s time to get some data. The most common format is “shapefile.”
Working with zip codes from the US Census data: TIGER files
TIGER/Line files are a digital database of geographic features, such as roads, railroads, rivers, lakes, legal
boundaries, census statistical boundaries, etc., covering the entire United States and are freely available
here.
Once you’ve navigated to this website, you can download a multitude of fascinating pieces of data.
Example:
c gis_demo
118
Step two: Download the 2019 zip code shapefile
The shapefile is stored in a zip file. Unzip the entire archive before attempting an upload.
URL: [Link]
#
# Execute the following, the actual arguments will vary
#
shp2pgsql -c -D -I tl_2019_us_zcta510.shp [Link] | psql -U postgres gis_demo
Once the upload into the Postgres database is complete, this is what you’ll get:
gis_demo=# d
List of relations
Schema | Name | Type | Owner
----------+-------------------+----------+----------
public | geography_columns | view | postgres
public | geometry_columns | view | postgres
public | spatial_ref_sys | table | postgres
topology | layer | table | postgres
topology | topology | table | postgres
topology | topology_id_seq | sequence | postgres
(6 rows)
gis_demo=# dn
List of schemas
Name | Owner
----------+----------
gis | postgres
public | postgres
topology | postgres
(3 rows)
gis_demo=# d zipcodes
Table “[Link]”
Column | Type | Collation | Nullable | Default
------------+------------------------+-----------+----------+-----------------------------------
----
gid | integer | | not null | nextval(‘zipcodes_gid_
seq’::regclass)
zcta5ce10 | character varying(5) | | |
geoid10 | character varying(5) | | |
classfp10 | character varying(2) | | |
mtfcc10 | character varying(5) | | |
funcstat10 | character varying(1) | | |
aland10 | double precision | | |
awater10 | double precision | | |
intptlat10 | character varying(11) | | |
intptlon10 | character varying(12) | | |
geom | geometry(MultiPolygon) | | |
Indexes:
“zipcodes_pkey” PRIMARY KEY, btree (gid)
“zipcodes_geom_idx” gist (geom)
119
Step three: An example query
--
-- getting a zipcode based upon the LAT/LON
--
gis_demo=# select * from [Link](-87.798615,30.53711);
longitude | latitude | zipcode
------------+----------+---------
-87.798615 | 30.53711 | 36576
There are a couple of things to keep in mind when querying PostGIS-powered databases:
• Follow the PostGIS reference documentation closely: it’s easy to read, explains many of the ideas
behind the function call, and provides many examples.
• The GIS standard requires that functions be documented using mixed case.
• GIS function calls, implemented in Postgres, are written using lowercase.
• For the adventurous, here’s a comprehensive and freely available dataset of shapefiles for the city
of Kamloops, British Columbia.
Because so much of our modern big data insights depend upon raw GIS data, directly querying a GIS
database empowers one to create even more powerful and precise insights. PostGIS is an excellent way for
the DEV, DBA, and SRA to learn all things GIS. Have Fun!
120
Bonus chapter:
Cost optimization
This bonus chapter is dedicated to reducing PostgreSQL costs
in the cloud and is pulled from both the Percona blog and a
comprehensive webinar our experts did on the subject.
If you’ve been tasked with reducing your PostgreSQL costs, this is the
chapter you have been waiting for!
121
Reducing PostgreSQL Costs in the
Cloud
By Mario García
Let’s take a look at how to get the benefits you need while spending
less, based on the recommendations presented by Dani Guzmán
Burgos, our Percona Monitoring and Management (PMM) Tech Lead,
on this webinar (now available on demand) hosted in November
last year.
122
Usage reduction: What to look for to reduce PostgreSQL cloud costs
The first step in cost reduction is to use what you need and not more. Don’t pay for capacity you don’t use or
need.
Usage reduction is a continuous process. Identifying which resources you can trim to reduce your monthly
bill can be difficult, but looking at the right metrics will help you understand the application’s actual
requirements.
In the Home Dashboard of PMM, a low CPU utilization on any of the database services that are being
monitored could mean that the server is inactive or over-provisioned. Marked in red in Figure 1 is a server
with less than 30% of CPU usage. PMM can also show you historical data that can help you identify how long
a service has been in a given state. Configuration of the CPU metrics can be changed in the dashboard.
These color-coded states on panels are available in PMM 2.32.0 and later.
From the Amazon Web Services (AWS) documentation, an instance is considered over-provisioned when
at least one specification of your instance, such as CPU, memory, or network, can be sized down while still
meeting the performance requirements of your workload, and no specification is under-provisioned.
Use reduction at scale requires a cultural shift, and engineers must consider the cost as they think of
memory or bandwidth as another deployment KPI.
Think of a gaming company that creates a game that is getting popular, so the number of resources
needed to support more users would increase considerably. But if the game loses popularity, the server
would become over-provisioned, and the resources allocated must be re-sized to better fit the application’s
needs.
123
Re-sizing to save costs
Regardless of the method you’re using to deploy your PostgreSQL instance, here are some metrics that
would determine when re-sizing is needed:
• CPU utilization
• Memory usage
• Network throughput
• Storage usage
Remember that optimizing your infrastructure is intended for more than cost savings. You have to ensure
that the operation is not impacted when you’re making decisions based on the metrics. The primary goal is
to ensure that the services themselves do not run out of the required operating capacity.
CPU
Considering AWS as your cloud platform of choice, the configuration made for your infrastructure will
influence the performance of your application and monthly costs. For example, an Amazon Elastic Compute
Cloud (EC2) instance with Graviton2 processors will be a better choice than non-ARM options, as it’s
cheaper, and you will get real and faster cores, which means the CPU cores are physical and not with
hyper-threading. Graviton2 processors aim to use Reserved Instances to save costs in the long run.
Storage
Continuing with the AWS example, choosing the right storage option will be key to performance. Amazon
Elastic Block Store (EBS) is your good-to-go option for disk space.
124
Running relational or NoSQL databases is one of the use cases where EBS is recommended. You can deploy
and scale your databases, including SAP HANA, Oracle, Microsoft SQL Server, PostgreSQL, MySQL, Cassandra,
and MongoDB.
With EBS, you can configure HDD-based volumes optimized for large streaming workloads or SSD-based
volumes (recommended for database workloads) optimized for transactional workloads.
Which one is a better choice for storage? It will depend on the requirements of your workload, including disk
space, input/output operations per second (IOPS), and throughput rate (MB/s). Your configuration must be
cost-optimized as well.
Avi Drabkin’s blog post is a recommended reading on this matter, as he analyzes the configuration required
for every volume type to satisfy the requirements of a particular use case. For more information on EBS
volume types, check the Amazon EBS Volume Types page.
Multi-AZ deployments vs. read replicas
Multi-AZ deployment
In an Amazon RDS Multi-AZ deployment, Amazon RDS automatically creates a primary database (DB)
instance and synchronously replicates the data to an instance in a different AZ. Amazon RDS automatically
fails over to a standby instance without manual intervention when it detects a failure.
125
Read replica
Amazon RDS creates a second DB instance using a snapshot of the source DB instance. It then uses the
engines’ native asynchronous replication to update the read replica whenever there is a change to the
source DB instance. The read replica operates as a DB instance that allows only read-only connections;
applications can connect to a read replica just as they would to any DB instance. Amazon RDS replicates all
databases in the source DB instance.
Multi-AZ deployments offer advantages, especially for HA and disaster recovery. The trade-off is that multi-
AZ deployments are expensive.
A better option would be to deploy reader instances and combine them with the use of a reverse proxy, like
pgpool-II or pgbouncer. The reader instances also cost more than a standard setup, but you can use them
for production to handle everyday database traffic.
Pgpool-II can be used not only for reducing connection usage, which will be helpful in reducing CPU and
memory usage but also for load balancing. With load balancing, you can redistribute the traffic, sending the
reading requests to your read replicas and writing requests to your main database instance automatically.
Regarding read replicas, in AWS, you cannot promote an RDS PostgreSQL read replica, which means a read
replica can’t become the primary instance. Whenever you try to do this, the read replica detaches from the
primary instance and become its own primary instance, and you will end up having two different clusters.
One solution is using the pglogical extension for creating replicas outside the RDS path. When combining the
pglogical replication with a reverse proxy, you will still get the benefits of a managed database, including
backups, minor upgrades maintenance, recovery support, and being tied to the Multi-AZ configuration,
which translates to full control over planned failovers.
Also, converting a replica to the primary instance would be a better upgrade approach. For example, if you
need to upgrade a database with a large amount of data, this process could take hours, and your instance
won’t be available during this time. So, with this configuration, you can upgrade a replica and later convert
that replica to the primary instance without interrupting operations.
Vacuum
Bloating in the database is created when tables or indexes are updated; an update is essentially a delete-
and-insert operation. The disk space used by the delete is available for reuse but not reclaimed, creating
the bloat.
126
How to remove bloat? That’s what the vacuum process is intended for with the help of autovacuum and
vacuumdb.
Autovacuum is a daemon that automates the execution of VACUUM and ANALYZE (to gather statistics)
commands. Autovacuum checks for bloated tables in the database and reclaims the space for reuse.
vacuumdb is a utility for cleaning a PostgreSQL database. vacuumdb will also generate internal statistics
used by the PostgreSQL query optimizer. vacuumdb is a wrapper around the SQL command VACUUM.
Running autovacuum on a database with tables that store tens of terabytes can become such a big
overhead, not only for the amount of data but the dead tuples generated on every transaction. A solution
could be changing the EBS volume type for an io1 that can support this workload, but your monthly bill would
also increase.
A better solution for this scenario would be running the vacuum process on demand. Disabling autovacuum
doesn’t mean not running it at all. You just run it on demand on lower-traffic hours where the overhead on
the disk performance doesn’t affect the operation. The overhead of running with a high percentage of dead
tuples is cheaper in that case.
This way, you guarantee that the resources will be available during the day when most operations occur.
Is this the best solution for every scenario? No, it will depend on the requirements of your database.
Monitoring your database for dead tuples (bloat) is also recommended. For this matter, you can use the
Experimental PostgreSQL Vacuum Monitoring. This experimental dashboard is not part of PMM, but you can
try it and provide feedback.
With serverless, you truly pay only for what you’re actively using, and unused resources aren’t typically
easy to be left flying around, but the move to serverless isn’t without cost. The complexity of building any
migration plan to serverless resides in execution and has very little to do with cost savings. There’s an
entirely different lens through which you can evaluate serverless: total cost of ownership (TCO).
The TCO refers to the cost of engineering teams that are required to build a solution and the impact of
time to market on the success and profitability of a service. Serverless allows you to delegate a lot of
responsibility to the cloud provider.
Duties that DevOps engineers would typically perform (server management, scaling, provisioning, patching,
etc.) become the responsibility of AWS, GCP, or Azure. It leaves dev teams with free time to focus on shipping
127
differentiated features faster.
With TCO, you must consider that people’s costs may cancel out any infrastructure savings when
considering moving from a monolithic application to a serverless one.
Returning to the benefits versus effort, you should consider the overall cost of redesigning services for
serverless against the potential for reducing costs.
Conclusion
Knowing your project’s database requirements will be essential when choosing the services and hardware
configuration you will pay your cloud service provider for. The configuration you make must guarantee the
proper functioning of your application and will determine the monthly costs.
The number of resources required may vary over time, and metrics like CPU usage, memory usage, and disk
storage will help you determine when re-sizing your infrastructure is needed.
For example, if the number of database transactions decreases after a certain period of time, you will have
more resources than you need, and it will be important to change the configuration of your infrastructure to
guarantee the new requirements and pay what is really being used.
128
Conclusion
As we wrap up PostgreSQL Performance Tuning, we hope you have gained some of the
knowledge and tools necessary to navigate the complexities of PostgreSQL optimization.
Each chapter, curated with expertise from Percona’s seasoned professionals, has
provided insight into such advanced topics as partitioning, schema design, query
optimization… and everything in between. While it’s not a comprehensive guide to
all things PostgreSQL performance, the strategies inside will help you manage and
maintain a PostgreSQL environment that is optimized for peak performance and aligned
with the principles of cost efficiency and scalability.
129
Databases Run Better
with Percona
[Link]