Developers often ask me the same question: "My API is slow. Where do I start?" The reflex is usually to upgrade the server or double the RAM. That costs money and rarely fixes the root cause. In most Laravel applications, the bottleneck lives in the database layer. The framework's elegant syntax makes it easy to forget that each Eloquent call eventually becomes SQL, and that SQL is often where the pain begins.

Before you touch a server setting, work through your queries methodically.

Start With the Right Diagnostic

Do not optimize in the dark. Randomly rewriting queries is guesswork, and guesswork wastes hours.

You need to find the statements that consume the most total time. Watch your application under real load. Laravel Telescope gives you a clean view of every query executed during a request, with timing attached. Laravel Debugbar surfaces them in your browser during local development so you can spot anomalies immediately. When you need to catch problems in production, enable the MySQL Slow Query Log. It records statements that exceed a threshold you define, which makes it ideal for finding surprises that do not show up in small datasets. If you run something larger, an Application Performance Monitoring tool can correlate slow HTTP endpoints with specific database calls.

As you review the data, look for two things: absolute execution time and call frequency. A query taking forty milliseconds sounds harmless until you realize it runs two thousand times per minute. A three-second report that runs once per hour might matter less than a half-second lookup that runs on every page. Fix the high-impact problems first.

Stop Asking for Everything

SELECT * is convenient. It is also expensive. When you write Model::all() or fetch a result set without naming columns, MySQL hauls across every field for every matched row. That includes large text fields, JSON blobs, and anything else sitting on the table. The result set grows, memory usage climbs, and the time spent serializing the response increases.

Be explicit. If your controller only needs the id, name, and email fields, request exactly those:

User::select('id', 'name', 'email')->get();

In the query builder, the same principle applies. Smaller payloads move faster across the network and consume less RAM on your application server. This is one of the cheapest wins available, yet it is easy to skip because Laravel makes SELECT * the default behavior.

Let EXPLAIN Guide Your Changes

Never refactor a slow query without running EXPLAIN first. In MySQL, the EXPLAIN keyword shows the query execution plan. It reveals exactly how the optimizer intends to find your data.

Pay attention to the type column. If you see ALL, MySQL is performing a full table scan. That means it is reading every row to satisfy your WHERE clause. Look at the key column to see whether the optimizer is using an index at all. Then check the Extra column. If you spot Using temporary or Using filesort, MySQL is building intermediate tables or sorting in memory because your current structure cannot satisfy the query neatly.

Run EXPLAIN in your MySQL client, or use a tool that formats the output for you. Once you see the plan, you know whether the problem is a missing index, a bad join, or a predicate that the engine cannot optimize. Guessing becomes unnecessary.

Index With Intention

Indexes are the most powerful tool for speeding up lookups, but they work only when they match how you query. Without the right index, MySQL scans row by row. That might feel fine in development on a table with a thousand rows, and then collapse in production on a table with ten million.

Start with single-column indexes for fields that appear frequently in WHERE clauses. If you constantly filter by status, add an index on status.

When a query filters on multiple columns together, move to composite indexes. The order of columns inside the index matters because MySQL reads composite indexes from left to right. This is called the leftmost prefix rule. If your query searches by user_id and then orders by created_at, a composite index on (user_id, created_at) helps significantly. Reverse the order and the optimizer may not use the index for the filter at all.

Do not index every column. Each index adds overhead to inserts, updates, and deletes because MySQL must maintain the structure. Add them deliberately based on the patterns you found during measurement.

Keep Functions Away from Columns

This mistake quietly disables indexes. When you wrap a column in a function inside a WHERE clause, MySQL cannot