ML
Database

The N+1 Query Problem: Spot It, Kill It, Prevent It

The single most common performance bug in ORM-backed apps. Here's how it sneaks in and the three reliable ways to stop it.

June 25, 20257 min readDatabasePerformance

N+1 is the bug where you run 1 query to load a list of N records, then run N more queries to load a related field — one per record. It's almost never intentional. Every ORM ships with the footgun in the default settings.

The anatomy

// TypeORM / Sequelize / Hibernate — relation is "lazy" by default:
const users = await userRepo.find();     // 1 query
for (const u of users) {
  console.log(u.posts.length);           // lazy load → 1 query per user
}
// Total: 1 + N queries. A list of 200 users = 201 round trips.

The giveaway in logs: a burst of near-identical queries with only the WHERE-clause ID changing.

Fix #1: eager loading with JOIN

// Prisma
const users = await prisma.user.findMany({ include: { posts: true } });
// TypeORM
const users = await userRepo.find({ relations: ["posts"] });

One query with a JOIN. Good when the relation is 1:1 or small 1:N.

Fix #2: the IN-query (dataloader pattern)

For larger 1:N, a JOIN explodes the row count (cartesian on parent fields). Better: two round trips.

const users = await userRepo.find();
const ids   = users.map(u => u.id);
const posts = await postRepo.find({ where: { userId: In(ids) } });
// in-memory group
const byUser = groupBy(posts, p => p.userId);

This is exactly what Facebook's DataLoader does — it batches calls within a request tick and deduplicates IDs.

Fix #3: project what you need

If you only need user.name and COUNT(posts), don't load whole entities. Write the aggregate in SQL. ORMs are not required to mediate every query.

How to prevent it

  • Instrument every request. Log query count per request. Any endpoint whose query count scales with input size is a red flag.
  • Make lazy loading opt-in. In Hibernate: fetch = FetchType.LAZY is default — set your DTOs to require explicit fetch planning.
  • Use a linter. Tools like eslint-plugin-prisma and n-plus-one detectors catch the common shapes.

The 2-second endpoint with 847 queries behind it is practically always this one bug.

SharePostLinkedIn

Reader Discussion

8 replies// weighed in

TopNewestAuthor
Add to the thread
Disagree, agree harder, or share your own experience…
Email instead →markdown okbe kind
  1. Highlighted by author
    Victor Petrov· Senior BackendAgrees

    "removing indexes is the third-year move" — saving this. first year you add, second year you tune, third year you realise half of them have been dead weight slowing every write since that one feature got cut.

    Jun 27, 2025·2 days later
  2. Linh Phạm· Java DeveloperAgrees

    @Version optimistic locking is the 80/20 of CRUD apps. genuinely think JPA should make it default-on with a @NoVersion opt-out. the number of last-write-wins races I've debugged in spring boot apps that didn't set it is too damn high

    Jun 30, 2025·5 days later
  3. Mateus Silva· Backend DevAsks

    Q — SELECT FOR UPDATE SKIP LOCKED as a job queue: still a good fit in 2026 or do you reach for a real broker (sqs, rabbit, etc) past a certain QPS? we run ~400 jobs/sec on PG and it's been chill but I'm nervous

    Jul 01, 2025·6 days later
    • Aya Fujimoto· Database Engineer

      We do 11k jobs/sec on PG with SKIP LOCKED + LISTEN/NOTIFY. The thing that breaks first is your job table bloat — set up partman + auto-vacuum tuning before you scale.

      Jul 02, 2025
    • ML
      Minh LeAuthor

      Plus one. The cliff isn't the QPS — it's the visibility-of-dead-rows pattern. SKIP LOCKED holds up if you're disciplined about cleanup.

      Jul 03, 2025
  4. Greta Schäfer· Senior EngineerAgrees

    isolation levels are the part juniors skip and seniors keep underestimating. read committed feels safe; it's not — phantom reads still exist. the moment money is involved, default to repeatable read and benchmark.

    Jul 02, 2025·1 week later
  5. Aya Fujimoto· Database EngineerPushback

    small note on Postgres covering indexes — INCLUDE columns don't get the same treatment as key columns for HOT updates and dedup. people assume index-only scan == free, but the heap visibility map can still send you back to disk. measure first.

    Jun 29, 2025·4 days later
  6. Rachel Gold· Staff SREAgrees

    the on-call framing throughout this piece is what makes it land. too many infra articles assume you never get paged. those are written by people who never got paged.

    Jun 28, 2025·3 days later

Worked on something similar? Email ducminhldm@gmail.com — I read every one. The good ones become future posts.

Comments seeded · live discussion via email