Introduction to PostgreSQL Performance Tuning and Optimization

Optimising the performance of a PostgreSQL database is a crucial task for any database administrator. The complexities in fine-tuning a database can be vast, but a few adjustments can lead to significant improvements. Configuring your system to handle large pages, for example, can greatly enhance memory management efficiency. This involves a process that requires changing system settings and restarting your server, ensuring that changes are effectively applied.

In the realm of database optimisation, it’s important to understand the variety of parameters that contribute to the overall performance. From initial setup to query tuning and regular maintenance tasks like autovacuum, all play a vital role. Taking a strategic approach to tune these parameters can help in reducing latency, speeding up query times, and ultimately providing a more stable and faster database environment.

Key Takeaways

  • Changing system settings can improve PostgreSQL memory management efficiency.
  • Various parameters from setup to maintenance impact database performance.
  • Strategic tuning of a PostgreSQL server can enhance query speed and stability.

Enhancing File System Efficiency

To improve your disk’s performance in a Linux environment, consider altering the way your system handles file access timestamps. By default, every read operation updates the file’s last-accessed record, consuming unnecessary CPU resources. You can optimise this aspect by tweaking your file system’s mount options.

Adjust the /etc/fstab file to include the noatime option alongside the default settings for the partitions that house your PostgreSQL data and Write-Ahead Logging (WAL) files. An example mount entry is shown below:

  • /dev/mapper/pgdata-01-data /pgdata xfs defaults,noatime 1 1

To implement these changes without restarting, execute:

mount -o remount,noatime /pgdata

This adjustment is particularly beneficial for spinning disks, as it reduces the disk’s workload for read and write operations. Remember, this is the starting point of optimising your system; ongoing monitoring of both the operating system and PostgreSQL performance is imperative to identify opportunities for further enhancements. Frequent hardware reviews, including network cards and disk spaces, may also lead to considerable performance uplifts.

Large Memory Pages in PostgreSQL

When using PostgreSQL on a Linux system, performance can be improved by utilising large memory pages, commonly referred to as ‘huge pages’. Linux generally uses a default page size of 4kB, which when using memory-intensive applications like PostgreSQL could result in inefficiencies due to fragmentation. To mitigate this, huge pages are used to allocate memory in larger blocks, which can significantly boost the application’s performance.

To begin configuring huge pages, you will need to determine the exact number required for your PostgreSQL instances. This can be done by checking the ‘’ file for the main process ID, then retrieving the ‘VmPeak’ value which indicates the peak virtual memory size.

For a quick check of the process ID, use the command:

head -n 1 $PGDATA/

To locate the ‘VmPeak’, run:

grep -i vmpeak /proc/{process_id}/status

Should you operate multiple PostgreSQL instances, sum up the VmPeak values. Next, verify the huge page size on your system:

grep -i hugepagesize /proc/meminfo

Divide the VmPeak sum by the huge page size to find out the number of huge pages needed. It’s prudent to allocate a slightly larger number than the calculated result to ensure adequate memory, but be cautious not to overestimate excessively as this can cause system boot issues or affect other processes that require standard page sizes.

Proceed to adjust the ‘tuned.conf’ by adding your calculated huge page number to the [sysctl] section. For instance:


Apply the new settings with:

tuned-adm profile edbpostgres

Now set ‘huge_pages’ to ‘on’ within the ‘postgresql.conf’ file and restart PostgreSQL to activate the changes.

Furthermore, modify the PostgreSQL service unit file to establish that the tuned service starts before PostgreSQL after a system reboot. Edit with:

systemctl edit postgresql-13.service

Then include:


After saving the changes, reload the daemon to ensure they are adopted:

systemctl daemon-reload

By following these steps, you should have your PostgreSQL instance configured to use huge pages, potentially enhancing the performance of your database server.

Initial Steps in Improving PostgreSQL Efficiency

Tweaking Settings & Access Controls

In tailoring your PostgreSQL to achieve better operational results, precise adjustment of settings and access control mechanisms is crucial. This involves modifying specific configurations in the postgresql.conf file which serves as the database’s main configuration repository.

Optimal Connection Numbers

Determining the appropriate max_connections setting is pivotal for balancing resource availability and system workload. Aim for the highest of either 100 or a count quadruple that of your CPU cores. Exceeding this could lead to unnecessary strain on the system, while suboptimal figures might not adequately sustain your needs. For more extensive demands, consider implementing a connection pooler, such as pgbouncer, to maintain efficient resource management.

Remember, the max_connections parameter’s influence is quite significant as it directly affects PostgreSQL’s internal data structure sizes, which could lead to CPU inefficiency if not configured properly. Adjust carefully to suit your performance requirements.

Memory Allocation and Management

Buffer Distribution

When considering buffer allocation, various tasks may demand differing sizes. An initial guide may be to allocate the lesser of half your system’s Random Access Memory (RAM) or 10GB. Although there is no rigid science behind this guidance, it stems from the PostgreSQL community’s accumulated experience. Kernel cache and buffer interactions present complexities not easily outlined, so this approach tends to yield acceptable outcomes.

Working Memory

A suggested baseline calculation for your working memory setup is to subtract your buffer allocation from your total RAM, then divide this figure by sixteen times the quantity of CPU cores your system has. Should multiple queries saturate your memory, CPU capacity would frequently be your limiting factor. Within each query plan, every operation node could potentially employ memory volumes designated here, making prudence advisable to avoid excessive allocation.

Maintenance Memory

Your system’s maximum memory usage for database maintenance — inclusive of tasks such as vacuuming and index creation — often determines the speediness of these operations. Commencing with 1GB can be judicious, facilitating these tasks to complete more swiftly and potentially enhancing I/O activity during their execution.

Vacuum Working Memory

Adjusting your maintenance memory higher empowers each autovacuum worker to utilise equivalent memory. For each unneeded tuple the vacuum clears, it uses 6 bytes. Hence, an assignment of 8MB can manage approximately 1.4 million such tuples, influencing the efficiency of cleanup processes.

I/O Request Management

Adapting the effective_io_concurrency value is meant to align with your storage’s disk number, enhancing read-ahead capabilities during database operations. With traditional spinning disks arranged in a striped RAID, this setting assists PostgreSQL in parallel read predictions. However, solid-state drives (SSDs) exhibit different properties, advocating for a 200 setting to capitalise on their distinct performance characteristics.

Write-Ahead Log

WAL Data Compression

Enabling data compression for Write-Ahead Logging (WAL) can significantly improve the efficiency of your database. Switch it on to allow the PostgreSQL server to condense full-page images in the WAL, when full-page write operations are activated or during a base backup.

Hints in WAL Files

For facilitating the use of tools like pg_rewind, you should ensure the hints logging feature within WAL is switched on. This aids in the seamless tracking of necessary changes.

Memory Allocation for WAL

Allocate sufficient space for the WAL data to reside in memory, which allows for background writing to disk. A setting of 64MB can improve performance as it offers substantial space to buffer the WAL segments, each typically sized at 16MB.

Timing of Checkpoints

Strike a balance with your checkpoint intervals as they affect WAL volume and recovery time. The recommendation is a minimum duration of 15 minutes, subject to your recovery point objectives (RPO).

Checkpoint Completion

You’re advised to aim for nearly complete checkpoints within 90% of the designated checkpoint interval. This strategy smoothens the input/output load by dispersing checkpoint-related write operations.

Upper Limit on WAL Size

To ensure efficient checkpointing initiated by timeouts, set the max_wal_size cautiously. It’s advised to use between one-half and two-thirds of the total disk space allocated for WAL to prevent out-of-space issues.

Archive Mode

Keep the archive mode activated if there is any possibility that you might require WAL archiving in the future, as altering this setting will necessitate a system restart.

Archiving WAL Files

A placeholder archive command, ‘: to be configured’, is recommended until actual archiving is set up. This returns a success message, allowing PostgreSQL to understand that the WAL segment can be recycled or deleted.

By adhering to these suggestions and adjustments, you can enhance the reliability and performance of your PostgreSQL database’s WAL operations.

Query Tuning

Disc Access Cost

To optimise your queries, adjusting the random_page_cost parameter is crucial. This setting informs the PostgreSQL query planner about the expense of retrieving a random disk page. If your system is equipped with SSDs, consider setting this to 1.1 to promote the use of index scans. With standard spinning disks, the default setting generally suffices. Adjust this value not just globally but for individual tablespaces based on their storage mediums.

Cache Size Estimation

The effective_cache_size parameter helps with the estimation of memory available for caching data. It’s sensible to set this to either three-quarters of your total RAM or to the aggregated value of buffer/cache, available memory, and shared buffers—whichever is smaller. For instance, given a system with 64GB RAM and 8GB allocated for shared buffers, the effective_cache_size would be the minimum of 64GB * 0.75 and the sum 58GB + 5.8GB + 8GB (assuming these are the free command outputs), resulting in 48.2GB.

Row Processing Cost

Last but not least, review the cpu_tuple_cost. This is the projected cost for processing each individual row. The default might be set too low at 0.01, and it’s often beneficial to increase this to 0.03 to reflect a more accurate cost, thereby improving the query planner’s decisions and overall query performance.

Reporting and Logging

Gathering Logs

To effectively capture log output, particularly when stderr or csvlog are included as destinations, enable the log collection feature.

Log File Location

Once you enable the log collector, specify a directory location separate from the data path. Logs maintained outside the data directory will not be included in foundational backups, keeping them isolated and manageable.

Checkpoint Logging

Activate checkpoint logging to ensure that checkpoints occur as scheduled by the checkpoint_timeout setting rather than being driven by the max_wal_size threshold. This information is crucial for future diagnostics.

Log Line Identifier

The log line prefix must be structured to assist in quick diagnosis. It should include at a minimum the timestamp, process identifier, line number, user, database, and application name. For example, ‘%m [%p-%l] %u@%d app=%a ‘—make note of the space at the end.

Lock Wait Logging

For identifying slow queries due to lock waits, turn this setting on.

Statement Logging

Record data definition language (DDL) statements to generate a basic audit trail and assist in pinpointing the exact moment of critical errors, like an unintended table deletion.

Temporary File Recording

Log all temporary files by setting this to zero, which can indicate whether your work_mem is configured properly.

Collecting Timed Statistics (EPAS)

For the Dynamic Runtime Instrumentation Tools Architecture (DRITA) functionality, the timing data feature should be enabled. Activate it to collect timing information, bringing insight into your query execution.

Autovacuum Settings

Monitoring Vacuum Activity

To optimise the autovacuum function, it’s integral to keep track of its operations. By setting the monitoring value to zero, every instance of the autovacuum process will be logged, allowing for a detailed view of its performance.

Worker Limit for Autovacuum

Autovacuum is supported by a set number of workers, with a default quantity of three. Only a single worker can operate on a table at any given time, so having more workers enhances the ability for simultaneous and regular maintenance across diverse tables. To improve efficiency, consider setting this limit to five workers. Remember, modifying the number of workers necessitates a restart of the database server.

I/O Quota for Autovacuum

There’s a cap on the I/O resources the autovacuum can use, preventing it from monopolising the server’s bandwidth. The vacuum’s I/O consumption is metered by a quota, which, once reached, causes the autovacuum to pause. The starting quota is modest, so to maintain a steady cleaning pace, a rise to a quota limit of 3000 might be beneficial. This adjustment allows the vacuum to proceed with more intensive operations without frequent interruptions.

Client Connection Defaults

Timeout for Idle Transactions

If your transactions remain idle for an extended period, they might block other operations. To avoid this, configure your system to terminate these sessions after a certain duration, such as 10 minutes. This ensures a stalled transaction does not indefinitely hold resources. However, your application should be capable of handling such terminations gracefully.

Localisation Settings for Messages

For effective log analysis, it’s best to set your logging messages to a non-localised format (‘C’). Doing so ensures compatibility with log analysers that might not interpret translated messages correctly.

Essential Libraries to Preload

Consider preloading libraries like pg_stat_statements as it provides significant insights with minimal performance overhead. Although it’s not compulsory, integrating this can optimise tracking and analysing database performance metrics.

Remember to adjust your listen_addresses or incorporate a connection pooler like PgBouncer to manage your database connections efficiently and ensure that your superuser settings are secure.

Identifying Inefficient SQL Queries

To uncover inefficient SQL commands that may be hindering your database performance, two primary strategies can help:

Using log_min_duration_statement

This parameter acts like a stopwatch, recording any SQL query that surpasses a specific duration threshold, allowing you to single out longer-running queries. For example, even setting this to one millisecond (1ms) will catch every query, but be aware this might overwhelm your system with excessive logging details.

A common practice is to begin with a boundary of one second (1s), and step by step reduce this value as you refine your queries. Persist with this iterative process until you arrive at a time limit that balances detailed scrutiny with manageable logging.

Do keep in mind: Not all frequently executed, shorter-duration queries will get caught in this net, yet their cumulative effect can be more taxing on the system than less frequent, longer-running ones.

Implementing the pg_stat_statements Module

For a comprehensive overview, including those elusive, often-run yet brief queries, enabling the pg_stat_statements module is advised. Essential to note, this requires adding it to shared_preload_libraries and restarting the server, but its marginal footprint and significant insight typically justify the initial setup.

What the pg_stat_statements delivers is a digest of all queries processed by the server: it normalizes, combines similar patterns, and collates them into one record brimming with metrics like execution count, total time, and min/max durations, giving you a precise benchmark to identify and tackle inefficient queries.

Keep in mind, ‘similar’ in this context means syntax structure, not the literal content. For example, SELECT * FROM table WHERE id = 10; would be aggregated with SELECT * FROM table WHERE id = 20; due to their matching formats.

To start gathering data, you’ll need to install the pg_stat_statements extension using CREATE EXTENSION pg_stat_statements;. This creates a view where you can observe the collected statistics.

Be conscious of security: this module indiscriminately gathers stats across all user and database interactions on the server. The extension can be added to one or more databases and, by default, restricts users to only see their queries. Superusers and those with pg_read_all_stats or pg_monitor roles are exempt from this restriction, gaining visibility of every query.

Remember: While dealing with datasets that exhibit signs of bloat, such as excessive, unused space or redundant data, the insights from pg_stat_statements may provide indications for cleanup that could further streamline your query performance.

Lastly, for a more intuitive experience, consider using tools like EDB’s PostgreSQL Enterprise Manager (PEM), which features an SQL Profiler for a more user-friendly analysis of your SQL queries’ performance metrics.

Refining Query Structure

Utilising Exposed Column References

When crafting SQL queries, aim for a construct where the column of interest remains unencumbered on one side of the condition. For instance, rather than adding intervals directly to a timestamp column in the WHERE clause, compare the column to a manipulated version of the current timestamp. This allows the effective use of indexes and enhances query speed significantly. Here’s a structure to follow:

Instead of:

WHERE t.a_timestamp + interval '3 days' < CURRENT_TIMESTAMP

Adopt this format:

WHERE t.a_timestamp < CURRENT_TIMESTAMP - interval '3 days'

Avoiding NOT IN with Subqueries

To ensure your queries return accurate results, use NOT EXISTS rather than NOT IN when dealing with subqueries that may include NULL values. The presence of NULL can result in the IN clause only returning true or null, but never false. This applies inversely to NOT IN, where only false or null returns are possible, potentially impacting the query’s logic. By opting for NOT EXISTS, you enable the database to execute an Anti Join, leading to more efficient relations processing and more predictable performance.

For positive checks:

SELECT 1 IN (1, 2);

For negative checks, instead of:




By adjusting your query formulation, you’ll leverage the structured nature of the database schema, boost sorting, and querying through efficient index utilization like CREATE INDEX, and minimise sequential scans and unnecessary aggregates.

Utilising EXPLAIN (ANALYZE, BUFFERS) for Query Diagnostic

Inaccurate Predictions

When your database returns unexpected query times, outdated table statistics could be the culprit. The planner’s misjudgment, such as expecting a handful rather than hundreds of rows, could lead to an inefficient choice of query plan, such as opting for a nested loop over a more appropriate hash or merge join. By refreshing these statistics using the ANALYZE command, or improving planner insight with CREATE STATISTICS for correlated data, you can rectify inaccurate estimates and enhance performance.

Insufficient Memory for Sorting

Feel your queries slowing down unexpectedly? An operation using disk space due to a lack of work_mem could be the reason. Memory is faster, so when sorting tasks overflow into disk space, response times suffer. Consider allocating more memory to work_mem to counteract this. Beware, though, an overemphasis on this parameter might be unnecessary if a well-designed index can obviate the need for sorting in the first place. To prevent extreme cases of large temporary file generation, adjust the temp_file_limit setting.

Partitioned Hash Operations

If you encounter hashing in numerous batches, it’s a sign that work_mem is not sufficient for the task at hand. Reducing the batch count by increasing this memory allocation can sharply reduce execution time. A side-by-side comparison of plans before and after adjusting work_mem will highlight the stark improvements in performance.

Fetches from the Table Heap

Whenever there is a mismatch between the Visibility Map and table heap concerning row visibility, more I/O operations occur than necessary, particularly with Index Scans. Index-Only Scans aim to avoid this but if a Visibility Map is outdated, Index-Only Scans will default back to standard practices, impacting performance. Running VACUUM ensures the Visibility Map reflects the latest state, potentially reducing unnecessary heap accesses.

Approximated Bitmap Scans

For scattered data, Postgres uses Bitmap Index Scans, efficiently fetching each required page only once. However, when work_mem can’t store the complete map, the scan mode switches to ‘lossy’, requiring a full perusal of all rows on the identified pages, instead of directly accessing the relevant rows. Your query’s efficiency depends greatly on the allocation of enough work_mem to maintain a precise bitmap.

Misguided Query Plans

Finally, keep a sharp lookout for cases where the database’s execution plan might not align with what you’d expect for an efficient query. This misalignment indicates that the database’s understanding of how best to execute the query is flawed, often because it lacks all the necessary information or parameters it needs to make an informed decision. Addressing this may involve tweaking various database settings or restructuring how the data is accessed and stored.


Partitioning your database can enhance its maintenance and bolster its performance through parallel processing.

Maintenance Advantages

  • Autovacuum Efficiency: When you have an extensively large table, the vacuuming process to remove outdated or deleted rows can become unwieldy. By segmenting your tables, each piece and associated index undergo vacuuming individually, easing and hastening the task for autovacuum workers.
  • Memory Management: If you’re unable to allocate sufficient autovacuum work memory, partitioning may reduce the required space for dead row lists during cleanup.
  • Conflict Prevention: As partitioned tables are smaller, the autovacuum process needs fewer interruptions. This comes in handy especially when it’s crucial for avoiding database wraparound issues.
  • Indexes: By splitting the table, you optimise index usage. Insertions and updates more readily fill spaces in the main table’s data pages, but the same isn’t true for indexes. Regular vacuuming is a necessity to keep index performance up – a smaller, partitioned index has better opportunities for reuse and does not need frequent reindexing.
  • Frozen Rows and Partition Detachment: To address the challenge of permanent historical data and avoid vacuuming, implementing partitioning can simplify things. Once data is no longer altered and is ‘frozen’, older partitions storing this data can be detached and removed, obviating the need for autovacuuming.

Parallelisation Benefits

  • Improved I/O: By partitioning your data (for example, by tenant in a multi-tenant system), you can allocate different workloads to separate tablespaces. This enhances I/O performance due to reduced contention.
  • Streamlined Workers: Concurrently running workers can make light work of smaller data partitions. Frequent and quicker maintenance tasks keep the system in optimum health.

It’s worth noting that it’s a misconception to presume partitioning enhances query performance merely by dividing a large table into smaller ones. This strategy could potentially lead to inferior performance. Instead, consider partitioning as a method to manage and scale your database effectively.

Optimising Database Management for OLTP Systems

To enhance your PostgreSQL performance, it’s essential to engage in regular monitoring and fine-tuning. This tailored approach is crucial to ensure that the system aligns with the unique demands of online transaction processing (OLTP).

  • Monitor diligently: Keep a close watch on system performance.
  • Adjust settings: Modify configurations to meet evolving workload requirements.
  • Uphold data integrity: Maintain reliable data storage and retrieval processes.

Adhering to these best practices will significantly contribute to a robust and efficient database environment.

Frequently Asked Questions

What Techniques Can Enhance PostgreSQL Database Efficiency?

There are several techniques to enhance the performance of a PostgreSQL database. These include adjusting the configuration parameters to suit your system’s specifics, utilising appropriate indexing strategies, and streamlining queries by rewriting or splitting complex ones. Additionally, hardware improvements should be considered, using tools for monitoring PostgreSQL performance, and regularly running the vacuum and analyze commands.

How Can One Optimise Queries Within PostgreSQL?

To optimise queries in PostgreSQL, you should:

  • Identify slow-running queries with EXPLAIN and EXPLAIN ANALYZE commands.
  • Make use of appropriate indexes.
  • Rewrite queries to remove unnecessary complexity.
  • Use subqueries and common table expressions wisely.
  • Reduce the number of rows to be processed as early as possible in the query.

Distinguishing Between Database Tuning and Optimisation

Database tuning generally refers to system-level adjustments to enhance performance, such as changing configuration settings or upgrading hardware to reduce bottlenecks. On the other hand, optimisation often deals with improving the efficiency of individual queries and ensuring that the database schema supports the workload.

The Role of Indexing in PostgreSQL Performance

Indexing can significantly improve query performance in PostgreSQL by allowing the database engine to locate data more quickly rather than scanning entire tables. Types of indexes and factors for their use:

  • B-Tree indexes, effective for equality and range queries.
  • GIN and GiST indexes, suitable for complex data types like JSON or full-text search.
  • Partial indexes, which include only a subset of records.

Considerations for Tuning PostgreSQL with Large Datasets

With large datasets, consider the following:

  • Hardware: Ensure adequate memory and SSDs for swifter I/O.
  • Maintenance: Regularly vacuum and analyze the database.
  • Partitioning: Splitting large tables into smaller, more manageable pieces.
  • Resource Management: Use work_mem and shared_buffers to manage memory allocation effectively.

Best Practices for SQLITE Joins

When using joins:

  • Selectivity: Choose the table with the smallest result set to lead the join.
  • Indexing: Create indexes on the columns used in join conditions.
  • EXPLAIN Analyze: Use this command to understand how your joins are being executed and to identify inefficiencies.
  • Join Types: Understand the differences between INNER, OUTER, LEFT JOIN, and others, to select the most appropriate for the query.

By adhering to these best practices, you can enhance the performance of your joins and, by extension, your PostgreSQL database operations.

Leave a Comment