Search documentation of Metastatic

# Metastatic.Analysis.BusinessLogic.NPlusOneQuery (Metastatic v0.13.2)

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)