[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"post-content-laravel-n-plus-one-queries-find-fix":3},"\u003Cfigure>\n  \u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1555949963-ff9fe0c870eb?auto=format&fit=crop&w=1200&q=80\" alt=\"Laptop screen showing lines of code during a debugging session\" loading=\"lazy\" \u002F>\n\u003C\u002Ffigure>\n\n\u003Cp>Last month I shipped an order dashboard for a client. Five rows in my local seed data, page rendered in 40ms, tests green, off it went. Two days later the client asks why the page \"sometimes takes four seconds.\" I pulled up Laravel Debugbar on a copy of their production data and the number at the bottom made me laugh out loud: \u003Cstrong>141 queries\u003C\u002Fstrong>. One page. One request.\u003C\u002Fp>\n\n\u003Cp>That's the N+1 problem, and if you've written more than two Eloquent queries in your life, you've shipped it at least once. The good news: it's one of the easiest performance bugs to catch, if you let Laravel yell at you.\u003C\u002Fp>\n\n\u003Ch2>What's actually happening\u003C\u002Fh2>\n\n\u003Cp>The classic version looks completely innocent:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">$orders = Order::latest()-&gt;take(20)-&gt;get();\n\nforeach ($orders as $order) {\n    echo $order-&gt;customer-&gt;name;\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>One query fetches the orders. Then every pass through the loop accesses \u003Ccode>customer\u003C\u002Fcode>, which wasn't loaded, so Eloquent helpfully lazy-loads it. That's one extra query per row. Twenty orders means twenty-one queries. Five thousand rows means five thousand and one. Nothing is broken. The data is correct. The page just gets slower as the table grows, which is exactly why these bugs survive code review and QA.\u003C\u002Fp>\n\n\u003Ch2>Make it fail loudly in development\u003C\u002Fh2>\n\n\u003Cp>This is the single most useful line I add to every Laravel project now:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">\u002F\u002F app\u002FProviders\u002FAppServiceProvider.php\nuse Illuminate\\Database\\Eloquent\\Model;\n\npublic function boot(): void\n{\n    Model::preventLazyLoading(! app()-&gt;isProduction());\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>With that in place, the loop above throws a \u003Ccode>LazyLoadingViolationException\u003C\u002Fcode> naming the model and the relationship, with a stack trace pointing at the exact line. Local and staging fail loudly; production keeps serving pages, which matters because some third-party packages lean on lazy loading and I don't want to find that out via a 500 error on a Tuesday.\u003C\u002Fp>\n\n\u003Cp>Want visibility in production without the exceptions? Log the violations instead:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">Model::handleLazyLoadingViolationUsing(function ($model, $relation) {\n    logger()-&gt;warning(\"Lazy load of {$relation} on \" . get_class($model));\n});\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>If you want to go further, \u003Ccode>Model::shouldBeStrict()\u003C\u002Fcode> also catches silently discarded attributes and access to missing ones. Worth turning on once lazy loading is under control.\u003C\u002Fp>\n\n\u003Ch2>The fix: load once, use many\u003C\u002Fh2>\n\n\u003Cp>The fix itself is one method call. Eager loading replaces the per-row query with a single additional query, no matter how many rows you have:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">$orders = Order::with('customer:id,name')\n    -&gt;latest()\n    -&gt;take(20)\n    -&gt;get();\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Two queries total. One for the orders, one \u003Ccode>WHERE id IN (...)\u003C\u002Fcode> for every customer at once.\u003C\u002Fp>\n\n\u003Cp>One gotcha that cost me an afternoon last year: when you select specific columns on the relation, the foreign key has to be in the list. \u003Ccode>with('customer:id,name')\u003C\u002Fcode> works. Drop the \u003Ccode>id\u003C\u002Fcode> and every relation silently comes back \u003Ccode>null\u003C\u002Fcode>. No error, no exception, just mysteriously blank output. Eloquent matches relations on that key, so if it's not selected, there's nothing to match on.\u003C\u002Fp>\n\n\u003Cp>Counts deserve their own mention, because the \"obvious\" fix is eager loading the whole relation just to display a number. If the page only shows \"12 items\", don't load twelve item models per row. Use SQL:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">$orders = Order::with('customer:id,name')\n    -&gt;withCount('items')\n    -&gt;get();\n\n\u002F\u002F $order-&gt;items_count is computed in SQL, no models hydrated\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>There's \u003Ccode>withSum\u003C\u002Fcode>, \u003Ccode>withAvg\u003C\u002Fcode> and friends if you need totals. Honestly, half the \"slow Eloquent page\" reports I've seen were someone counting a collection inside a loop.\u003C\u002Fp>\n\n\u003Ch2>Pin the query count so it stays fixed\u003C\u002Fh2>\n\n\u003Cp>Fixing it once is fine. Stopping the next person from reintroducing it is better. Laravel's test suite can assert an exact query count:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-php\">public function test_dashboard_runs_a_fixed_number_of_queries(): void\n{\n    Order::factory()-&gt;count(50)-&gt;for(Customer::factory())-&gt;create();\n\n    $this-&gt;expectsDatabaseQueryCount(3);\n\n    $this-&gt;get('\u002Fdashboard')-&gt;assertOk();\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>The exact number doesn't matter much. What matters is that it stays the same whether the factory creates 50 rows or 500, and that the test fails the day someone adds a lazy load inside a loop. That test has caught two regressions since I wrote it. Cheap insurance.\u003C\u002Fp>\n\n\u003Ch2>One caveat before you eager-load everything\u003C\u002Fh2>\n\n\u003Cp>Eager loading multiplies rows into a single \u003Ccode>IN\u003C\u002Fcode> query, and that query is only fast if the foreign key columns are actually indexed. Eloquent doesn't add indexes for you. If the \"after\" version is still slow, check the table side of the equation. I wrote up the indexing half of this in \u003Ca href=\"\u002Fposts\u002Fhow-i-cut-database-query-time-mysql-indexing\u002F\">how I cut database query time with MySQL indexing\u003C\u002Fa>, and it pairs directly with what's above.\u003C\u002Fp>\n\n\u003Cp>I keep these three snippets (strict mode, the violation logger, the query-count test) in \u003Ca href=\"\u002Fsnippetark\u002F\" rel=\"noopener noreferrer\" target=\"_blank\">Snippet Ark\u003C\u002Fa> so pasting them into a new project takes seconds. You probably should too. And if your page still crawls after all this, the bug probably isn't Eloquent. That's a job for the slow query log.\u003C\u002Fp>\n",1789138766796]