Skip to content
BuildThenTestedSee packages

One database query per list item

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. How the app behaves when there is more data or more users. Check it yourself with the steps below; our test adds what a script and a second account can find.

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];
}
A list of orders followed by one extra database query per order to fetch its customer: one query for the list, then one more for every row, the shape known as `N+1`.

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.

  1. 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".
  2. Reload the page and count the requests to your database or API (for Supabase they go to /rest/v1/...).
  3. 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).

Covered by

Full audit ($1,900, 5 business days)

Both include a re-check after your fixes.

Compare packages

Related patterns

  • List endpoints that return everythingPerformance

    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.

    Medium
  • No index on the columns you filter byPerformance

    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.

    Medium
  • Whole table downloaded, filtered in the browserPerformance

    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.

    Medium

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.