Speed Up Big Laravel Queries with toBase()

Speed Up Big Laravel Queries with toBase()

Eloquent is Laravel's most loved feature. It makes working with database tables interactive by defining relationships, accessors, mutators and custom casts. But there's a price to pay for that convenience, that is model hydration.

When you query 10000 rows trough Eloquent, Laravel doesn't just get the rows, it creates 10000 individual PHP model objects in memory, loads traits, sets up event listeners, tracks dirty attributes and more. All of that if you just need the raw data to build a dropdown, generate an export or calculate some numbers. That's where toBase() comes in.

What Model Hydration Costs

Consider fetching 20,000 rows for a report:

// Standard Eloquent: Hydrates 20,000 Order instances
$orders = Order::select('id', 'total_amount', 'created_at')->get();

For each record PHP instantiates full App\Models\Order instance with all attributes, original values, casts, relations and metadata about the table. This one query alone could devour anywhere between 40 to 70 MB of memory and burn hundreds of milliseconds of CPU time just on object instantiation.

How toBase() Works

The toBase() method parses down from the Eloquent query builder (Illuminate\Database\Eloquent\Builder) to the core Query Builder (Illuminate\Database\Query\Builder).

use App\Models\Order;

// Returns a Collection of plain stdClass objects — zero model hydration
$orders = Order::where('status', 'completed')
    ->toBase()
    ->get(['id', 'total_amount', 'created_at']);

Instead of returning a collection of Eloquent models, toBase()->get() returns a collection of lightweight stdClass objects directly from the PDO driver. Due to this memory usage also drops around 70%-80%.

Practical Example: Dropdowns & Exports

A basic functionality where developer uses traditional eloquent is populating form dropdowns or generating file exports where model methods are never called:

namespace App\Http\Controllers;

use App\Models\City;
use Illuminate\Http\JsonResponse;

class CityController extends Controller
{
    public function dropdown(): JsonResponse
    {
        // 5,000 cities fetched without instantiating 5,000 City models
        $cities = City::where('is_active', true)
            ->orderBy('name')
            ->toBase()
            ->get(['id', 'name']);

        return response()->json($cities);
    }
}

The output sent to the browser or client is identical JSON, but your server uses only a fraction of the RAM.

toBase() vs DB::table()

You might think: "Why not just write DB::table('cities') instead?"

While DB::table() works, toBase() offers some unique advantages while working inside an existing application architecture:

  • Model-Defined Table Names & Connections: If your model defines a custom table (protected $table = 'app_cities') or a specific database connection, City::toBase() inherits those automatically. DB::table('cities') requires hardcoding table names.
  • Reusing Scopes: You can apply your Eloquent local scopes and conditions first, and then call toBase() at the end before fetching:
    // Reuses existing model scopes, drops to base at the end
    $activeVendors = Vendor::verified()->inGoodStanding()->toBase()->get(['id', 'name']);
  • Soft Deletes & Global Scopes: Global scopes (like soft-deleted column checks) defined on the model apply before toBase() converts the query builder.

What You Give Up When Using toBase()

Because toBase() returns simple PHP stdClass objects rather than Eloquent instances, you can't use all the features that are provided by the model class:

  • No accessors/mutators: Attributes such as $user->full_name will not exist unless you specifically select them as raw SQL expressions
  • No relationship loading: You cannot call $order->customer or use Eloquent's with() functionality for eager loading child relationships
  • No custom casts: JSON columns will stay JSON strings, and dates will not be automatically cast to Carbon instances
  • No model events: No retrieved or lifecycle hooks will be fired

Summary

Feature Standard Eloquent (Model::get()) Base Query (Model::toBase()->get())
Output Type Collection of Model instances Collection of stdClass objects
Memory Consumption High (model properties, tracking, traits) Minimal (raw primitive data)
Model Features Full (casts, accessors, relations) None (raw SQL columns only)
Best Use Case CRUD operations, business logic, domain mutations Exports, charts, dropdowns, large read-only lists

Whenever you are reading large volumes data just to present, report, or serialize directly into an API response, call toBase(). It allows you to keep your model scopes and table definitions while shedding the memory cost of model hydration.

Thank you for reading this article 😊

For any query, do not hesitate to comment 💬


Previous Post Next Post

Contact Form