An N+1 query pattern issues one query to fetch a list of rows and then one additional query per row to fetch each row’s related data, where a single query with an explicit join or batched load would return the same result.
In simpler words
Fetching 20 tickets and then separately fetching each ticket’s assignee, one at a time, means 21 database round trips instead of one.
Ground this in Ticket’s assignee relation, which TicketsService.list does not currently join — a natural place N+1 could creep in.
After this you can
Recognize an N+1 pattern in a code sample
Rewrite a per-row lookup as a single joined query
Explain why the fix scales better as list size grows
Where this pattern would appear here
Definition
TicketsService.list currently selects tickets without their assignee relation, so any code that later loops over the returned rows to fetch each assignee individually would reintroduce the query for every row instead of loading the relation once.
In simpler words
The risk is not the Ticket–User relation itself; it is loading it one row at a time after the fact.
Ticket has a ManyToOne assignee relation to User with a nullable assignee_id foreign key — well suited to a single joined fetch, risky as a per-row lookup.
A naive implementation might call a repository lookup per ticket inside a .map — 20 tickets means 20 extra queries for one list response.
20 tickets means 20 extra round trips, and the count grows with every additional row on the page.
Loading the relation deliberately
Definition
A deliberate loading strategy fetches related rows in the same query as their parent — through a join such as leftJoinAndSelect, or through TypeORM’s relations option — so the total query count stays fixed regardless of how many parent rows are returned.
In simpler words
One query, or one join, for the whole page — not one per row.
qb.leftJoinAndSelect("ticket.assignee", "assignee") adds the relation to the same SQL statement TicketsService.list already builds, with no extra round trip.
Select only the columns the response actually needs when a joined relation gets wide — toResponse() already narrows the ticket shape; do the same for a hydrated assignee.
Assignee data now arrives with the tickets in a single round trip, however many rows the page contains.
How to prove N+1 in practice
Definition
You diagnose N+1 by counting SQL statements for a single list request, not by reading TypeScript and hoping. Enable query logging or use a DB/proxy that shows statement counts.
In simpler words
If one GET /tickets produces 1 + pageSize queries, you found it.
Turn on TypeORM logging (or Postgres log_min_duration) in local dev while hitting the list endpoint once.
Compare: without join → many SELECTs on users; with leftJoinAndSelect → one (or two with count) statements.
DataLoader-style batching (conceptual): collect ids during a request, then fetch once — useful in GraphQL; in REST Nest services, prefer an explicit join or In(ids) batch.
Measurement checklist
1. Hit GET /tickets?limit=20 once
2. Count SELECT statements in logs
3. If count ≈ 1 + 20 on users → N+1
4. Add leftJoinAndSelect / relations / In(ids)
5. Re-hit; count should stay ~constant as limit grows
Intermediate habit: measure before and after, do not argue from intuition alone.
Keep in mind
Spot N+1 by counting queries in dev logs against one list response, not by guessing.
Prefer a join or a single relations fetch over a per-row lookup.
Re-check loading strategy whenever a list endpoint gains a new relation.
Prove the fix by re-counting queries after the change.
Test
Check your understanding
At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥ 80% (enforced by the API) to save progress.