Avoid N+1 data access in hot paths

perf-001

Intent

Prevent latency and infrastructure regressions caused by doing one database or remote lookup per item in a result set.

Applicability

Applies when the diff adds or changes code that iterates over many records and performs database, cache, filesystem, or network lookups for each item in a request path, render path, or large batch path. Return unknown if the changed code handles only a single entity, if the iteration is clearly over a tiny fixed set, or if no data-access call exists inside the loop.

What to inspect

Loops over query results or collections, ORM lazy-loading, repository calls inside loops, outbound HTTP calls inside loops, and nearby controller, handler, or job code that shows whether the path is latency-sensitive or processes many items.

Pass criteria

Direct evidence shows the code batches related lookups, eager-loads needed associations, precomputes a lookup map, or otherwise avoids one remote or database call per iterated item.

Fail criteria

The diff adds or keeps a pattern where code iterates over a collection and performs a database query or other remote call inside that loop for each item, with no repository evidence that the loop is intentionally bounded to a tiny constant size.

Do not flag

Do not flag test code, migrations, one-off maintenance scripts, loops over visibly tiny constant collections, or code where the lookup is already satisfied from an in-memory map prepared before the loop.

Confidence guidance

HIGH when the loop and per-item database or remote call are directly visible in a request or batch path. MEDIUM when the call is hidden behind helpers but the per-item access pattern is still strongly implied. LOW when the path’s scale or latency sensitivity is unclear from repository evidence.

Remediation

Batch the lookup once before iterating, or eager-load or project the related data needed by the loop.

Pass example

user_ids = [row.user_id for row in orders]
users_by_id = {
    user.id: user
    for user in db.users.find_many({"id": {"$in": user_ids}})
}

return [format_order(order, users_by_id[order.user_id]) for order in orders]

Fail example

result = []
for order in orders:
    user = db.users.find_one({"id": order.user_id})
    result.append(format_order(order, user))

Sources

  • Performance Excuses Debunked — Casey Muratori article
  • EF Core Performance Docs — Vlad Mihalcea article