Query Performance Patterns
Sequential database queries are a silent performance killer. A page that loads three queries sequentially at 400ms each takes 1200ms total. Run them in parallel and it drops to 400ms. Optimize with database joins and it's 150ms. That's an 8x speedup with minimal code changes.
Outcome
Database query patterns refactored from sequential to parallel or optimized joins, with measurable latency improvements.
Fast Track
- Identify sequential database queries causing waterfalls
- Refactor independent queries to use
Promise.all() - Optimize related data fetching with ORM includes/joins
The N+1 Query Problem (Making N Extra Queries When 1 Would Suffice)
Sequential Queries (Slow)
// ❌ SLOW: Sequential queries create a waterfall
async function getPostData(slug: string) {
const post = await db.post.findUnique({ where: { slug } })
const author = await db.user.findUnique({ where: { id: post.authorId } })
const comments = await db.comments.findMany({
where: { postId: post.id }
})
return { post, author, comments }
}Performance: Each query waits for the previous one to complete.
- Query 1: 400ms (post)
- Query 2: 400ms (author) - starts after post finishes
- Query 3: 400ms (comments) - starts after post finishes
- Total: 1200ms
Parallel Queries (Better)
// ✅ BETTER: Parallel queries with Promise.all
async function getPostData(slug: string) {
// First get the post to get IDs for related queries
const post = await db.post.findUnique({ where: { slug } })
// Then fetch related data in parallel
const [author, comments] = await Promise.all([
db.user.findUnique({ where: { id: post.authorId } }),
db.comments.findMany({ where: { postId: post.id } })
])
return { post, author, comments }
}Performance: Related queries run simultaneously.
- Query 1: 400ms (post) - must run first
- Query 2: 400ms (author) - runs in parallel with comments
- Query 3: 400ms (comments) - runs in parallel with author
- Total: 800ms (1.5x faster)
Optimized with Database Joins (Best)
// ✅ BEST: Single optimized query with relationships
async function getPostData(slug: string) {
const postData = await db.post.findUnique({
where: { slug },
include: {
author: {
select: { id: true, name: true, avatar: true }
},
comments: {
orderBy: { createdAt: 'desc' },
take: 20,
include: {
author: { select: { name: true, avatar: true } }
}
},
_count: {
select: { comments: true, likes: true }
}
}
})
return postData
}Performance: Database handles joins efficiently with indexes (database structures that speed up lookups).
- Single query: 150ms
- Total: 150ms (8x faster than sequential)
Performance Comparison
| Strategy | Time | Speedup | When to Use |
|---|---|---|---|
| Sequential | 1200ms | 1x | ❌ Never use |
Parallel (Promise.all) | 400ms | 3x | ✅ Independent queries |
| Optimized (includes/joins) | 150ms | 8x | ✅ Related data |
When to Use Each Pattern
Use Promise.all() for independent queries:
// These queries don't depend on each other
const [user, settings, notifications] = await Promise.all([
db.user.findUnique({ where: { id } }),
db.settings.findUnique({ where: { userId: id } }),
db.notifications.findMany({ where: { userId: id, unread: true } })
])Use ORM includes for related data:
// Single query with relationships
const user = await db.user.findUnique({
where: { id },
include: {
posts: true,
comments: true,
followers: true
}
})Never use sequential for independent queries:
// ❌ BAD: No reason to wait between these
const user = await db.user.findUnique({ where: { id } })
const posts = await db.posts.findMany({ where: { authorId: id } })Hands-On Exercise 4.2
Create a posts page that demonstrates parallel data fetching with Promise.all().
Target file: apps/web/src/app/posts/[slug]/page.tsx
Requirements:
- Create a post detail page that fetches post, author, and comments
- Use
Promise.all()for independent queries - Add timing logs to measure before/after performance
- Verify the speedup in server logs
- Document the performance improvement
Implementation hints:
- Look for multiple
awaitstatements in sequence - Check if queries depend on previous results (if not, parallelize)
- Use ORM's
includeorselectto reduce query count - Add
console.time()andconsole.timeEnd()to measure:console.time('getPostData') const data = await getPostData(slug) console.timeEnd('getPostData') - Test with realistic data volumes (100+ records)
Example timing measurement:
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const start = Date.now()
const postData = await getPostData(slug)
const duration = Date.now() - start
console.log(`[post] Loaded post data in ${duration}ms`)
return <Post data={postData} />
}Try It
-
Before optimization:
# Load the post page and check logs curl http://localhost:3000/posts/my-first-postExpected log:
[post] Loaded post data in 1200ms -
After parallel optimization:
# Same request, parallel queries curl http://localhost:3000/posts/my-first-postExpected log:
[post] Loaded post data in 800ms(1.5x faster) -
After join optimization:
# Same request, single query with includes curl http://localhost:3000/posts/my-first-postExpected log:
[post] Loaded post data in 150ms(8x faster)
Commit & Deploy
git add -A
git commit -m "perf(queries): eliminate N+1 problem with parallel and optimized queries"
git push -u origin feat/polish-query-concurrencyDone-When
- Post page uses
Promise.all()for independent queries - Related data fetches (author, comments) run in parallel
- Server logs show timing (e.g.,
[post] Loaded in 200ms) - Load time is reduced from sequential baseline
- At least 1.5x speedup verified in logs
Solution
Complete optimized post page
Here's a post detail page demonstrating Promise.all() for parallel data fetching:
import { Suspense } from "react";
// Mock data fetching functions (simulate API calls)
async function fetchPost(slug: string) {
await new Promise((resolve) => setTimeout(resolve, 200));
return {
id: "post-1",
slug,
title: "Understanding Parallel Data Fetching",
content: "This post demonstrates how to optimize data loading...",
authorId: "user-1",
createdAt: new Date("2024-01-15"),
};
}
async function fetchAuthor(authorId: string) {
await new Promise((resolve) => setTimeout(resolve, 200));
return {
id: authorId,
name: "Demo Author",
avatar: "/avatars/demo.jpg",
};
}
async function fetchComments(postId: string) {
await new Promise((resolve) => setTimeout(resolve, 200));
return [
{ id: "c1", author: "Reader 1", text: "Great article!" },
{ id: "c2", author: "Reader 2", text: "Very helpful, thanks!" },
];
}
// ✅ OPTIMIZED: Parallel fetching with Promise.all
async function PostContent({ slug }: { slug: string }) {
const start = Date.now();
// First fetch the post (needed for authorId and postId)
const post = await fetchPost(slug);
// Then fetch related data in parallel
const [author, comments] = await Promise.all([
fetchAuthor(post.authorId),
fetchComments(post.id),
]);
const duration = Date.now() - start;
console.log(`[post] Loaded in ${duration}ms (parallel)`);
// Expected: ~400ms instead of ~600ms sequential
return (
<article className="space-y-6">
<header>
<h1 className="font-bold text-3xl">{post.title}</h1>
<div className="flex items-center gap-2 mt-2 text-gray-600">
<span>By {author.name}</span>
<span>•</span>
<time>{post.createdAt.toLocaleDateString()}</time>
</div>
</header>
<div className="prose">
<p>{post.content}</p>
</div>
<section>
<h2 className="mb-4 font-semibold text-xl">
Comments ({comments.length})
</h2>
<ul className="space-y-3">
{comments.map((comment) => (
<li key={comment.id} className="p-3 bg-gray-50 rounded">
<strong>{comment.author}</strong>
<p>{comment.text}</p>
</li>
))}
</ul>
</section>
</article>
);
}
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<main className="mx-auto max-w-2xl p-8">
<Suspense
fallback={<div className="animate-pulse">Loading post...</div>}
>
<PostContent slug={slug} />
</Suspense>
</main>
);
}Key optimization:
- Post must be fetched first (we need
authorIdandpostId) - Author and comments are fetched in parallel with
Promise.all() - Total time: ~400ms instead of ~600ms sequential (1.5x faster)
For maximum optimization, use database includes:
// Single query with all related data
const postData = await db.post.findUnique({
where: { slug },
include: {
author: { select: { name: true, avatar: true } },
comments: { include: { author: true } }
}
})
// Total: ~150ms (4x faster than sequential)Verify the improvement:
# Check server logs after loading /posts/my-first-post
# Before: [post] Loaded in 600ms (sequential)
# After: [post] Loaded in 400ms (parallel)
# Best: [post] Loaded in 150ms (with includes)Query Optimization Checklist:
- Identify waterfalls: Look for multiple sequential
awaitstatements - Parallelize independent queries: Use
Promise.all()when queries don't depend on each other - Use database relationships: Leverage ORM includes/joins to fetch related data in one query
- Measure impact: Add timing logs to prove the improvement
- Document: Note before/after times in comments or commit message
Key principle: Database round-trips are expensive. Minimize them by running queries in parallel or combining them with joins.
Performance gains:
- Sequential → Parallel: 3x faster
- Sequential → Optimized joins: 8x faster
- Zero code complexity increase
Cache Invalidation APIs
Next.js provides refined cache invalidation APIs for granular control over cache behavior: updateTag() for immediate cache updates in Server Actions and revalidateTag() for stale-while-revalidate semantics (serve cached data immediately while fetching fresh data in the background) in both Server Actions and Route Handlers.
updateTag() - Immediate Cache Updates (Server Actions Only)
Use updateTag() in Server Actions when you need immediate cache invalidation for read-your-own-writes scenarios:
'use server'
import { updateTag } from 'next/cache'
export async function updateUserAction(userId: string, data: any) {
await db.user.update({ where: { id: userId }, data })
// Immediate cache invalidation - next request waits for fresh data
updateTag(`user-${userId}`)
return { success: true }
}Benefits:
- User sees their changes immediately (no stale data)
- Surgical cache updates without invalidating unrelated data
- Better cache hit rates compared to broad invalidation
revalidateTag() - Stale-While-Revalidate
Use revalidateTag() with 'max' profile for stale-while-revalidate semantics. Works in both Server Actions and Route Handlers:
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const { tag } = await request.json()
// Webhook updates CMS content
await processCMSUpdate()
// Mark cached data as stale - serves stale while fetching fresh in background
revalidateTag(tag, 'max')
return Response.json({ revalidated: true })
}In Server Actions:
'use server'
import { revalidateTag } from 'next/cache'
export async function updateUserAction(userId: string, data: any) {
await db.user.update({ where: { id: userId }, data })
// General user list cache - stale-while-revalidate
revalidateTag('users', 'max')
// This specific user's data - stale-while-revalidate
revalidateTag(`user-${userId}`, 'max')
return { success: true }
}When to use which:
- updateTag(): User creates/updates content and needs to see it immediately (Server Actions only)
- revalidateTag(): Background cache updates where slight delay is acceptable (Server Actions + Route Handlers)
Example: User profile update with correct API choice:
'use server'
import { updateTag, revalidateTag } from 'next/cache'
export async function updateProfileAction(userId: string, updates: any) {
await db.user.update({ where: { id: userId }, data: updates })
// User's own data - immediate consistency (read-your-own-writes)
updateTag(`user-${userId}`)
// Related data - eventual consistency is fine
revalidateTag('user-list', 'max')
revalidateTag(`user-${userId}-posts`, 'max')
return { success: true }
}References
- https://nextjs.org/docs/app/getting-started/fetching-data
- https://nextjs.org/docs/app/getting-started/caching-and-revalidating
- https://nextjs.org/docs/app/api-reference/functions/revalidateTag
- https://nextjs.org/docs/app/api-reference/functions/updateTag
- https://nextjs.org/docs/app/api-reference/functions/cacheLife
Was this helpful?