Ten rows in the dev database, on the same machine, 0.1 ms per query. Ten extra queries is one millisecond. The page feels instant, the code ships. Production has a thousand rows and the database one network hop away. Round trip is about a millisecond even in a good datacenter. A thousand queries is a second of pure network waiting. Not slow SQL. Each query is fast. The plural is slow.
I wrote about N+1 five years ago, in the Kohana times. The bug did not age. Only the reason it survives code review became clearer to me: locally it costs nothing.
This is also why you cannot find it in a slow query log. No single query is slow. You find it in a trace of the endpoint, where it looks like a barcode: the same SQL shape repeated hundreds of times with different ids. Take a trace from production, or at least count queries per request in staging with production-sized data. Count, not time. I said it in 2014 and I repeat it now: time depends on the machine, count is a property of the code.
The fix is the same menu as always. Eager loading when the ORM does it well. A join when you need a flat list and can give up the object model. Two queries with WHERE id IN (...) when the join multiplies rows. Any of the three turns a thousand round trips into two.
The new part of my routine is a test. After fixing an endpoint, I pin the query count in an integration test: this request performs 4 queries, assert it. The number looks fragile and pedantic. That is the point. When someone adds an innocent $order->customer->name into the loop next year, the test fails with 104 instead of 4, and the barcode never reaches production.
Regressions are silent. A failing assertion with 104 in it is not.