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 queries

Bad (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:

  1. Semantic (preferred): Uses op_kind metadata from semantic enrichment. If a function_call has op_kind: [domain: :db, ...], it's definitively a database operation. This is accurate and framework-aware.

  2. 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)