What Happens When You Type a URL and Press Enter
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →Your page loads 20 posts and quietly runs 21 database queries. The N+1 problem is the most common ORM performance bug. Here is how to recognise it and fix it in any stack.
Here's a performance bug that hides perfectly in development and shows up the moment real data arrives. Your page lists 20 blog posts with their authors. Locally, with five rows in the database, it's instant. In production, with thousands of rows and a database a few milliseconds away, it crawls.
The cause, very often, is the N+1 query problem.
You run 1 query to get a list of things, then N more queries, one for each item, to fetch related data.
const posts = await Post.findAll({ limit: 20 }) // 1 query
for (const post of posts) {
post.author = await User.findByPk(post.authorId) // 20 more queries
}
That's 21 queries where 1 or 2 would do. Each query has network and parsing overhead, so 21 small queries are much slower than one slightly bigger one. Add comments, tags and likes and you can easily hit hundreds of queries for a single page.
ORMs make this easy to do by accident, because lazy-loaded relationships look like ordinary property access:
for post in Post.objects.all()[:20]:
print(post.author.name) # a query hides behind this dot
Tell the ORM upfront which relationships you need, and it fetches them in one or two queries.
// Sequelize
Post.findAll({ include: [User], limit: 20 })
// Prisma
prisma.post.findMany({ take: 20, include: { author: true } })
// Laravel
Post::with('author')->take(20)->get();
# Django
Post.objects.select_related('author')[:20] # foreign key, via JOIN
Post.objects.prefetch_related('tags')[:20] # many-to-many, via second query
# Rails
Post.includes(:author).limit(20)
Same idea everywhere, different spelling.
Without an ORM, or with complex logic, collect the IDs and fetch them in one go:
const posts = await db.query('SELECT * FROM posts ORDER BY id DESC LIMIT 20')
const ids = [...new Set(posts.map(p => p.author_id))]
const authors = await db.query('SELECT * FROM users WHERE id = ANY($1)', [ids])
const byId = new Map(authors.map(a => [a.id, a]))
posts.forEach(p => { p.author = byId.get(p.author_id) })
Two queries, no matter how many posts.
Sometimes the cleanest fix is plain SQL:
SELECT p.id, p.title, u.name AS author_name
FROM posts p
JOIN users u ON u.id = p.author_id
ORDER BY p.id DESC
LIMIT 20;
Great when you need a few columns from the related table. For one-to-many relations, joins can multiply rows, so a second query (like Fix 2) is often cleaner.
GraphQL resolvers run per field, which makes N+1 almost the default. The DataLoader pattern collects every ID requested during one tick and loads them in a single batch. Most GraphQL server ecosystems ship an implementation. Use it from the start.
Eager loading everything "just in case" creates the opposite problem: huge queries pulling data nobody displays. Load what the page actually uses. And make sure the foreign key columns you join on are indexed, otherwise even the fixed query is slow. In Postgres, foreign keys are not indexed automatically (why indexing matters).
Model::preventLazyLoading() is a good example.If you're chasing general slowness rather than this specific bug, start with why is my website slow?
No. It happens any time code fetches related data inside a loop: raw SQL, REST calls to another service, even frontend code calling an API once per list item. ORMs just make it easier to miss.
Not always. It's better when you'll definitely use the related data. If you only need it occasionally, lazy loading the few cases is fine. The goal is a predictable, small number of queries per request.
There's no magic number, but a list page should usually need a handful of queries, and that number shouldn't grow with the number of items displayed.
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →There are dozens of HTTP status codes, but about fifteen cover nearly everything. What each one means, when to return it, and the mix-ups (401 vs 403, 301 vs 302) that confuse everyone.
Read →A, AAAA, CNAME, MX, TXT and NS records in plain English, why DNS changes seem slow, and the troubleshooting commands that tell you what is really going on.
Read →