← Back to blog

Three PostgreSQL traps that quietly slow down full-stack apps

By 2 min read
  • #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.