One database query per list item
What it looks like
const { data: orders } = await supabase.from("orders").select("*");
for (const order of orders) {
const { data: customer } = await supabase
.from("customers").select("*").eq("id", order.customer_id);
order.customer = customer[0];
}What happens
Pages that were fast with test data slow to a crawl as real data grows, the database handles hundreds of tiny queries per page view, and timeouts and higher hosting bills follow.
Where it shows up
Descriptive, not a verdict: builders and stacks whose defaults make this reachable, or that a public incident names. Each hub states the tool's own security features.
Seen in public
Snyk showed that GitHub Copilot uses open files as context: when a neighbouring file contained an SQL injection, the next suggestion reproduced the same pattern, taking the project from one SQL injection to two.
- from 1 to 2 SQL injections in demo project after suggestion
Feb 2024 · Snyk Labs · all sources
GitClear's analysis of 211 million changed lines from 2020 to 2024 found copy-pasted lines rose from 8.3% to 12.3% of changes while refactoring lines fell from 25% in 2021 to under 10% in 2024.
- 211 million Changed lines analysed
- January 2020 to December 2024 Period
- 8.3% to 12.3% Copy-pasted lines share
- 25% (2021) to under 10% (2024) Refactoring (moved) lines share
Jan 2025 · GitClear
Check it yourself
Uses your builder's dashboard and your browser. No tools to install.
- Open a page in your app that shows a list (orders, projects, customers) in Chrome, press F12, choose the Network tab and filter by "Fetch/XHR".
- Reload the page and count the requests to your database or API (for Supabase they go to
/rest/v1/...). - Add a few more items to the list, or open a longer list, and reload again.
Bad result: the number of requests grows with the number of items on screen, and many requests look identical except for an id at the end. A page should make a fixed handful of requests however long the list is.
What we check
We load list pages with seeded data at several sizes and count queries at the database, not just in the browser, so server-side loops in API routes and edge functions are caught too. The report names each page, the queries per row and the load time at each size.
Fix direction
Fetch the related rows in the same query. Supabase's nested select syntax such as select('*, customers(*)') returns orders with their customers in one request, and ORMs such as Prisma offer include or an in filter for the same purpose. Where a loop is unavoidable, collect the ids first and fetch them in one query.
Mechanism documented in: Supabase: Querying Joins and Nested tables (verified 2026-09-21); Prisma: Query optimization and performance (solving the n+1 problem) (verified 2026-09-21).
Related patterns
- List endpoints that return everythingPerformanceMedium
As the table grows, the response gets slower and heavier until it hits a platform body-size or time limit and the page simply stops loading, usually for your biggest customer first.
- No index on the columns you filter byPerformanceMedium
Every message list, dashboard filter and row-level security check scans the full table, so pages slow down as data grows and the database CPU stays busy until queries start timing out.
- Whole table downloaded, filtered in the browserPerformanceMedium
Every user's device downloads far more than it shows, so pages slow down as the table grows, and if the database does not also restrict rows, other people's records land in the browser.
Questions
why does my app get slower as i add more data
A common cause is a loop that runs one database query per item in a list, so a page with a hundred rows makes a hundred and one queries. It feels fine with a few test records and drags once customers add real data. Count the requests in the Network tab while the page loads; if the count tracks the list length, this is it.
what is the n plus one query problem
It is one query to fetch a list, then one more query for each item in that list to fetch something related, written N+1. Prisma's docs describe it as looping through query results and performing one additional query per result. The fix is to ask for the related data in the same query, or to fetch all related rows at once with an in filter.
how to fetch related data in one query with supabase
Supabase detects the relationships between your tables from their foreign keys. A select such as select('*, customers(*)') on the orders table returns each order with its customer nested inside, in one request. The docs cover one-to-many, many-to-many and tables with several foreign keys.
Back to the catalogue, or run the self-check for your stack.