How to spot N+1 queries in code review
An N+1 query loads a list with one query and then runs one more query for every row in it. With ten rows in a test database, nobody notices. With ten thousand rows in production, the page makes 10,001 round trips to the database.
What it looks like in a diff
A loop over query results that touches a relation or calls the data layer inside the loop body, for example for playlist in playlists: playlist.owner.name, or one await per playlist to load its tracks.
ORMs hide it well, because the extra query is a property access, not a visible call. Read every attribute access inside a loop as a possible query, and look for the eager-loading call that should be there (select_related, prefetch_related, selectinload, an include or a fetch join, depending on the ORM).
Questions to ask in review
- How many rows can this loop see in production, not in the test data?
- Does anything inside the loop read a relation or call the database?
- Is the related data loaded up front, in one query or a batched
IN (...)? - Would a test that counts queries catch it if someone removes the eager load later?
How to word the comment
Name the input, the path it takes and what goes wrong, then the fix. For example (on an imagined PR, not one from the library):
This reads each playlist's tracks inside the loop, so a page of 200 playlists runs 201 queries. Load the tracks for all of them in one query (
INon the playlist ids) and group them in memory.
Practise on real PRs
As of October 2026, the DiffDojo library has 5 live PRs that plant N+1 queries, across 4 stacks. They are not listed here: a PR you open knowing its bug teaches less. Each review is graded on whether you find the bug and explain it.