{"id":321,"date":"2025-08-27T19:44:12","date_gmt":"2025-08-27T19:44:12","guid":{"rendered":"https:\/\/1v0.net\/blog\/?p=321"},"modified":"2025-08-27T19:44:14","modified_gmt":"2025-08-27T19:44:14","slug":"query-performance-tuning-in-laravel-mysql","status":"publish","type":"post","link":"https:\/\/1v0.net\/blog\/query-performance-tuning-in-laravel-mysql\/","title":{"rendered":"Query Performance Tuning in Laravel + MySQL"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\"><strong>Query Performance Tuning in Laravel + MySQL<\/strong><\/h2>\n\n\n\n<p>Even well-built Laravel apps can slow down when queries become inefficient. Performance tuning involves analyzing how queries run, indexing properly, caching results, and monitoring live traffic. In this guide, you\u2019ll learn practical techniques to tune MySQL queries in Laravel, from <code>EXPLAIN<\/code> plans to eager loading, indexes, and caching. We\u2019ll also add a simple UI profiler to visualize queries during development.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1 &#8211; Find Slow Queries with Laravel Debug Tools<\/strong><\/h2>\n\n\n\n<p>Enable query logging in Laravel to see what queries run and how long they take. In dev, you can log all queries easily:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Providers\/AppServiceProvider.php (boot)<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">DB<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">Log<\/span>;\n\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">boot<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span>\n<\/span>{\n    DB::listen(<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">($query)<\/span> <\/span>{\n        Log::info(\n            $query-&gt;sql,\n            &#91;<span class=\"hljs-string\">'bindings'<\/span> =&gt; $query-&gt;bindings, <span class=\"hljs-string\">'time_ms'<\/span> =&gt; $query-&gt;time]\n        );\n    });\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This hooks into Laravel\u2019s DB layer and logs every query\u2019s SQL, bindings, and runtime to <code>storage\/logs\/laravel.log<\/code>. Use this in development or staging only\u2014it can be noisy in production.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2 &#8211; Analyze Queries with EXPLAIN<\/strong><\/h2>\n\n\n\n<p>Use MySQL\u2019s <code>EXPLAIN<\/code> to see how a query executes. This reveals whether indexes are used and where full scans happen.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"SQL (Structured Query Language)\" data-shcb-language-slug=\"sql\"><span><code class=\"hljs language-sql\"><span class=\"hljs-keyword\">EXPLAIN<\/span> <span class=\"hljs-keyword\">SELECT<\/span> * <span class=\"hljs-keyword\">FROM<\/span> orders <span class=\"hljs-keyword\">WHERE<\/span> <span class=\"hljs-keyword\">status<\/span> = <span class=\"hljs-string\">'paid'<\/span> <span class=\"hljs-keyword\">ORDER<\/span> <span class=\"hljs-keyword\">BY<\/span> created_at <span class=\"hljs-keyword\">DESC<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">SQL (Structured Query Language)<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">sql<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If the result shows \u201cUsing where; Using index\u201d you\u2019re good. If it says \u201cUsing filesort\u201d or \u201cUsing temporary\u201d, you may need to add or adjust indexes.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3 &#8211; Add Proper Indexes in Migrations<\/strong><\/h2>\n\n\n\n<p>Indexes speed up WHERE, ORDER BY, and JOIN clauses. Add them in migrations for frequently queried columns.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ database\/migrations\/xxxx_add_indexes_to_orders.php<\/span>\nSchema::table(<span class=\"hljs-string\">'orders'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">(Blueprint $table)<\/span> <\/span>{\n    $table-&gt;index(<span class=\"hljs-string\">'status'<\/span>);\n    $table-&gt;index(&#91;<span class=\"hljs-string\">'user_id'<\/span>,<span class=\"hljs-string\">'created_at'<\/span>]); <span class=\"hljs-comment\">\/\/ compound index<\/span>\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>status<\/code> gets a single index for filters like <code>WHERE status = 'paid'<\/code>. A compound index on <code>(user_id, created_at)<\/code> speeds up queries filtering by user and sorting by creation date. Don\u2019t over-index\u2014each index costs extra storage and slower writes.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4 &#8211; Reduce N+1 Queries with Eager Loading<\/strong><\/h2>\n\n\n\n<p>N+1 queries often cause slowdowns. Fix them with <code>with()<\/code> or <code>load()<\/code> to prefetch related data in fewer queries.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ Slow (lazy loading)<\/span>\n$users = User::all();\n<span class=\"hljs-keyword\">foreach<\/span> ($users <span class=\"hljs-keyword\">as<\/span> $user) {\n    <span class=\"hljs-keyword\">echo<\/span> $user-&gt;posts-&gt;count(); <span class=\"hljs-comment\">\/\/ query per user<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ Fast (eager loading)<\/span>\n$users = User::withCount(<span class=\"hljs-string\">'posts'<\/span>)-&gt;get();\n<span class=\"hljs-keyword\">foreach<\/span> ($users <span class=\"hljs-keyword\">as<\/span> $user) {\n    <span class=\"hljs-keyword\">echo<\/span> $user-&gt;posts_count; <span class=\"hljs-comment\">\/\/ no extra queries<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>withCount()<\/code> lets you grab relation counts directly with one query, instead of running a query for every loop iteration. This can save hundreds of queries on list pages.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5 &#8211; Optimize Pagination Queries<\/strong><\/h2>\n\n\n\n<p>Pagination with <code>OFFSET<\/code> gets slower on large tables because MySQL still scans skipped rows. Use <code>where('id','&gt;',...)<\/code> or <code>chunkById()<\/code> for \u201ckeyset pagination.\u201d<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ Classic pagination (slow with big OFFSET)<\/span>\n$posts = Post::orderBy(<span class=\"hljs-string\">'id'<\/span>)-&gt;offset(<span class=\"hljs-number\">50000<\/span>)-&gt;limit(<span class=\"hljs-number\">20<\/span>)-&gt;get();\n\n<span class=\"hljs-comment\">\/\/ Keyset pagination (fast)<\/span>\n$posts = Post::where(<span class=\"hljs-string\">'id'<\/span>,<span class=\"hljs-string\">'&gt;'<\/span>, $lastSeenId)\n             -&gt;orderBy(<span class=\"hljs-string\">'id'<\/span>)\n             -&gt;limit(<span class=\"hljs-number\">20<\/span>)\n             -&gt;get();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Keyset pagination avoids scanning all skipped rows, making it dramatically faster on large datasets. Store the last seen ID in your pagination links.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>6 &#8211; Cache Heavy Queries<\/strong><\/h2>\n\n\n\n<p>When a query result doesn\u2019t change often, cache it in Redis or the file cache. Use <code>remember()<\/code> to wrap queries.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">Cache<\/span>;\n\n$stats = Cache::remember(<span class=\"hljs-string\">'dashboard_stats'<\/span>, <span class=\"hljs-number\">600<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">()<\/span> <\/span>{\n    <span class=\"hljs-keyword\">return<\/span> Order::selectRaw(<span class=\"hljs-string\">'status, COUNT(*) as total'<\/span>)\n        -&gt;groupBy(<span class=\"hljs-string\">'status'<\/span>)\n        -&gt;pluck(<span class=\"hljs-string\">'total'<\/span>,<span class=\"hljs-string\">'status'<\/span>);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This caches the grouped order counts for 10 minutes (<code>600<\/code> seconds). Subsequent requests return from cache instantly instead of rerunning the query.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>7 &#8211; UI Example: Query Profiler Panel<\/strong><\/h2>\n\n\n\n<p>Let\u2019s add a simple UI snippet to show executed queries on a page during development. This is a lightweight alternative to installing Telescope or Debugbar.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;!-- resources\/views\/layouts\/partials\/query-profiler.blade.php --&gt;\n@php\n  $queries = DB::getQueryLog();\n@endphp\n\n@<span class=\"hljs-keyword\">if<\/span>(app()-&gt;environment(<span class=\"hljs-string\">'local'<\/span>))\n  &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">container<\/span> <span class=\"hljs-title\">mt<\/span>-5\"&gt;\n    &lt;<span class=\"hljs-title\">h5<\/span>&gt;<span class=\"hljs-title\">Executed<\/span> <span class=\"hljs-title\">Queries<\/span> (<\/span>{{ count($queries) }})&lt;\/h5&gt;\n    &lt;ul <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">list<\/span>-<span class=\"hljs-title\">group<\/span>\"&gt;\n      @<span class=\"hljs-title\">foreach<\/span>($<span class=\"hljs-title\">queries<\/span> <span class=\"hljs-title\">as<\/span> $<span class=\"hljs-title\">q<\/span>)\n        &lt;<span class=\"hljs-title\">li<\/span> <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">list<\/span>-<span class=\"hljs-title\">group<\/span>-<span class=\"hljs-title\">item<\/span>\"&gt;\n          <\/span>{{ $q&#91;<span class=\"hljs-string\">'query'<\/span>] }} \n          &#91;{{ implode(<span class=\"hljs-string\">','<\/span>, $q&#91;<span class=\"hljs-string\">'bindings'<\/span>]) }}] \n          ({{ $q&#91;<span class=\"hljs-string\">'time'<\/span>] }} ms)\n        &lt;\/li&gt;\n      @<span class=\"hljs-keyword\">endforeach<\/span>\n    &lt;\/ul&gt;\n  &lt;\/div&gt;\n@<span class=\"hljs-keyword\">endif<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This partial prints all queries on the page with execution times. To enable logging, call <code>DB::enableQueryLog()<\/code> in a service provider for your local environment.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping Up<\/h2>\n\n\n\n<p>Performance tuning in Laravel is a mix of good schema design, query inspection, caching, and avoiding N+1 traps. You saw how to log and analyze queries, add indexes, use eager loading, optimize pagination, and cache results. With these tools, your Laravel + MySQL app can handle far more traffic smoothly.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">What\u2019s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/10-proven-ways-to-optimize-laravel-for-high-traffic\">10 Proven Ways to Optimize Laravel for High Traffic<\/a><\/li>\n<li><a href=\"\/blog\/handling-large-data-sets-in-laravel-with-chunking-cursors\">Handling Large Data Sets with Chunking &amp; Cursors<\/a><\/li>\n<li><a href=\"\/blog\/laravel-and-docker-setting-up-a-scalable-dev-environment\">Laravel and Docker: Setting Up a Scalable Dev Environment<\/a><\/li>\n<\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>Query Performance Tuning in Laravel + MySQL Even well-built Laravel apps can slow down when queries become inefficient. Performance tuning involves analyzing how queries run, indexing properly, caching results, and monitoring live traffic. In this guide, you\u2019ll learn practical techniques to tune MySQL queries in Laravel, from EXPLAIN plans to eager loading, indexes, and caching. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":325,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[49,44,50],"class_list":["post-321","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-laravel","tag-mysql","tag-performance","tag-query-optimization"],"_links":{"self":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/321","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/comments?post=321"}],"version-history":[{"count":1,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/321\/revisions"}],"predecessor-version":[{"id":324,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/321\/revisions\/324"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media\/325"}],"wp:attachment":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media?parent=321"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/categories?post=321"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/tags?post=321"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}