Metastatic.Analysis.BusinessLogic.NPlusOneQuery
(Metastatic v0.10.4)
View Source
Detects N+1 query anti-pattern across all ORM frameworks.
This analyzer identifies code that performs database queries inside loops or collection operations, leading to N+1 query problems - a major performance issue in database-driven applications.
Cross-Language Applicability
This is a universal ORM anti-pattern that appears in all languages:
- Python/Django:
[User.objects.get(id=i.user_id) for i in items] - Python/SQLAlchemy:
[session.query(User).get(item.user_id) for item in items] - JavaScript/Sequelize:
items.map(item => User.findByPk(item.userId)) - JavaScript/TypeORM:
items.map(async item => userRepo.findOne(item.userId)) - Elixir/Ecto:
Enum.map(items, fn item -> Repo.get(User, item.user_id) end) - Ruby/ActiveRecord:
items.map { |item| User.find(item.user_id) } - C#/Entity Framework:
items.Select(item => context.Users.Find(item.UserId)) - Java/Hibernate:
items.stream().map(item -> session.get(User.class, item.getUserId()))
Problem
When you iterate over a collection and perform a database query for each item, you end up with:
- 1 query to fetch the collection
- N queries (one per item) to fetch related data
This scales poorly: 100 items = 101 queries!
Examples
Bad (Python/Django)
users = BlogPost.objects.all()
for post in posts:
author = User.objects.get(id=post.author_id) # N queries!
print(author.name)Good (Python/Django)
posts = BlogPost.objects.select_related('author').all()
for post in posts:
print(post.author.name) # No additional queriesBad (JavaScript)
const posts = await Post.findAll();
const enriched = posts.map(async post => ({
...post,
author: await User.findByPk(post.authorId) // N queries!
}));Good (JavaScript)
const posts = await Post.findAll({ include: [User] });
const enriched = posts.map(post => ({
...post,
author: post.User // Already loaded
}));Bad (Elixir/Ecto)
posts = Repo.all(Post)
Enum.map(posts, fn post ->
user = Repo.get(User, post.user_id) # N queries!
{post, user}
end)Good (Elixir/Ecto)
posts = Post |> preload(:user) |> Repo.all()
Enum.map(posts, fn post -> {post, post.user} end)Detection Strategy
Detects the MetaAST pattern:
{:collection_op, operation, lambda, collection}Where lambda (the function passed to map/filter/etc.) contains:
- Database query operations (identified by heuristics)
- Function calls matching database patterns
Database Operation Heuristics
Function names suggesting database operations:
*get*,*find*,*query*,*fetch**load*,*select*,*retrieve*- Specific ORM patterns:
Repo.*,*.objects.*,*Repository.*
Configuration
This analyzer is not configurable - N+1 queries are always a performance issue.
Detection Modes
The analyzer supports two detection modes:
Semantic (preferred): Uses
op_kindmetadata from semantic enrichment. If a function_call hasop_kind: [domain: :db, ...], it's definitively a database operation. This is accurate and framework-aware.Heuristic (fallback): Uses pattern matching on function names when semantic metadata is not available. May produce false positives.
Limitations
- Heuristic mode may produce false positives for non-database "get" operations
- Cannot detect N+1 at higher levels (e.g., GraphQL resolvers)