The N+1 Query Problem Explained

The N+1 query problem is one of the most common performance bugs in applications using an ORM — invisible in development with a handful of test records, and genuinely crippling once real data volume arrives.

# The N+1 problem: 1 query for orders, then N more queries, one per order's customer
orders = Order.objects.all()          # 1 query
for order in orders:
    print(order.customer.name)          # a SEPARATE query for EACH order's customer!
# Total: 1 + N queries, where N = number of orders

# Fixed with eager loading: 2 queries total, regardless of how many orders exist
orders = Order.objects.select_related('customer')   # Django ORM example
for order in orders:
    print(order.customer.name)          # no additional query - already loaded

Why This Is So Easy to Miss

With 10 test records in development, N+1 means “11 queries instead of 2” — invisible in a page load that already feels instant either way. With 10,000 real production records, it’s the difference between 2 queries and 10,001, turning a page load from milliseconds into seconds or worse. This exact gap between dev and production data volume is why N+1 bugs so routinely slip through code review and testing.

Catching It

Most ORMs offer a query-count assertion for tests (Django’s assertNumQueries, Rails’ assert_queries) specifically to catch N+1 regressions automatically — genuinely worth adding to tests for any endpoint returning a list with related data, since it catches this class of bug before it ever reaches production.