Course topics

By WebNest Studio Engineering Team

PostgreSQL Tutorial

PostgreSQL Performance Basics: VACUUM, ANALYZE, and Connection Pooling

Beyond indexing and query plans, PostgreSQL has a few operational fundamentals that keep it running efficiently over time: routine maintenance via VACUUM and ANALYZE, and managing how client connections reach the server. Both matter more as an application grows, and both are frequently overlooked until performance quietly degrades.

Why it matters

PostgreSQL never overwrites a row in place: an update writes a new version and leaves the old one behind. Without clean-up, tables grow and slow down however good the queries are. Vacuuming, statistics and connection pooling are the three things that keep a busy PostgreSQL database healthy.

Why VACUUM Is Necessary

PostgreSQL uses a storage strategy called MVCC (Multi-Version Concurrency Control): an UPDATE or DELETE does not immediately erase the old row version, it marks it as "dead" so that other concurrent transactions reading it still see a consistent snapshot. Over time, dead row versions accumulate as table "bloat," wasting disk space and slowing scans. VACUUM reclaims that space for reuse (without necessarily shrinking the file on disk, unlike VACUUM FULL, which rewrites the whole table and requires a stronger lock).

ANALYZE and the Query Planner

ANALYZE collects statistics about the distribution of values in each column — how many distinct values, how common each is, roughly how the data is sorted — and stores them for the query planner to use when estimating plan costs. Outdated statistics (for example, after a large bulk load) are a common cause of the planner choosing a poor plan, since its row-count estimates become inaccurate. PostgreSQL runs both VACUUM and ANALYZE automatically in the background via autovacuum, but understanding what it does helps you diagnose when its defaults need tuning for a specific heavily-written table.

Connection Pooling with PgBouncer

Each PostgreSQL connection is a full operating-system process with meaningful memory overhead, so PostgreSQL handles at most a few hundred to low thousands of concurrent connections comfortably. Applications that open (and often leak) many short-lived connections — common with serverless functions or large web fleets — benefit from a connection pooler like PgBouncer sitting between the application and PostgreSQL. PgBouncer maintains a small pool of real server connections and multiplexes many client connections onto them, dramatically reducing the load a spiky application places on the database itself.

Example

Running VACUUM and ANALYZE, and checking a table's dead tuple count

SQL
VACUUM ANALYZE products;

SELECT relname, n_dead_tup, n_live_tup
FROM pg_stat_user_tables
WHERE relname = 'products';
Output
VACUUM
   relname  | n_dead_tup | n_live_tup
------------+------------+------------
 products   |          0 |          3

Common Mistakes

  • Disabling autovacuum on a busy table "to reduce overhead," causing dead tuple bloat and progressively slower sequential scans over time.
  • Running VACUUM FULL on a large, actively used table during peak hours — it takes an exclusive lock that blocks reads and writes for its duration.
  • Forgetting to run ANALYZE after a large bulk data load, leaving the planner with stale statistics and a higher chance of choosing a poor plan.
  • Letting every application instance open its own uncapped pool of direct PostgreSQL connections instead of routing through a pooler like PgBouncer, eventually exhausting max_connections.

Exercise

This exercise uses PostgreSQL maintenance commands, so it needs a PostgreSQL server. For the orders table, write the command that reclaims dead rows and refreshes the planner's statistics in one go. Then write a query against the statistics view that shows the table's number of live rows, its number of dead rows, and when autovacuum last ran on it.

Expected result: after the VACUUM, the dead row count for orders is at or near zero.

SQL · starter code
-- TODO: vacuum and analyze the orders table

-- TODO: live rows, dead rows and last autovacuum time for orders,
--       from pg_stat_user_tables
Show hints
  • The two operations combine as VACUUM (ANALYZE) table_name.
  • The columns are n_live_tup, n_dead_tup and last_autovacuum, filtered by relname.
Show solution
SQL · solution
VACUUM (ANALYZE) orders;

SELECT relname, n_live_tup, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'orders';

Quick quiz

1. Why does PostgreSQL need VACUUM?

  • A. To back up the data
  • B. Updates and deletes leave old row versions behind, and VACUUM makes their space reusable
  • C. To rebuild indexes every night
  • D. To encrypt tables
Show answer

B. This is a consequence of MVCC, the mechanism that lets readers and writers work without blocking each other.

2. What does ANALYZE (the maintenance command) do?

  • A. Runs a query and shows its plan
  • B. Deletes dead rows
  • C. Collects statistics about the data, which the planner uses to choose query plans
  • D. Checks for corruption
Show answer

C. It is a different thing from the ANALYZE option of EXPLAIN.

3. Why is a connection pooler such as PgBouncer used?

  • A. Each PostgreSQL connection is a separate server process, so many connections are costly; a pooler shares a small number among many clients
  • B. It makes queries run faster
  • C. It replaces indexes
  • D. It stores backups
Show answer

A. Applications with many short-lived connections benefit most.

Interview questions

What is MVCC and why does it make VACUUM necessary?

Show answer

Multi-version concurrency control lets readers and writers work without blocking each other by keeping several versions of a row. An update writes a new version and marks the old one as expired; a delete only marks the row. The old versions, called dead tuples, remain in the table until no transaction can still see them. VACUUM then marks their space as reusable. Without it, tables and indexes bloat and queries slow down.

What is the difference between VACUUM and VACUUM FULL?

Show answer

Plain VACUUM marks the space of dead rows as reusable within the table, runs alongside normal reads and writes, and does not usually shrink the file on disk. VACUUM FULL rewrites the whole table into a new, compact file and returns the space to the operating system, but takes an exclusive lock that blocks all access while it runs. Autovacuum performs the plain form automatically; VACUUM FULL is reserved for severe bloat.

Key Points to Remember

  • MVCC means UPDATE/DELETE leave dead row versions behind; VACUUM reclaims that space for reuse.
  • ANALYZE refreshes the statistics the query planner relies on to estimate costs and choose plans.
  • Autovacuum runs both automatically in the background, but heavily-written tables sometimes need tuned settings.
  • A connection pooler like PgBouncer reduces overhead from many short-lived or spiky client connections.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering