Marcio Cunha

Slow database? How to optimize queries with EXPLAIN ANALYZE

Learn how to read execution plans in PostgreSQL to eliminate Sequential Scans, understand IOPS bottlenecks, and build efficient composite indexes.

Marcio Cunha•4 min
Also available in:PortuguêsEspañol
Summary
  • The EXPLAIN ANALYZE command actually executes the query on the database and reveals the real time spent on each step.
  • Sequential Scans force the database to read entire tables row by row, causing high disk usage and freezes.
  • Composite indexes require strict attention to column order to work properly with multiple filters.
  • Partial indexes drastically reduce disk space by indexing only specific slices of huge production tables.
  • Hash Joins combine large volumes of data using RAM, but degrade severely if they exceed the work_mem limit.

What is EXPLAIN ANALYZE and why it saves your application

When a web application starts freezing due to database sluggishness, many people's first instinct is to reboot the server or blame the infrastructure. In practice, this means we are trying to fix a water leak by replacing the living room floor instead of turning off the faucet. In PostgreSQL, the ultimate tool to find the exact culprit is the EXPLAIN ANALYZE command. It not only shows the route the database plans to take, but also actually runs the query and measures real time, processing costs, and the row count consumed by each step.

Before blindly applying any changes to the database, we must understand that the query planner (PostgreSQL's internal engine) makes decisions based on periodically collected statistics. If these statistics are outdated, the database can choose disastrous paths. To deepen your investigations when the problem is systemic rather than an isolated query, check out our guide on how to use EXPLAIN ANALYZE in PostgreSQL for quick diagnostics under high load.

Spotting bottlenecks: the danger of Sequential Scans

When reading an EXPLAIN ANALYZE output, the first monster we usually encounter is the dreaded Sequential Scan (or Seq Scan). In practice, a Seq Scan means the database had to open every single drawer and read every row in the table, from top to bottom, just to find a handful of records matching the search criteria. If your table has ten thousand rows, this is almost instantaneous. But if we are talking about ten million rows, the hard drive suffers, memory usage spikes, and the entire application chokes.

Another common villain in these reports is unregulated Hash Join, which happens when the database tries to cross two large tables using system RAM. If the data volume exceeds the limit configured in PostgreSQL's work_mem variable, the database is forced to use temporary disk files, creating a brutal input/output bottleneck. When database issues escalate to complete freezes and exhausted connections, knowing how to kill slow queries in SQL becomes an indispensable operational survival skill for any engineer.

Building composite indexes with intelligence and correct order

The classic solution to avoid heavy sequential reads is creating indexes, which work just like the back-of-the-book index in a textbook: instead of reading the whole book, you look up the keyword and jump straight to the right page. However, creating indexes without discretion consumes disk space and makes write operations (INSERT, UPDATE) slower. When we need to filter by multiple columns at once, composite indexes come into play, grouping multiple fields into a single B-Tree structure.

The big trap with composite indexes is column order. PostgreSQL can only take advantage of the index if the query uses the leading column of the definition. If we create an index on (customer_id, created_at), a search filtering only by created_at will completely ignore the index, forcing the database back into a costly Sequential Scan. Therefore, always place the most selective and frequently used WHERE clause column in the first position of the index declaration.

Saving space with partial indexes in production

In large production databases, tables with tens of millions of rows often contain columns with null values or inactive states that are never queried day-to-day. Keeping this useless data inside a standard index is a drastic waste of hardware resources. That is where partial indexes come in, a powerful PostgreSQL feature that lets you index only rows meeting a specific boolean condition, such as WHERE status = 'active'.

In practice, this means your index becomes incredibly small, fitting entirely into RAM (high Cache Hit Ratio), which drastically accelerates the application's most common queries. Furthermore, index maintenance operations become much faster because the database completely ignores old history or archived records that do not fall under the partial filtering rule defined by the developer. To explore these architectural concepts further in production, see our related technical analysis on How to test transformer thermal fuses with a multimeter.

Final thoughts on production performance

Optimizing queries in PostgreSQL is not a one-time event performed upon launch, but rather a continuous process of monitoring, execution plan analysis, and index tuning. Tools like EXPLAIN ANALYZE remove the blindfold and reveal exactly where processing time is being wasted. By replacing raw sequential scans with well-crafted composite indexes and lean partial indexes, we restore application agility and guarantee scalability without spending a fortune on extra infrastructure.