--- title: Database Indexing for Laravel Apps at Scale category: Laravel published: 2026-07-02 reading_time: 8 min canonical: https://technologiesninja.com/blog-database-indexing-laravel.html --- # Database Indexing for Laravel Apps at Scale “It was fast in staging” almost always means staging had a thousand rows and production has a million. The most common Laravel performance incident is not a framework problem. It is a query that scans an entire table, ran fine for a year on small data, and became a wall the moment the table grew. Indexing is how you fix it — but only if you index the right thing, in the right order, for the right reason. ## Find the query before you touch anything Guessing which query is slow wastes days. Turn on the slow-query log, or use the database’s own statistics view, and let the data name the culprit. It is almost always a small number of queries doing almost all the damage, and they are rarely the ones you would have guessed. > Measure first. The query you assume is slow and the query that is actually slow are usually different queries. ## Read the plan, then decide Run EXPLAIN on the offending query. A sequential scan over a large table where you expected an index lookup is the signal. Add an index only when the plan shows it is needed — every index speeds reads and slows writes, so an unused index is pure cost. ``` EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42 AND status = 'paid' ORDER BY created_at DESC LIMIT 20; -- Seq Scan on orders -> you want an Index Scan here ``` ## Composite index column order matters A multi-column index is not a set; it is ordered, and the order decides which queries it can serve. The rule of thumb: equality columns first, then the range or sort column. For the query above, an index on (customer_id, status, created_at) serves the two equality filters and then the ordered limit — one index doing the whole job. - Put columns used with = before columns used with ranges or ORDER BY - A leading column the query does not filter on wastes the index - One well-ordered composite index often replaces three single-column ones - Index the columns in your WHERE and join conditions, not every column ## N+1 is not an indexing problem If a page fires one query to list orders and then one more per order to fetch its customer, no index will save you — the problem is the number of round trips, not the speed of each. That is eager loading’s job. Indexing and N+1 are different diseases with different cures, and confusing them is why people add indexes that change nothing. ``` // N+1: one query per order for the customer $orders = Order::all(); foreach ($orders as $o) { echo $o->customer->name; } // fixed: one extra query total $orders = Order::with('customer')->get(); ``` ## Why staging lies Staging has a few thousand rows, so a full table scan finishes instantly and every query looks fast. Production has millions, and the same scan is a timeout. The lesson is to test performance against production-scale data, or at least to read query plans rather than trusting wall-clock time on a tiny dataset. A plan that says “sequential scan” is a warning even when the query is currently fast. ## The short version Log slow queries, EXPLAIN the worst ones, add composite indexes with equality columns first, and fix N+1 with eager loading rather than with indexes. Do that and most “we need a bigger database” conversations turn into a handful of well-chosen indexes and a couple of query changes. --- ## Read next - Article — PHP-FPM Tuning for Laravel Under Load → /blog-php-fpm-tuning - Article — Queue Architecture for Scalable Laravel SaaS → /blog-laravel-queue-architecture - Service — DevOps & Performance → /service-devops-performance --- © 2026 TechnologiesNinja · Built and maintained in-house