Back to Blog
N+1 Query ProblemMySQLSQL OptimizationPerformanceNode.js

Fixing N+1 Queries in a Live Multi-Store System

The morning report fetched a list of 100 stores, then ran a separate aggregation query for each one โ€” 101 round trips per page load, every day. A single JOIN with GROUP BY replaced all 101 queries and dropped load time from 3+ seconds to under 40ms.

Apr 15, 20266 min read

N+1 is one of those problems that is invisible at small scale and obvious in production. The system had a report endpoint that worked fine in testing. In production with 100 stores, it was the slowest endpoint on the server.

Problem

The original code fetched the list of active stores in one query. Then it looped through the result and ran a separate aggregation query for each store to get that store's totals.

One query to get 100 stores. One hundred queries to get their totals. 101 round trips per page load.

At 30ms average query time, that is 3 seconds of serial database time per request, not counting network and processing overhead. Under concurrent load it was worse. Multiple managers opening the report at the same time means 303 or 404 database queries from a single endpoint in a few seconds.

Query log showing 101 queries per page load

I found it by logging query count per request. Seeing 101 in the log for one page load made the problem obvious. Without that log it would have just looked like the report is slow.

Solution

Rewrote the endpoint as a single JOIN query with GROUP BY.

The stores table joins to transactions, inventory, and deliveries. GROUP BY store_id aggregates the totals in a single pass. The database returns one row per store with all aggregated values included.

One query instead of 101. The database handles the aggregation, which is what databases are for.

Added the composite indexes at the same time:

  • (store_id, transaction_date) on transactions for the GROUP BY aggregation
  • (store_id, is_active) on stores for the initial filter

Without the indexes, the JOIN query would do full table scans. With them, it uses the indexed access path and runs efficiently.

Single JOIN query replacing 101 individual queries

Result

The report endpoint went from 101 queries per page load to 1.

Load time dropped proportionally. At 30ms average, 101 queries is 3+ seconds. One aggregation query for 100 stores is 30 to 40ms.

Concurrent load handling improved significantly. 10 managers opening the same report simultaneously now generates 10 database queries instead of 1010.

If your report endpoint response time does not match the apparent query complexity, check the query count first. N+1 is the most common cause.

Found this useful?

Share it with someone who'd appreciate it.

https://wardvisual.com/blogs/fixing-n-plus-one-queries-live-system

Eduardo Manlangit Jr.
Eduardo Manlangit Jr.

@wardvisual ยท ๐Ÿ‡ต๐Ÿ‡ญ Dasmarinas City, Cavite PH

Full-stack engineer. Business systems, database optimization, and operations software.

Follow