[{"data":1,"prerenderedAt":276},["ShallowReactive",2],{"blog-laravel-query-optimization":3},{"id":4,"title":5,"body":6,"date":263,"description":27,"excerpt":264,"extension":265,"meta":266,"navigation":144,"path":267,"seo":268,"stem":269,"tags":270,"__hash__":275},"blog\u002Fblog\u002Flaravel-query-optimization.md","N+1 Queries in Laravel: Detection and Elimination at Scale",{"type":7,"value":8,"toc":255},"minimark",[9,14,18,21,57,60,64,67,122,126,156,163,183,187,206,209,213,224,244,248,251],[10,11,13],"h2",{"id":12},"the-problem-no-one-talks-about-until-its-too-late","The Problem No One Talks About Until It's Too Late",[15,16,17],"p",{},"Your local environment runs fine. Tests pass. Then you hit 10,000 users and your database server catches fire. N+1 queries are often the culprit.",[15,19,20],{},"Consider a basic relationship:",[22,23,28],"pre",{"className":24,"code":25,"language":26,"meta":27,"style":27},"language-php shiki shiki-themes github-light github-dark","$posts = Post::all();\nforeach ($posts as $post) {\n    echo $post->author->name; \u002F\u002F SELECT * FROM users WHERE id = ?\n}\n","php","",[29,30,31,39,45,51],"code",{"__ignoreMap":27},[32,33,36],"span",{"class":34,"line":35},"line",1,[32,37,38],{},"$posts = Post::all();\n",[32,40,42],{"class":34,"line":41},2,[32,43,44],{},"foreach ($posts as $post) {\n",[32,46,48],{"class":34,"line":47},3,[32,49,50],{},"    echo $post->author->name; \u002F\u002F SELECT * FROM users WHERE id = ?\n",[32,52,54],{"class":34,"line":53},4,[32,55,56],{},"}\n",[15,58,59],{},"For 100 posts, this executes 101 queries. For 10,000 posts: 10,001 queries.",[10,61,63],{"id":62},"detection-with-laravel-telescope","Detection with Laravel Telescope",[15,65,66],{},"Enable query watching in Telescope and filter by request path. Any endpoint showing a query count that scales linearly with record count is a candidate for investigation.",[22,68,70],{"className":24,"code":69,"language":26,"meta":27,"style":27},"\u002F\u002F In a service provider (dev only)\nDB::listen(function ($query) {\n    if ($query->time > 100) { \u002F\u002F milliseconds\n        Log::warning('Slow query', [\n            'sql' => $query->sql,\n            'time' => $query->time,\n        ]);\n    }\n});\n",[29,71,72,77,82,87,92,98,104,110,116],{"__ignoreMap":27},[32,73,74],{"class":34,"line":35},[32,75,76],{},"\u002F\u002F In a service provider (dev only)\n",[32,78,79],{"class":34,"line":41},[32,80,81],{},"DB::listen(function ($query) {\n",[32,83,84],{"class":34,"line":47},[32,85,86],{},"    if ($query->time > 100) { \u002F\u002F milliseconds\n",[32,88,89],{"class":34,"line":53},[32,90,91],{},"        Log::warning('Slow query', [\n",[32,93,95],{"class":34,"line":94},5,[32,96,97],{},"            'sql' => $query->sql,\n",[32,99,101],{"class":34,"line":100},6,[32,102,103],{},"            'time' => $query->time,\n",[32,105,107],{"class":34,"line":106},7,[32,108,109],{},"        ]);\n",[32,111,113],{"class":34,"line":112},8,[32,114,115],{},"    }\n",[32,117,119],{"class":34,"line":118},9,[32,120,121],{},"});\n",[10,123,125],{"id":124},"the-fix-eager-loading","The Fix: Eager Loading",[22,127,129],{"className":24,"code":128,"language":26,"meta":27,"style":27},"\u002F\u002F Bad — triggers a query per iteration\n$posts = Post::all();\n\n\u002F\u002F Good — single JOIN resolves the relationship\n$posts = Post::with(['author', 'tags', 'comments.author'])->get();\n",[29,130,131,136,140,146,151],{"__ignoreMap":27},[32,132,133],{"class":34,"line":35},[32,134,135],{},"\u002F\u002F Bad — triggers a query per iteration\n",[32,137,138],{"class":34,"line":41},[32,139,38],{},[32,141,142],{"class":34,"line":47},[32,143,145],{"emptyLinePlaceholder":144},true,"\n",[32,147,148],{"class":34,"line":53},[32,149,150],{},"\u002F\u002F Good — single JOIN resolves the relationship\n",[32,152,153],{"class":34,"line":94},[32,154,155],{},"$posts = Post::with(['author', 'tags', 'comments.author'])->get();\n",[15,157,158,159,162],{},"For conditional relationships, use ",[29,160,161],{},"when",":",[22,164,166],{"className":24,"code":165,"language":26,"meta":27,"style":27},"$posts = Post::with(['author'])\n    ->when($request->include_comments, fn($q) => $q->with('comments'))\n    ->paginate(20);\n",[29,167,168,173,178],{"__ignoreMap":27},[32,169,170],{"class":34,"line":35},[32,171,172],{},"$posts = Post::with(['author'])\n",[32,174,175],{"class":34,"line":41},[32,176,177],{},"    ->when($request->include_comments, fn($q) => $q->with('comments'))\n",[32,179,180],{"class":34,"line":47},[32,181,182],{},"    ->paginate(20);\n",[10,184,186],{"id":185},"select-only-what-you-need","Select Only What You Need",[22,188,190],{"className":24,"code":189,"language":26,"meta":27,"style":27},"Post::with(['author:id,name,avatar'])\n    ->select(['id', 'title', 'user_id', 'created_at'])\n    ->paginate(20);\n",[29,191,192,197,202],{"__ignoreMap":27},[32,193,194],{"class":34,"line":35},[32,195,196],{},"Post::with(['author:id,name,avatar'])\n",[32,198,199],{"class":34,"line":41},[32,200,201],{},"    ->select(['id', 'title', 'user_id', 'created_at'])\n",[32,203,204],{"class":34,"line":47},[32,205,182],{},[15,207,208],{},"Selecting specific columns dramatically reduces memory usage and network transfer between your app and database. At scale, this matters more than eager loading alone.",[10,210,212],{"id":211},"lazy-eager-loading-for-conditional-data","Lazy Eager Loading for Conditional Data",[15,214,215,216,219,220,223],{},"Sometimes you need to load relationships after the initial query. Use ",[29,217,218],{},"loadMissing"," instead of ",[29,221,222],{},"load"," to avoid redundant queries when the relationship may already be loaded:",[22,225,227],{"className":24,"code":226,"language":26,"meta":27,"style":27},"$posts->loadMissing(['comments' => function ($query) {\n    $query->where('approved', true)->with('author:id,name');\n}]);\n",[29,228,229,234,239],{"__ignoreMap":27},[32,230,231],{"class":34,"line":35},[32,232,233],{},"$posts->loadMissing(['comments' => function ($query) {\n",[32,235,236],{"class":34,"line":41},[32,237,238],{},"    $query->where('approved', true)->with('author:id,name');\n",[32,240,241],{"class":34,"line":47},[32,242,243],{},"}]);\n",[10,245,247],{"id":246},"monitoring-in-production","Monitoring in Production",[15,249,250],{},"The Debugbar package adds a query count badge in dev. For production, track query counts per request in your APM (Datadog, New Relic) and alert when any endpoint exceeds a threshold. Catching a 50-query endpoint before it reaches 50,000 users is always cheaper than the firefight after.",[252,253,254],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":27,"searchDepth":41,"depth":41,"links":256},[257,258,259,260,261,262],{"id":12,"depth":41,"text":13},{"id":62,"depth":41,"text":63},{"id":124,"depth":41,"text":125},{"id":185,"depth":41,"text":186},{"id":211,"depth":41,"text":212},{"id":246,"depth":41,"text":247},"2024-11-15",null,"md",{},"\u002Fblog\u002Flaravel-query-optimization",{"title":5,"description":27},"blog\u002Flaravel-query-optimization",[271,272,273,274],"Laravel","PHP","MySQL","Performance","WI39NZWbsoMpKtYn4pAdM5wJO-5GYRFcv_LSmJ1uTu4",1779430667060]