The slow query log was empty and the database was on fire. Connection pool exhausted, CPU pinned, request latency through the roof, and every individual query returning in under two milliseconds. Nothing was slow. That's the part that wastes an afternoon, because the obvious tool, the slow query log, has nothing to say when the problem isn't slowness. It's volume.
I turned on the general query log for sixty seconds on a single backend and counted. One page load, the dashboard, was issuing over four hundred queries. They were all the same shape:
SELECT * FROM line_items WHERE order_id = ?;
Classic N+1. The dashboard fetched a list of orders with one query, then looped over them and fetched the line items for each order individually. Forty orders on screen, one query for the list, forty for the items, and then another layer underneath doing the same thing for a related lookup, so it multiplied. Each query was genuinely fast. Four hundred fast queries, each with its own round trip and its own moment of pool contention, is not fast. It's a denial of service you wrote yourself.
The fix was to fetch the children in one go and stitch them in memory:
SELECT * FROM line_items WHERE order_id IN (?, ?, ?, ...);
One query for the list, one query for all the line items, group them by order_id in code, done. The page went from four hundred-odd queries to single digits and the database load problem simply evaporated. Whatever ORM or data layer you use, the trick is the same: load the parent set, collect the foreign keys, fetch all the children in a single IN query, and join them up in application code.
Two lessons stuck. The first is that "no slow queries" is not "no database problem". A profile that counts queries per request would have found this in minutes, where staring at the slow log found nothing in an hour. The second is that N+1 is invisible in development because you test with three rows, and at three rows four queries is fine. It only bites at production cardinality, which is exactly where you're least pleased to discover it. I now keep a query counter on a debug header in non-production builds, so a page that suddenly fires four hundred queries shouts about it on my machine, long before it shouts about it on the database's.