Three PostgreSQL traps that quietly slow down full-stack apps
- #backend
- #postgresql
- #performance
Most performance problems I run into aren’t exotic. They’re the same handful of mistakes, hiding behind an ORM that makes them easy to write. Here are three I keep catching in full-stack apps.
1. The N+1 query
You fetch a list of orders, then loop over them to load each order’s customer. One query becomes one-plus-N. On a page with 50 rows that’s 51 round trips to the database.
// bad: one query per order
const orders = await repo.findOrders();
for (const o of orders) {
o.customer = await repo.findCustomer(o.customerId); // N more queries
}
// good: one join, or one batched lookup
const orders = await repo.findOrdersWithCustomers();
The cure is a join or a single batched WHERE id = ANY($1). Most ORMs have an
“include/eager-load” option — the trick is noticing the N+1 in the first
place, which is why I keep query logging on in development.
2. Indexes on the wrong columns
A primary key is indexed automatically. The columns in your WHERE and
ORDER BY clauses are not. If you filter orders by status and sort by
created_at, those are the columns that need an index — and often a composite
one in that order.
EXPLAIN ANALYZE is the only honest answer here. A Seq Scan on a big table
in a hot query path is the smell to look for.
3. SELECT count(*) on everything
Paginating with a full count on a large, filtered table forces Postgres to walk
every matching row just to tell you “12,418 results”. Half the time the user
only needs to know there is a next page. Keyset pagination (WHERE id < $last ORDER BY id DESC LIMIT 20) sidesteps both the count and the deep-OFFSET
slowdown.
None of these need a new database or a cache layer. They just need you to look at the queries your app actually runs — which, with an ORM, is the easiest thing in the world to stop doing.