Overview

6 Performance bad Practices

PostgreSQL can deliver excellent performance, but only when you avoid a handful of common anti-patterns. This chapter surveys the pitfalls that most often hurt throughput and stability: shipping to production with stock settings, mismanaging connections and transaction lifecycles, neglecting essential maintenance, misunderstanding query plans, and mishandling indexes. The unifying themes are that defaults are intentionally conservative, workload shapes every tuning decision, and seemingly harmless choices—like letting sessions idle or piling on connections—can cascade into slowdowns, stalls, or even crashes.

Configuration and concurrency mistakes top the list. Relying on default memory settings wastes hardware; overcorrecting by inflating shared buffers or work mem can be worse, as per-node memory is multiplied by query complexity and concurrent sessions, risking out-of-memory kills and downtime. High connection counts add OS overhead and lock contention; real throughput usually improves by limiting active backends, queuing via a pooler, and matching concurrency to CPU cores. Idle sessions and especially idle-in-transaction sessions amplify snapshot and lock costs, block DDL and autovacuum, and promote table bloat. Long-running queries hold snapshots, starve autovacuum, and can cause replication conflicts and lag. Sustained high transaction rates burn through XIDs, pushing the system toward anti-wraparound autovacuum; batching commits, tuning autovacuum more aggressively, and curating data ingest help keep pace. Above all, do not disable autovacuum—tune it; its cost is the price of MVCC health.

On the query side, skipping EXPLAIN (ANALYZE) hides the real causes of slowness and blinds you to planner choices and statistics gaps. Indexes are essential when predicates filter meaningfully: add what you need (for example, on referencing columns in foreign key joins), but avoid blanket or redundant indexing that bloats storage and drags down writes. Identify unused indexes thoughtfully—usage is instance-specific, so confirm a standby isn’t depending on them before dropping. Finally, avoid explicit heavyweight locks that serialize work and induce head-of-line blocking; prefer retry-friendly logic or SERIALIZABLE with retries to preserve correctness without pervasive blocking. The practical recipe is to test with production-like data, measure TPS and latency under realistic client counts, watch MVCC and lock indicators, and iterate configurations to match your workload rather than dogma.

TPS & latency test result plot with various numbers of clients
CH06 F01 Angelakos
PostgreSQL transaction timeline showing linear sequence of XIDs
CH06 F02 Angelakos
Depiction of PostgreSQL’s transaction IDs as a circular space
CH06 F03 Angelakos

Summary

  • PostgreSQL’s default configuration is very conservative and in most cases will not be optimal for a real-world workload. This can mean that you are leaving your system resources underutilized and leaving potential performance gains on the table.
  • You can shoot yourself in the foot by calculating memory allocation settings incorrectly. You need to take your workload into account, including what types of queries you are running and how memory-hungry they are along with what level of client concurrency you are expecting.
  • Excessive concurrency can kill your performance rather than allow more work to be performed in parallel because of the way Multi-Version Concurrency Control (MVCC) works. You need to be aware of the risks associated with opening too many sessions inside PostgreSQL, and also the nature of your workload and the limitations of your particular database host.
  • Connections that are mostly or entirely idle don’t come for free; they are associated with computational overhead that may affect PostgreSQL’s, and your operating system’s, performance in general. You should try to avoid having connections around that don’t do much actual work. If you can’t, you should use a connection pooler that’s aware of these connection semantics, such as PgBouncer in Transaction Mode.
  • Idle in transaction sessions and sessions with long-running queries can cause unexpected blocking of other queries, leading to application delays or errors. They can also postpone or altogether block autovacuum, and this can lead to performance degradation beacuse of bloat, or more serious errors.
  • If you let your transaction rate get out of control, it can outrun the efforts of autovacuum to prevent XID wraparound failure and bring down your database. You can mitigate this by batching, reorganizing, and summarizing data — or even skipping the ingestion of data you aren’t likely to use again.
  • Autovacuum is essential for the correct operation of your database. Lowering its effectiveness or disabling it altogether to save system resources is a fallacy, because any performance gains will be undone in time by bloat accumulation, inaccurate optimizer statistics, and forced anti-wraparound prevention.
  • Where there’s evidence that a query is running slowly, checking its EXPLAIN is a quick and accurate way to identify the reasons why and help troubleshoot it.
  • Locking objects explicitly can lead to read/write blocking that can make your application feel sluggish or broken. Where synchronization is needed, try to use the SERIALIZABLE isolation level and make your application able to retry actions.
  • Indexes can make or break your queries' performance, and PostgreSQL has lots of index types. Examine your query plans and take advantage of indexing to boost performance for your WHEREs. Sequential scans are bad because they become linearly slower as the table grows.
  • Having indexes that you don’t need can slow down your table for writing and consume valuable disk space. Identify those that aren’t in use from pg_stat_all_indexes and remove them.
  • Be careful when dropping indexes that appear unused as this statistic is for the local node only, and those indexes may be in use on another node you are physically replicating to.

FAQ

Why shouldn’t I run PostgreSQL in production with the default configuration?The defaults are intentionally conservative so PostgreSQL can “just work” on almost any machine. They’re rarely appropriate for real workloads. Basic adjustments (for example, increasing shared_buffers, work_mem, maintenance_work_mem) can yield large performance gains. Always stage, measure, and tune for your workload rather than trusting out‑of‑the‑box settings.
How should I size shared_buffers and work_mem without hurting performance?Avoid “bigger is always better.” work_mem is per plan node per query, so large values multiply quickly with concurrency and can trigger the OS OOM killer. shared_buffers benefits depend on workload (OLTP often benefits more than OLAP). Start modestly, measure spills and memory use, and adjust. Also mind units: settings use different defaults (e.g., shared_buffers in 8KB buffers if no unit; others in kB/MB).
What happens if I allow thousands of concurrent connections to the database?Excessive concurrency increases latency, causes LWLock contention, adds OS overhead (process creation, scheduling, context switching), and can reduce throughput. A rough rule is to keep active connections to within a few times your CPU core count. Use a connection pooler (e.g., PgBouncer) to accept many app connections while limiting true DB concurrency.
Why are many idle connections harmful if they aren’t doing anything?Every new snapshot must consider all sessions to determine visibility, so the cost of taking a snapshot grows with total connections—even idle ones. Thousands of backends also add OS scheduling and IPC overhead, and idle pools can surge active all at once. Prefer pooling (transaction pooling works well) and set appropriate timeouts if needed.
What’s wrong with long-running or “idle in transaction” sessions?They hold snapshots and locks, blocking operations (even simple reads can block DDL that needs ACCESS EXCLUSIVE). They also delay autovacuum from removing dead tuples or freezing rows, causing bloat and risking XID wraparound. Design apps to commit promptly, use idle_in_transaction_session_timeout, and cancel truly long-running queries safely (pg_cancel_backend over terminate when possible).
What is XID wraparound and how do I avoid it under high transaction rates?XIDs are 32-bit and “wrap” after ~4.3B. Very old tuples can become indistinguishable from “future” ones unless vacuum “freezes” them. Heavy write rates can outpace autovacuum and force anti-wraparound vacuums that slow the system. Mitigate by batching commits, tuning autovacuum more aggressively, and avoiding storing unnecessary high-volume data.
Should I disable autovacuum/autoanalyze to boost performance?No. Any short-term gains are outweighed by bloat, worse query plans, and risk of anti-wraparound vacuums. VACUUM FULL to reclaim disk is disruptive and locks tables. Keep autovacuum on and tune it for your write rate (you can override per table). If the server can’t handle both workload and maintenance, scale resources or reduce workload.
Why and how should I use EXPLAIN (ANALYZE) when tuning queries?EXPLAIN shows the chosen plan; EXPLAIN ANALYZE adds actual timings to reveal bottlenecks (e.g., seq scans, rechecks, join choices). It helps verify whether indexes are used and if plans match data characteristics. Caution: EXPLAIN ANALYZE runs the statement—wrap DML in a transaction and ROLLBACK if needed. auto_explain can log slow plans but adds overhead.
Why is explicit locking risky, and what are safer alternatives?Heavyweight locks can block reads/writes and create head-of-line blocking under load. Prefer letting PostgreSQL manage concurrency: use SERIALIZABLE (SSI) to detect anomalies and retry on serialization failures, or coordinate in the application with advisory locks. Design for retry rather than indefinite waiting.
Do foreign keys create indexes automatically, and how should I manage unused indexes safely?PostgreSQL indexes the referenced primary key but does not auto-index the referencing column. Create indexes on join/filter columns that matter. Avoid over-indexing—extra indexes slow writes and consume space. Find candidates to drop via pg_stat_all_indexes (e.g., idx_scan = 0), but always check every instance, including standbys: stats are per instance, and dropping on primary removes the index from replicas too.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • PostgreSQL Mistakes and How to Avoid Them ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • PostgreSQL Mistakes and How to Avoid Them ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • PostgreSQL Mistakes and How to Avoid Them ebook for free