Database Performance Issues That Hide in Plain Sight During Code Review
N+1 queries, missing indices, and unbounded result sets look innocuous at small scale and catastrophic at large scale. Here's how to spot them in a diff before they reach production.
The most common source of production performance incidents in web applications isn't network latency, isn't application code complexity, and isn't infrastructure limitations. It's database queries that weren't written with production data volumes in mind. The query that works fine in development, passes all tests, and survives code review fails in production — not because it's wrong, but because it's wrong at scale.
Performance issues that are scale-dependent are particularly dangerous because they don't appear until the system is under real load, they often appear suddenly (as the dataset crosses a threshold that changes the query plan), and they're frequently misdiagnosed as infrastructure problems rather than code problems.
The N+1 Pattern: Still the Most Common Database Bug
The N+1 query problem has been a known antipattern for decades and remains one of the most common performance bugs in production systems. The pattern: fetch a list of N records with one query, then execute one additional query for each record to fetch its related data. The result is N+1 database round trips where one round trip (with a JOIN) would suffice.
In code review, the N+1 pattern appears as a database query inside a loop: an ORM call that's made inside a forEach or map over the result set of a previous query. The fix is usually a single query with a JOIN or an eager-loading configuration. The challenge is recognizing the pattern in ORM code where the query isn't explicit — an ORM that lazy-loads associations will execute the N+1 queries invisibly unless the reviewer knows to look for it.
Unbounded Queries
A query that returns all records from a table without a LIMIT clause is a potential denial-of-service vector against your own database. During development, the table has hundreds of rows and the query returns in milliseconds. In production, the table has millions of rows and the same query tries to load gigabytes of data into memory, exhausts connection pool resources, and degrades performance for every other query running simultaneously.
Any code review that includes a database query fetching from a collection should ask: what is the upper bound on the number of records this query could return? Is there a LIMIT clause? Is pagination implemented? Queries that operate on unbounded datasets should have explicit safeguards — either a LIMIT, or a documented architectural assumption about maximum dataset size that's validated by monitoring.
Missing Indices on Filtered Columns
A query that filters on a column without an index performs a full table scan: it reads every row in the table to find the matching rows. On small tables, this is fast. On large tables, it's slow. On very large tables, it can take minutes and lock resources that other queries need.
Code review should check: for every WHERE clause in a new or modified query, does the filtered column have an appropriate index? For queries that filter on multiple columns, is there a composite index in the right column order? This requires reviewers to be aware of the database schema — not just the code. Treating schema migrations as a required part of any PR that introduces new query patterns, and including index creation in those migrations, catches this class of issue before production.
The Slow Query Review Workflow
For any PR that adds or modifies significant database queries, the review workflow should include: reviewing the EXPLAIN/EXPLAIN ANALYZE output for the new query against a representative dataset, checking for N+1 patterns in any ORM code, verifying that LIMIT clauses are present on unbounded queries, and confirming that indices exist on all filtered and joined columns. This adds 15-20 minutes to the review of a data-intensive PR and prevents hours of production incident response.
Try CodeMouse on your next PR
Free AI code review on every pull request. Bring your own API key — no subscription needed.
Install on GitHub — Free