0

Eloquent vs raw Query Builder — when to drop down

Intermediate5 min read·lv-12-021
sqlinterviewperformance

Concept

Eloquent is the right tool for most Laravel work. But knowing when to drop to the raw Query Builder (DB::table()) is an important skill — Eloquent adds overhead and abstractions that sometimes work against you.

Use Eloquent when:

  • You need model events, observers, or lifecycle hooks.
  • You need relationship loading with with().
  • You need accessors, mutators, casts, or serialization.
  • You need soft deletes.
  • You're working with model-specific business logic.

Use Query Builder when:

  • Bulk inserts/updates/deletes: User::where('active', false)->delete() loads no models (mass operations bypass Eloquent). Same with DB::table('users')->where('active', false)->delete() — but the latter fires no model events at all.
  • Complex queries with many JOINs that don't map to Eloquent relationships cleanly.
  • Reporting queries that aggregate across many tables.
  • High-throughput scripts where even Eloquent's hydration overhead matters.
  • Working with tables that have no corresponding model.

Eloquent mass operations (update(), delete() called on a Builder, not a model instance): These DO NOT trigger model events. User::where('active', false)->delete() fires no deleting/deleted events. This is sometimes desired (performance) and sometimes a bug.

DB::statement(): Raw SQL for DDL statements (ALTER TABLE, etc.) or complex expressions.

DB::select() / DB::insert() / DB::update(): Raw SQL with bound parameters.

Code Example

php
<?php
use Illuminate\Support\Facades\DB;

// Query Builder — returns stdClass objects, not Eloquent models
$users = DB::table('users')
    ->select('id', 'name', 'email')
    ->where('active', true)
    ->orderBy('name')
    ->get(); // Collection of stdClass

$user = DB::table('users')->find(1); // stdClass|null

// Eloquent mass update — NO model events fired
User::where('last_login_at', '<', now()->subYear())->update(['active' => false]);
// Equivalent query-builder:
DB::table('users')->where('last_login_at', '<', now()->subYear())->update(['active' => false]);

// Bulk insert — much faster than model-per-row
$data = collect($csvRows)->map(fn($row) => [
    'name'       => $row[0],
    'email'      => $row[1],
    'created_at' => now(),
    'updated_at' => now(),
])->all();

DB::table('users')->insert($data);            // single INSERT with multiple rows
DB::table('users')->insertOrIgnore($data);    // ignore duplicate key errors
DB::table('users')->upsert(
    $data,
    ['email'],                                // unique key to match on
    ['name', 'updated_at']                    // columns to update on conflict
);

// Raw SQL — with parameter binding (PDO)
$results = DB::select('SELECT * FROM users WHERE created_at > ? AND role = ?', [
    now()->subMonth(),
    'admin',
]);

// Complex reporting query — impractical with Eloquent
$report = DB::table('orders as o')
    ->join('order_items as oi', 'oi.order_id', '=', 'o.id')
    ->join('products as p', 'p.id', '=', 'oi.product_id')
    ->select('p.category', DB::raw('SUM(oi.quantity * oi.price) as revenue'))
    ->whereBetween('o.created_at', [now()->startOfMonth(), now()->endOfMonth()])
    ->groupBy('p.category')
    ->orderByDesc('revenue')
    ->get();