Laravel 4 min read

Laravel Best Practices for Fast Data Retrieval

When it comes to speeding up data retrieval in Laravel, the biggest wins almost always come from how you query the database not from tweaking Laravel itself. Here's a practical, battle-tested approach to writing fast, efficient Eloquent and Query Builder code.

1. Select Only the Columns You Need

Avoid:

$users = User::all();

Prefer:

$users = User::query()
    ->select(['id', 'name', 'email'])
    ->get();

For a single record:

$user = User::query()
    ->select(['id', 'name', 'email'])
    ->find($id);

2. Use exists() / count() Instead of Retrieving Data

Don't:

if (User::where('email', $email)->get()->isNotEmpty()) {
    // ...
}

Use:

if (User::where('email', $email)->exists()) {
    // ...
}

exists() is much cheaper because the database doesn't need to return the matching rows it just confirms they exist.

3. Use value() When You Need a Single Value

Instead of:

$user = User::select('name')->find($id);
$name = $user?->name;

Use:

$name = User::whereKey($id)->value('name');

4. Use pluck() for a Single Column

Instead of:

$users = User::select('id')->get();
$ids = $users->pluck('id');

Use:

$ids = User::pluck('id');

// Or with a condition:
$emails = User::where('active', true)->pluck('email');

5. Use Query Builder for Read-Heavy Queries

For simple reads where you don't need Eloquent models:

$users = DB::table('users')
    ->select('id', 'name', 'email')
    ->where('active', true)
    ->get();

This avoids some of the Eloquent model hydration overhead. For large or read-heavy datasets, prefer:

Query Builder → DTO/array

over:

Eloquent → thousands of hydrated models

6. Index Your Database Properly

This is probably the most important optimization on this list.

If you frequently run:

User::where('email', $email)->first();

Make sure email is indexed:

$table->string('email')->unique();

For queries like:

Order::where('user_id', $userId)
    ->where('status', 'completed')
    ->get();

Consider a composite index:

$table->index(['user_id', 'status']);

Don't blindly index every column design indexes around your actual queries.

7. Avoid N+1 Queries

Bad:

$orders = Order::get();

foreach ($orders as $order) {
    echo $order->user->name;
}

This produces 1 query for orders + N queries for users.

Use eager loading instead:

$orders = Order::with('user')->get();

Even better, restrict the relationship columns:

$orders = Order::with('user:id,name')->get();

8. Don't Use with() Unnecessarily

This can become expensive:

User::with([
    'orders',
    'addresses',
    'payments',
    'roles',
])->get();

Only load relationships that are actually required by the response.

9. Use cursor() / lazy() for Huge Datasets

If you're processing millions of records:

User::query()
    ->where('active', true)
    ->cursor()
    ->each(function (User $user) {
        // process
    });

Or:

User::query()
    ->where('active', true)
    ->lazyById(1000)
    ->each(function (User $user) {
        // process
    });

For processing large datasets, lazyById() is generally preferable to loading everything into memory.

10. Use chunkById() for Bulk Processing

User::query()
    ->where('active', true)
    ->chunkById(1000, function ($users) {
        foreach ($users as $user) {
            // process
        }
    });

Prefer chunkById() over chunk() when records may be updated during processing, since it re-queries by ID and avoids skipping rows.

11. Cache Expensive or Repeated Reads

If data doesn't change frequently:

$settings = Cache::remember(
    'settings',
    now()->addHour(),
    fn () => Setting::query()->pluck('value', 'key')
);

For a frequently accessed lookup:

$user = Cache::remember(
    "user:{$id}",
    now()->addMinutes(10),
    fn () => User::select('id', 'name', 'email')->find($id)
);

But don't cache everything database indexes and good queries should come first.

12. Use EXPLAIN for Slow Queries

Given:

$orders = Order::query()
    ->where('customer_id', $customerId)
    ->where('status', 'pending')
    ->latest()
    ->get();

Check the SQL query plan:

$query = Order::query()
    ->where('customer_id', $customerId)
    ->where('status', 'pending')
    ->latest();

dd($query->toRawSql());

Then run the SQL with EXPLAIN ... to confirm the database is actually using your indexes.

My Practical Hierarchy

When optimizing reads in a Laravel application, I follow this order:

  1. Correct database indexes

  2. Efficient SQL/query conditions

  3. Select only required columns

  4. Avoid N+1 queries

  5. Avoid unnecessary Eloquent hydration

  6. Pagination / chunking for large datasets

  7. Cache repeated expensive queries

  8. Optimize PHP/Laravel code

For an API, a good pattern is:

$users = User::query()
    ->select(['id', 'name', 'email'])
    ->where('active', true)
    ->latest('id')
    ->paginate(50);

rather than retrieving the entire table and filtering/processing it in PHP.

The Bottom Line

If you want fast Laravel retrieval: think "database first, Laravel second." A well-indexed database with a small, targeted query will almost always outperform attempts to optimize PHP code around an inefficient query.

Share:

Related Posts

Explore more Laravel content?

Browse Laravel packages or explore my backend development services.

All Articles →