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
| node | est | actual | loops | pages | share |
|---|---|---|---|---|---|
| Index Only Scan using events_pkey on eIndex Cond: (id < 40) | 1 | 3939x over | 1 | 3 |
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.