Queries with no index, and the N+1 that follows
What you'd see: Pages that were instant during development take seconds in production, and get slower every week.
§ 01 — What's actually happening
Two problems usually arrive together. Without an index, every filtered query reads the entire table — invisible at 50 rows, fatal at 50,000. And a loop that queries inside itself turns one page load into hundreds of round trips. Neither shows up in testing, because test data is small.
Why an AI tool writes it this way
Schema generation optimises for correctness, not access patterns — indexes are only obvious once you know which queries matter. And <code>for (const x of items) { await fetch(x) }</code> is the most readable way to express "get the details for each", so it is what gets written.
What it costs you
Timeouts under normal traffic, a database bill climbing faster than your userbase, and connection-pool exhaustion that takes the whole app down rather than one slow page. On serverless the cost lands directly on your invoice.
§ 02 — Before & after
Fine with ten users, unusable at five hundred
Illustrative code, written for this page — never a client's project.
AI-generated
- Unindexed columns in every
whereclause - One query per row inside a loop (N+1)
- Whole table pulled into memory, then filtered in JavaScript
Human-reviewed
- Indexes on the columns actually filtered and sorted
- One query with a join or
include - Filtering, sorting and pagination done by the database
§ 03 — Check your own
How to tell in two minutes
You don't need us to run these. If any of them come back the wrong way, you have this problem.
- Turn on query logging and load your busiest page. If you see the same query repeated with different IDs, that is an N+1.
- Run
EXPLAIN ANALYZEon your slowest query.Seq Scanon a large table means no usable index. - Index every column you filter, sort or join on — foreign keys especially. Postgres does not index them automatically.
- Copy production-sized data into a staging database before you believe any performance result. 50 rows proves nothing.
Found it in your project? Fixing this one properly usually takes an engineer under an hour. Finding the other nine takes longer — which is what the free health check is for.
Get my free health check →§ 04 — Related
Other things we find
Stripe webhook accepts unverified events
Your webhook route reads the request body and acts on it directly.
Read the fix → CRITICAL Database & dataSupabase Row Level Security is off, or has no policy
Your app works, and the Supabase dashboard shows "RLS disabled" or "No policies" next to your tables.
Read the fix → CRITICAL Exposed credentialsSecret keys shipped to the browser
You have an environment variable named something like NEXT_PUBLIC_OPENAI_API_KEY or VITE_STRIPE_SECRET_KEY.