Resolvers & the N+1 Problem
You'll learn to
- -Explain how GraphQL resolvers execute per-field, and why that naturally produces the N+1 query problem
- -Use a DataLoader (or equivalent batching layer) to collapse N+1 queries into a single batched fetch
Every field in a GraphQL schema has a resolver function behind it - a piece of server-side code responsible for producing that field's value. This per-field execution model is what makes GraphQL's flexible querying possible, and it is also the direct cause of GraphQL's most notorious performance problem.
How Resolvers Execute
query {
users(limit: 10) {
name
orders {
total
}
}
}The `users` resolver runs once, fetching 10 users. Then, naively implemented, the `orders` resolver runs once per user returned - 10 separate calls, each fetching that one user's orders individually. This is the N+1 problem: 1 query for the parent list, plus N queries (one per item) for each item's related data, where a hand-written REST endpoint would typically fetch both in a single join or a single batched query from the start.
def resolve_orders(user, info):
# called once PER USER in the result set - 10 users means 10 separate
# database round trips, one at a time, instead of one batched query
return db.query("SELECT * FROM orders WHERE user_id = ?", user.id)DataLoader: Batching Within a Single Request
A DataLoader collects individual load requests that happen within the same tick of the event loop (effectively, within the same GraphQL request) and batches them into a single call, instead of dispatching each one immediately. Rather than 10 separate "get orders for user X" calls, all 10 user IDs are collected first, then issued as one "get orders for users [1,2,3...10]" query.
# The DataLoader batches every load() call made during this request into
# one function call, keyed by all the collected IDs at once:
order_loader = DataLoader(batch_load_fn=lambda user_ids:
db.query("SELECT * FROM orders WHERE user_id IN ?", user_ids)
)
def resolve_orders(user, info):
return order_loader.load(user.id) # queued, not executed immediately
# 10 users -> 10 load() calls queued -> exactly ONE batched query executedThe key behavioral shift: `.load()` doesn't execute anything immediately - it queues the request and returns a promise/future that resolves once the batch actually runs. GraphQL's execution engine naturally creates the opportunity for this batching, since it already resolves sibling fields (like every user's `orders` field) before waiting on any of them individually.
DataLoader instances must be created fresh per request, not shared globally - a DataLoader also caches results within its lifetime, and a shared, long-lived instance would serve stale or, worse, cross-request-leaked cached data to the wrong caller.
Interview Signal is part of Pro
See a real weak answer next to a real strong one for this exact topic.
Quiz is part of Pro
Test what you just read with a short quiz, and bank the XP.
Design The Resolver Graph in the API Design Lab's GraphQL Mastery act.