‹ Back to Blog

Understanding N+1 Database Queries: Rails, Django, and Elixir

Performance Rails Python Elixir

N+1 queries are the single most common performance problem in web applications that use an ORM. They happen when your code fetches a list of N records, then executes a separate database query for each record to load related data. Instead of 1 or 2 queries, you end up with N+1.

Many major ORMs are susceptible: ActiveRecord in Rails, Django’s ORM, and Ecto in Elixir. The pattern looks slightly different in each framework, but the cause and the fix follow the same logic. If you work across multiple frameworks (or your team does), understanding the pattern once means you can spot it anywhere.

In this guide:

  • What N+1 queries look like and why they hurt performance
  • How to detect and fix them in Rails, Django, and Elixir
  • Tools for catching them before they hit production
  • How Scout automatically identifies N+1 patterns without configuration

For deeper framework-specific coverage, see our dedicated guides: Rails N+1 Queries, Django N+1 Queries, and Elixir/Ecto N+1 Queries.

What Is an N+1 Query?

The pattern is straightforward. You query a list of records (1 query), then loop through them and access a related record on each one (N queries). The total is N+1 queries where 1 or 2 would have been enough.

Here is a simple example in Rails:

# 1 query to fetch all posts
posts = Post.all

# N queries, one per post, to fetch each author
posts.each do |post|
  puts "#{post.title} by #{post.author.name}"
end

The same pattern in Django:

# 1 query to fetch all books
books = Book.objects.order_by("title")

# N queries, one per book, to fetch each author
for book in books:
    print(book.title, "by", book.author.name)

And in Elixir with Ecto:

# 1 query to fetch all posts
posts = Repo.all(Post)

# N queries, one per post, to fetch each author
Enum.each(posts, fn post ->
  author = Repo.preload(post, :author).author
  IO.puts("#{post.title} by #{author.name}")
end)

In all three cases, the database executes 1 query to load the list, then N separate queries to load each related record. With 10 records, that is 11 queries. With 1,000 records, that is 1,001 queries. Each query carries network and execution overhead, and the total response time grows linearly with the number of records.

Why N+1 Queries Matter

Records Queries Without Fix Queries With Fix Typical Impact
10 11 1-2 Barely noticeable
100 101 1-2 Noticeably slower
1,000 1,001 1-2 Seconds of delay
10,000 10,001 1-2 Timeout or crash

The problem scales linearly, which means it often goes unnoticed in development (where you have 5 records) and becomes a crisis in production (where you have 5,000). This is why automated detection matters more than code review for catching N+1 patterns.

How to Fix N+1 Queries

The fix is the same concept across all frameworks: tell the ORM to load related data upfront instead of lazily. The terminology differs, but the idea is identical.

Rails: includes, preload, and eager_load

# Fix: eager load authors in one query
posts = Post.includes(:author)

posts.each do |post|
  puts "#{post.title} by #{post.author.name}"
end

Rails provides three methods:

Method Strategy Best For
includes Lets Rails choose JOIN or separate query Default choice
preload Always uses a separate query Many-to-many, avoiding duplicate data
eager_load Always uses a LEFT OUTER JOIN When you need to filter on the association

For a deep dive into Rails-specific patterns including Sidekiq jobs, ActiveRecord callbacks, and the Bullet gem, see our Rails N+1 Query Guide.

# Fix: eager load authors with a JOIN
books = Book.objects.order_by("title").select_related("author")

for book in books:
    print(book.title, "by", book.author.name)
Method Strategy Best For
select_related Uses a SQL JOIN ForeignKey and OneToOneField
prefetch_related Uses a separate query ManyToManyField and reverse ForeignKey

For the complete Django treatment including django-auto-prefetch, nested queries (2N+1, NM+N+1), and detection with django-debug-toolbar, see our Django N+1 Queries Guide.

Elixir/Ecto: Repo.preload and Ecto.Query.preload

# Fix: preload authors in a separate query
posts =
  Post
  |> Repo.all()
  |> Repo.preload(:author)

Enum.each(posts, fn post ->
  IO.puts("#{post.title} by #{post.author.name}")
end)

Or inline with the query:

posts = from(p in Post, preload: [:author]) |> Repo.all()
Method Strategy Best For
Repo.preload/2 Separate query after fetch Simple cases, pipeline style
from ... preload Inline with query When building complex queries
join + preload Uses a JOIN When filtering on the association

For Ecto-specific patterns including Oban jobs, Phoenix LiveView, and nested preloads, see our Elixir/Ecto N+1 Queries Guide.

Tools for Detecting N+1 Queries

Fixing N+1 queries is easy once you find them. Finding them is the hard part. Here is a comparison of the most common detection approaches across frameworks.

Tool Framework Detects Automatically Works in Production Shows Code Location
Scout Rails, Django, Elixir Yes Yes Yes
Bullet gem Rails Yes No (dev/test only) Partial
django-debug-toolbar Django Manual inspection No Yes
nplusone Django Yes (logging) Yes No
Ecto telemetry Elixir Manual setup Yes Partial

How Scout Detects N+1 Queries

Scout’s approach is different from framework-specific tools because it works at the APM level. The agent monitors every database query inside every request, background job, and code path. When it sees repeated query patterns with varying parameters, it flags the N+1 automatically and shows:

  • The exact code location causing the repeated queries
  • How many times the query was executed
  • The total time impact on the request
  • The generated SQL

This works without any configuration or code changes. Install the agent, deploy, and N+1 patterns start appearing in your dashboard. It works equally well in development and production, so you catch problems regardless of where they surface.

For Rails teams, this means you see N+1 patterns in controller actions, Sidekiq jobs, and Action Cable handlers. For Django teams, it covers views, Celery tasks, and management commands. For Elixir teams, it covers Phoenix controllers, LiveView, and Oban workers.

When N+1 Queries Are (Maybe) Acceptable

Not every N+1 query needs fixing. A few cases where the cost of fixing may not be worth it:

  • N is always small and bounded. If a page always shows exactly 3 items and never more, the 4 queries may be fast enough.
  • The query is already cached. If the related records are in a query cache or application cache, the “extra” queries may not hit the database at all.
  • The fix adds complexity that obscures the code. Sometimes an explicit N+1 with a comment explaining why is clearer than a complex prefetch chain.

That said, these are edge cases. In most production applications, N+1 queries should be found and fixed, because the N tends to grow over time even when you think it will not.

Frequently Asked Questions

What is an N+1 query?

An N+1 query is a database performance pattern where your code executes 1 query to fetch a list of N records, then N additional queries to load related data for each record. The fix is to tell the ORM to load related data upfront using eager loading (includes in Rails, select_related/prefetch_related in Django, Repo.preload in Ecto).

How do I find N+1 queries in my application?

The fastest way is to use an APM tool like Scout that automatically detects N+1 patterns in both development and production. Scout shows the exact code location, query count, and time impact without any configuration. Framework-specific tools like the Bullet gem (Rails) or django-debug-toolbar (Django) can also help but are typically limited to development.

Do N+1 queries only happen with ORMs?

No. N+1 patterns can occur anywhere you loop over results and make a query per result, including raw SQL, GraphQL resolvers, REST API calls, and even file system operations. ORMs make N+1 queries especially common because they make lazy loading the default behavior.

Should I fix every N+1 query?

Not necessarily. If N is always small, bounded, and the query is fast, the overhead may be negligible. Focus on N+1 queries that appear in high-traffic code paths or where N can grow unbounded. Scout’s N+1 insights help you prioritize by showing frequency and total time impact.

Does Scout detect N+1 queries automatically?

Yes. Scout monitors every database query in every request and background job, identifies repeated query patterns, and flags N+1 issues with the exact code location and time impact. This works without configuration in Rails, Django, and Elixir applications. See how it works or start a free trial with no credit card required. Our free tier is always available.

Next Steps

If you are working in a specific framework, our dedicated guides go deeper:

For application monitoring with errors, logs, and traces, Scout Monitoring provides the fastest insights without the bloat. See what we offer for Rails teams, compare Ruby monitoring tools, or start a free trial with no credit card required.