Marcio Cunha

Slow database? How to optimize queries with EXPLAIN ANALYZE

Learn how to investigate database execution plans in PostgreSQL using EXPLAIN ANALYZE to eliminate slow queries and speed up your system APIs.

Marcio Cunha•4 min
Also available in:PortuguêsEspañol
Summary
  • The EXPLAIN ANALYZE command actually runs the query in the database, revealing the real cost and time spent at each step.
  • Identifying scan types, such as a full Seq Scan on a table, is the first step to eliminating performance bottlenecks.
  • Creating targeted indexes solves most API slowness, but having too many of them hurts write operations.
  • Helping the query planner estimate rows correctly prevents the database from choosing inefficient paths by mistake.
  • Monitoring memory usage and shared buffers prevents the database from wasting precious time reading data from the hard drive.

Why does your API freeze when the table grows?

As a system scales, it is common to notice that certain screens and API routes take seconds to respond. Most of the time, the culprit is a poorly constructed query sent to the relational database. To understand the root cause of this slowdown, we need to look under the hood and discover how PostgreSQL decides to fetch the requested information.

The database has an internal planner, a component that calculates the fastest route to find the data you asked for. However, this planner sometimes makes poor decisions, choosing long and costly paths. Before applying any blind improvements, such as adding indexes to every single column, we need to gather precise data on how the query behaves at runtime.

What is EXPLAIN ANALYZE and how to use it in practice?

The EXPLAIN command is the native PostgreSQL tool that shows the planned execution plan for a query. When we add the word ANALYZE next to it, the database goes beyond theory: it actually runs the query in the environment and measures the real time spent in milliseconds, alongside the exact number of rows processed in each operation.

To use the feature, simply open your favorite database client, like DBeaver or the psql terminal, and add the command before the problematic SQL statement. In practice, this means writing something like

EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'pending';
to see the complete x-ray of the operation.

Reading the execution plan without fearing complex terms

The command output displays a hierarchical tree full of technical terms that can look scary at first glance. Each line represents a node in the execution tree, showing operations like Seq Scan, Index Scan, or Nested Loop. A Seq Scan, for instance, means the database is reading the entire table row by row from start to finish, which is terrible for tables with millions of records.

Beyond scan types, the report details estimated costs, real time costs, the number of blocks read into memory, and the quantity of returned rows. Comparing the estimated row count with the actual returned count is an excellent indicator: if the gap is massive, the planner is working in the dark and needs updated statistics.

How to prevent the database from reading the entire hard drive

Reading data stored directly on the physical disk is the slowest operation a database can perform. To speed up the flow, PostgreSQL tries to keep the most accessed data pages in RAM through shared buffers. If a query forces excessive disk reads, the I/O subsystem saturates and the entire API suffers dramatic performance drops.

We can mitigate this problem by applying advanced indexing concepts and tuning the database, techniques similar to what we use when managing memory usage in distributed monitoring systems. Ensuring that data fits into the memory cache prevents catastrophic bottlenecks during peak traffic hours.

Step-by-step guide to investigate and fix a slow query

When an API route exhibits recurring bottlenecks in production, following a structured investigation method ensures you solve the root cause rather than just masking the symptom. Execute the steps below in a staging environment to diagnose the issue: To complement this implementation in production environments, see our related technical analyses on How to run local language models with low VRAM and Why Next.js 14 re-renders so much and how to fix it.

  1. Open your database terminal or management interface connected to a staging database with a workload similar to production.
  2. Run the command
    EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM customers WHERE email = '[email protected]';
    to capture the detailed plan in a structured format.
  3. Analyze whether the main operation uses a proper index or if it is performing a full sequential scan across the entire table.
  4. Create the necessary index using the command
    CREATE INDEX CONCURRENTLY idx_customers_email ON customers(email);
    to prevent locking new writes on the table.
  5. Run EXPLAIN ANALYZE again to compare execution time and ensure that the number of disk blocks read has dropped dramatically.

Conclusion and next steps in optimization

Investigating execution plans with EXPLAIN ANALYZE turns database optimization from a shot in the dark into an exact science. By understanding how PostgreSQL reads tables, manages indexes, and consumes memory, you can design much faster and more resilient APIs capable of handling massive data volumes.

Maintaining the habit of inspecting critical routes before pushing them to production ensures your application scales smoothly, preserving the end-user experience and the financial health of your server infrastructure.