Skip to content
Production

The N+1, from the database side

One query for the list and one for each row. From the application it looks like a loop; from the database it looks like a thousand identical statements, and the plan shows you the loops.

12 min · runs against the events dataset

An ORM fetches a list, and then some code touches a relation on each item. Each touch is a query. From inside the application it is a loop over objects; from inside the database it is one statement repeated a few hundred times, each one fast, and together far slower than a single join.

You can see it in a plan without ever seeing the application. A correlated subquery is an N+1 written in one statement, and the loops column is how many times it happened.

One statement. Look at the loops on the inner scan.

plan

nodeestactualloopspagesshare
Index Only Scan using events_pkey on eIndex Cond: (id < 40)13939x over13

The fix is almost always to ask for everything at once — a join, or an aggregate with a GROUP BY, or in an ORM the thing it calls eager loading. The plan changes from a loop to a single pass over each side, and the cost changes by a factor of however many rows there were.

Did it land?

  • Ask once, not once per row

    For every actor with events, return the actor_id and how many events they have, busiest first then by actor_id. Call the count events. One pass, not one query per actor.