LEARN · DEBUGGING GUIDE

How to Find and Fix Prisma N+1 Queries with include and select

Prisma's include + select can silently generate N+1 queries. This guide shows you exactly how to spot them, fix them, and verify the solution—no fluff.

IntermediateDatabase7 min read

What this usually means

The N+1 problem in Prisma occurs when your code iterates over a list of parent records and, for each parent, executes an additional query to fetch related child records. This is especially common when you use include without the appropriate relation loading strategy, or when you combine include with select in a way that Prisma cannot optimize into a single JOIN. Unlike raw SQL, Prisma's generated queries are not always visible in the application code, so you need to inspect the actual queries hitting the database. The root cause is usually a missing or misconfigured relation load strategy (eager loading vs. lazy loading) or an inefficient schema design that forces per-row lookups.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Enable Prisma query logging: add log: ['query'] to your PrismaClient constructor.
  • 2Run your endpoint with a small dataset (e.g., 5 parents) and count the number of SELECT queries in the logs.
  • 3If you see more queries than 1 + number of relations, you have an N+1.
  • 4Use prisma.$on('query', ...) to log query duration and stack trace to identify the caller.
  • 5Check the Prisma Studio or database monitoring tool for sequential SELECTs on the same relation table.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchPrisma schema file (schema.prisma) — examine relation definitions and @relation attributes
  • searchApplication code where Prisma queries are executed — look for loops calling prisma.model.findUnique() inside .map() or forEach()
  • searchPrisma query logs (stdout or file) — grep for 'SELECT' and count occurrences
  • searchDatabase slow query log — look for repeated similar queries with different WHERE parameters
  • searchPrisma middleware or event listeners if you have custom logging
  • searchAPI route handlers that return nested JSON responses with included relations
  • searchYour ORM configuration (e.g., previewFeatures in generator block)
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningUsing findUnique inside a loop instead of using findMany with include
  • warningUsing include with select: true on the relation, which may cause Prisma to issue separate queries per parent
  • warningNot using the relationLoadStrategy: 'join' (available in Prisma 5.2+) for complex includes
  • warningLazy loading by default on a relation that should be eager-loaded
  • warningNested include chains (e.g., include: { posts: { include: { comments: true } } }) causing multiple queries per level
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildReplace findUnique inside loops with a single findMany that uses include to load all relations at once
  • buildIn Prisma 5.2+, set relationLoadStrategy: 'join' on the query to force a single SQL JOIN
  • buildUse batch loading with DataLoader to coalesce multiple findUnique calls into one query
  • buildRestructure your query to use raw SQL (prisma.$queryRaw) for complex nested reads where Prisma cannot optimize
  • buildDenormalize or add a view in the database to pre-join the data you need
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedEnable query logging and run the endpoint again; count the total SELECT queries — should be 1 (or constant) regardless of parent count
  • verifiedMonitor database response times under load test with k6 or artillery; confirm p95 latency drops significantly
  • verifiedUse EXPLAIN ANALYZE on the generated query (if using join strategy) to ensure it's a single statement
  • verifiedCheck connection pool usage before and after the fix — should show fewer concurrent connections
  • verifiedVerify that the fix works for edge cases (e.g., parents with 0 children, deep nesting)
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningAdding more include without checking the generated SQL — always verify with query logging
  • warningAssuming Prisma always generates a single query for include — it can fall back to N+1 if the relation is not indexed or if select is misused
  • warningApplying relationLoadStrategy: 'join' blindly without checking if your Prisma version supports it (5.2+ only)
  • warningUsing raw SQL for everything — Prisma's include with join strategy works well for most cases
  • warningIgnoring the N+1 in development because it works 'fast enough' with small datasets
( 07 )War story

The 10x Slower User Feed: A Prisma N+1 Case

Backend EngineerNode.js 18, Prisma 5.1, PostgreSQL 14, Express, AWS RDS db.r6g.large

Timeline

  1. 09:15Production alert: /api/feed endpoint p95 latency > 5s (normally 200ms)
  2. 09:20Checked RDS CloudWatch: average active connections jumped from 10 to 150
  3. 09:25Enabled Prisma query logging in staging, reproduced with 50 users
  4. 09:30Counted 51 SELECT queries: 1 for users, 50 for posts (one per user)
  5. 09:35Identified culprit: loop calling prisma.user.findUnique({ include: { posts: true } })
  6. 09:40Refactored to use prisma.user.findMany({ include: { posts: true } })
  7. 09:45Redeployed to staging, verified query count dropped to 1
  8. 09:50Deployed to production, p95 latency back to 200ms
  9. 10:00Retro: root cause was a junior dev following an old pattern; added query logging to CI

We were running a social media app backend on Node.js with Prisma connecting to PostgreSQL. The /api/feed endpoint returned a list of users with their recent posts. It worked fine during development with 10 test users, but in production with 500+ users, the p95 latency spiked from 200ms to over 5 seconds. CloudWatch showed the database connection pool maxed out at 150 connections.

I enabled Prisma's query logging by adding log: ['query'] to the PrismaClient. In staging, I triggered the endpoint with just 50 users and counted 51 SELECT queries: one to fetch the users, then 50 separate SELECTs to fetch posts for each user. That's the classic N+1. The code was using a for loop calling prisma.user.findUnique inside an async map.

The fix was straightforward: replace the loop with a single prisma.user.findMany that includes posts. After redeploying, the query count dropped to exactly 1. The p95 latency returned to 200ms. We added query logging to our CI pipeline to catch regressions. The lesson: always profile queries in an environment that mirrors production data size.

Root cause

N+1 query caused by calling findUnique inside a loop instead of using findMany with include.

The fix

Replaced the loop with a single findMany call using include: { posts: true }, reducing queries from N+1 to 1.

The lesson

Always enable Prisma query logging during development and test with realistic dataset sizes to catch N+1 patterns early.

( 08 )Why include + select Combos Cause N+1

When you write something like prisma.user.findMany({ include: { posts: { select: { title: true } } } }), Prisma might still generate separate queries if the relation is not a simple one-to-many. The key is how Prisma resolves the select on the relation. In some cases, especially with many-to-many or when the relation is not indexed, Prisma falls back to individual queries. The fix is to either remove the select on the relation (fetch all columns) or use raw SQL for precise control.

Prisma 5.2 introduced the relationLoadStrategy option. Setting it to 'join' forces Prisma to use a SQL JOIN for the include, eliminating N+1. However, 'join' is not compatible with all select patterns. If you get an error, you may need to restructure your query. The safest approach is to avoid select on included relations and instead filter after fetching.

( 09 )Using Prisma Query Logging to Detect N+1

Add log: ['query'] to your PrismaClient constructor: const prisma = new PrismaClient({ log: ['query'] });. This prints every SQL query to stdout. To count queries, run your endpoint with a fixed number of parent records (e.g., 10) and count the lines starting with 'SELECT'. If you see more than 1 + (depth of includes), you have an N+1.

For production, you can use prisma.$on('query', (e) => { // log to external system }) to capture query duration and stack trace. This helps identify the exact code location causing the N+1. Also, consider using Prisma's built-in event system to set a threshold and alert when a single request triggers more than a certain number of queries.

( 10 )Batch Loading with DataLoader as an Alternative

If you cannot restructure your queries to use findMany with include, you can use DataLoader to batch individual findUnique calls. DataLoader coalesces multiple requests for the same key into a single batch query. For example, if you need to fetch posts for multiple users, you can create a DataLoader that takes an array of userIds and calls prisma.post.findMany({ where: { userId: { in: userIds } } }).

This approach is useful when the N+1 is deep (e.g., comments on posts on users) and you cannot include everything at once. However, it adds complexity. I recommend first trying the join strategy or restructuring the query. DataLoader is a patch, not a cure.

( 11 )Schema Design to Avoid N+1

Sometimes the schema itself encourages N+1. For example, if you have a one-to-many relation but the foreign key is not indexed, Prisma may do sequential scans. Ensure all foreign keys have indexes. Also, consider if you can denormalize: store the child count in the parent table if you only need counts, avoiding the join entirely.

Another trick: use Prisma's raw queries with JOINs for complex reports. Prisma's include is great for simple CRUD, but for analytics, raw SQL gives you full control. You can mix raw queries with Prisma client using prisma.$queryRaw. Just be careful to parameterize inputs to prevent SQL injection.

Frequently asked questions

Does Prisma always generate a single SQL query for include?

No. Prisma may generate multiple queries (N+1) depending on the complexity of the include, the use of select on relations, and the Prisma version. Always verify by enabling query logging. Starting from Prisma 5.2, you can use relationLoadStrategy: 'join' to force a single query, but it's not always compatible with all select patterns.

How do I fix an N+1 when using nested include?

First, try to flatten the include by fetching the top-level entity with all needed relations in one query. If that's not possible, use DataLoader to batch the nested queries. In Prisma 5.2+, you can set relationLoadStrategy: 'join' on the top-level query, but be aware of limitations with select. Alternatively, use raw SQL with JOINs for deep nesting.

Can Prisma's select on a relation cause N+1?

Yes. When you use select on an included relation, Prisma may not be able to combine it into a single JOIN, especially if the relation is a many-to-many. In such cases, Prisma falls back to individual queries. To avoid this, either omit the select on the relation (fetch all columns) or use a raw query with a precise SELECT clause.

What is relationLoadStrategy and how do I use it?

relationLoadStrategy is a Prisma query option introduced in v5.2 that lets you choose between 'join' (single SQL query) and 'query' (default, may produce multiple queries). To use it: prisma.user.findMany({ include: { posts: true }, relationLoadStrategy: 'join' }). Note: Not all Prisma versions support it; check your version with npx prisma --version.

How can I catch N+1 in CI?

Set up a test that creates a known number of parent records (e.g., 10) and queries them with include. Enable Prisma query logging and count the number of SELECT queries. Assert that the count is 1 (or a small constant). Run this test in your CI pipeline. Tools like prisma-n+1-detector can automate this.