Diagnosing Unexpected N+1 Queries in Prisma Client Relation Loading
Learn how to spot, verify, and fix extra SELECT statements that occur when Prisma Client relations are not explicitly loaded with include or select.
09 May 2026, 20:01 UTC

Recognizable Condition
The application shows a higher-than‑expected number of database queries after a single findMany or findUnique call. For example, loading a list of users and later accessing each user’s posts results in one query for the users plus an additional SELECT for every user’s posts.
Likely Cause
Prisma Client does not perform lazy loading of relations. If the original query omits an explicit include or select for a relation, accessing that relation later triggers separate queries—one per parent record—creating an N+1 pattern.
Diagnostic Steps
- Inspect the Prisma Client call – verify whether the relation you later use appears in the
includeorselectoptions. - Enable Prisma query logging – add
log: ['query', 'info']to thePrismaClientconstructor and run the request that triggers the suspected endpoint. - Count executed queries – attach a simple middleware or use a tracing tool to increment a counter for each SQL statement observed in the logs.
Cause / Diagnostic Table
| Observed Symptom | Typical Cause | Check to Perform |
|---|---|---|
| Multiple SELECT statements for the same relation after one findMany | Missing include or select for that relation |
Review the original query; enable logging and look for separate SELECTs with WHERE IN or WHERE = id patterns |
| Query count grows linearly with number of parent records | Relation accessed in a loop after the initial query | Add a query‑counting middleware; verify count matches parent count + 1 after fix |
Ordered Checks
-
Review the Prisma Client call
Locate the code that fetches the parent records. Example:
// Missing include → causes N+1 when accessing user.posts later const users = await prisma.user.findMany({ where: { active: true } });If you later do
user.postsinside a loop, Prisma will issue a separate query for each user. -
Enable query logging
Instantiate the client with logging:
const prisma = new PrismaClient({ log: ['query', 'info'], });Run the endpoint (e.g., via
curlor a test suite) and capture the console output. Look for patterns like:query: SELECT * FROM "User" WHERE "active" = true query: SELECT * FROM "Post" WHERE "authorId" = 1 query: SELECT * FROM "Post" WHERE "authorId" = 2 ... -
Count queries per request
Add a minimal middleware (Express example):
let queryCount = 0; prisma.$on('query', e => { queryCount++; }); app.use((req, res, next) => { queryCount = 0; res.on('finish', () => console.log(`Queries for ${req.path}: ${queryCount}`)); next(); });Run the request and note the printed count. Compare it to the expected count (parent query + 1 for the relation if loaded via JOIN or WHERE IN).
Fixes Tied to Findings
-
Add the missing relation to
includeorselectIf you need the whole relation:
const users = await prisma.user.findMany({ where: { active: true }, include: { posts: true }, });If you only need specific fields from the relation, use
select:const users = await prisma.user.findMany({ where: { active: true }, select: { id: true, name: true, posts: { select: { id: true, title: true } } }, }); -
Ensure the relation is loaded before accessing it
Avoid conditional access that might skip the include/select in some code paths. Load the relation upfront, then use the data.
-
Verify the generated SQL
After applying the fix, the logs should show either a single query with a JOIN (if the database supports it) or two queries: one for parents and one using a WHERE IN clause for all related records.
query: SELECT "User".*, "Post".* FROM "User" LEFT JOIN "Post" ON "Post"."authorId" = "User"."id" WHERE "User"."active" = trueor
query: SELECT * FROM "User" WHERE "active" = true query: SELECT * FROM "Post" WHERE "authorId" IN (1, 2, 3, ...)
Limitations and Practical Verification
- Large
includeorselectpayloads increase response size and memory usage. Only select the fields you truly need. - If the relation is optional or can be null, Prisma will still generate a LEFT JOIN or WHERE IN that returns empty rows for missing relations—this is expected and does not cause extra queries.
- Missing indexes on foreign‑key columns can make the WHERE IN query slow even though the query count is correct. Verify index existence with
SHOW INDEX FROM Post;in your database.
To confirm the fix works in practice:
- Run the request with query logging enabled.
- Check that the number of SQL statements matches the expected count (e.g., 2 statements for a list of users with posts).
- Ensure no additional SELECT statements appear when iterating over the relation in your application code.
Escalation Criteria
If after adding the proper include or select you still observe an elevated query count:
- Inspect the schema for circular relations that may cause Prisma to generate multiple queries.
- Confirm that relevant foreign‑key columns are indexed.
- Consider loading data in a single transaction using
prisma.$transactionor resorting to raw SQL for bulk fetching when the relation set is very large. - Collect the query logs, schema snippet, and the Prisma Client call, then open a GitHub issue in the Prisma repository with this information for further investigation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.