← Stackzilla Blog
Debugging Slow PostgreSQL Queries Before Buying a Bigger Server
Published October 22, 2026
· 3 min read
· PostgreSQL, SQL, performance, backend
Learn to read a query plan, identify wasted work, and choose an index based on the query your application actually runs.
A database that feels slow is not necessarily too small. A request can spend most of its time waiting for a connection, retrieving unnecessary rows, or sorting data that a better query would never fetch. Increasing server capacity before identifying that work often makes the same mistake more expensive.
Start with one slow user action, not the entire database. Capture its SQL, representative parameters, duration, and returned row count. Remove sensitive values before sharing diagnostics. A search request for an unusually popular term may behave differently from the narrow example used during development.
## Read the plan as a record of work
PostgreSQL's `EXPLAIN` describes the planned execution. `EXPLAIN (ANALYZE, BUFFERS)` also executes the statement and reports observed timings and buffer activity. That distinction matters: do not casually analyze a destructive statement on live data. Use a safe environment and understand transaction effects first.
Compare estimated and actual row counts. Large differences can indicate stale statistics or data distributions the planner models poorly. Look for operations repeated many times, large sorts, and filters that discard most scanned rows. A sequential scan is not automatically wrong; it can be appropriate for a small table or a query returning much of its contents.
## Build an index for the access pattern
Suppose an activity feed filters by account and orders by creation time. A composite index beginning with the account identifier and then the ordering fields may help. An index on creation time alone answers a different question. Confirm the actual predicate and sort order before choosing column order.
Indexes are not free. They consume storage and add work to inserts, updates, and maintenance. Avoid adding one for every column. Consider whether an existing index already serves the query and whether pagination can reduce the amount of data requested.
Also check the application. Ten individually fast queries can produce a slow page when they run serially. An ORM can hide an N+1 query pattern, where loading a list triggers another query for every item.
## Measure the change under realistic conditions
Repeat the same request with comparable data after the change. Record latency, rows examined, and buffer activity rather than relying on one warm-cache timing. Test both common and large accounts. A plan that works for twenty records may degrade badly at two million.
Keep the successful query and its reasoning with the application. Performance tuning becomes easier when future developers understand which access pattern an index supports. The objective is less unnecessary work, not a more impressive infrastructure bill.
## Further reading
- [PostgreSQL: Using EXPLAIN](https://www.postgresql.org/docs/current/using-explain.html)
- [PostgreSQL: Multicolumn indexes](https://www.postgresql.org/docs/current/indexes-multicolumn.html)
Read the full article on Stackzilla →