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 withDB::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();