Laravel Query Builder vs. Eloquent: A Comprehensive Comparison
19 May 2025 | Category: Laravel
Laravel provides two powerful tools for interacting with databases: Query Builder and Eloquent ORM. Both allow you to query and manipulate data, but they differ in their approach, syntax, and use cases. This SEO-friendly, plagiarism-free guide compares Laravel’s Query Builder and Eloquent ORM, highlighting their features, differences, and when to use each. Based on Laravel 11 (as of May 19, 2025), this tutorial builds on previous discussions about models and migrations and is designed for beginners and intermediate developers.
What is Query Builder?
Laravel’s Query Builder is a fluent, database-agnostic interface for constructing and executing SQL queries. It provides methods to build queries programmatically without writing raw SQL, using the DB
facade. Query Builder operates directly on tables and returns results as arrays or collections.
Key Features
- Fluent Interface: Chain methods to build queries (e.g.,
where()
,select()
,join()
). - Database Agnostic: Works across MySQL, PostgreSQL, SQLite, and SQL Server.
- Raw SQL Support: Allows raw SQL for complex queries.
- Lightweight: No overhead of ORM features like models or relationships.
- Returns Collections/Arrays: Results are typically
Illuminate\Support\Collection
or plain arrays.
What is Eloquent ORM?
Eloquent ORM (Object-Relational Mapping) is Laravel’s active record implementation, where each model class maps to a database table, and instances represent table rows. Eloquent builds on Query Builder, adding object-oriented features like relationships, mutators, and mass assignment.
Key Features
- Active Record: Models represent table rows with attributes matching columns.
- Relationships: Define one-to-one, one-to-many, or many-to-many relationships.
- Mass Assignment: Safely insert/update multiple fields.
- Timestamps and Soft Deletes: Automatic handling of
created_at
,updated_at
, anddeleted_at
. - Object-Oriented: Returns model instances for intuitive data manipulation.
Key Differences Between Query Builder and Eloquent
Feature | Query Builder | Eloquent ORM |
---|---|---|
Approach | Procedural, query-focused | Object-oriented, model-focused |
Syntax | Uses DB facade (e.g., DB::table() ) | Uses model classes (e.g., Post::where() ) |
Results | Arrays or Collections | Model instances or Collections |
Relationships | Manual joins required | Built-in relationship methods |
Mass Assignment | Not supported | Supported via $fillable or $guarded |
Timestamps | Manual handling | Automatic if enabled |
Soft Deletes | Manual implementation | Built-in with SoftDeletes trait |
Performance | Lightweight, faster for simple queries | Slightly heavier due to ORM overhead |
Complexity | Simpler for raw queries | More features for complex applications |
Use Case | Simple queries, non-model tables | Model-based apps with relationships |
Query Builder in Action
Query Builder is accessed via the DB
facade and operates directly on table names. It’s ideal for simple queries or when you don’t need model overhead.
Example: Querying Posts
use Illuminate\Support\Facades\DB;
// Get all published posts
$posts = DB::table('posts')
->where('is_published', true)
->orderBy('title')
->get();
// Get a single post by ID
$post = DB::table('posts')
->where('id', 1)
->first();
// Insert a post
DB::table('posts')->insert([
'title' => 'New Post',
'slug' => 'new-post',
'content' => 'Content here',
'is_published' => false,
'user_id' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
// Update posts
DB::table('posts')
->where('user_id', 1)
->update(['is_published' => true]);
// Delete posts
DB::table('posts')
->where('id', 1)
->delete();
// Join with users table
$posts = DB::table('posts')
->join('users', 'posts.user_id', '=', 'users.id')
->select('posts.title', 'users.name')
->get();
- Output:
$posts
is aCollection
ofstdClass
objects or arrays. - Note: You must manually handle timestamps and relationships (e.g., via joins).
Advanced Query Builder Features
- Aggregates:
$count = DB::table('posts')->count();
$maxViews = DB::table('posts')->max('views');
- Raw Queries:
$posts = DB::table('posts')
->whereRaw('LENGTH(title) > ?', [10])
->get();
- Subqueries:
$posts = DB::table('posts')
->whereIn('user_id', function ($query) {
$query->select('id')
->from('users')
->where('active', true);
})
->get();
Eloquent ORM in Action
Eloquent uses model classes to interact with tables, providing an object-oriented interface. It’s ideal for applications with complex relationships or model-specific logic.
Example: Querying Posts
Assume a Post
model (app/Models/Post.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
protected $fillable = ['title', 'slug', 'content', 'is_published', 'user_id'];
public function user()
{
return $this->belongsTo(User::class);
}
}
CRUD Operations:
use App\Models\Post;
// Get all published posts
$posts = Post::where('is_published', true)->orderBy('title')->get();
// Get a single post by ID
$post = Post::find(1); // Or Post::findOrFail(1);
// Create a post
$post = Post::create([
'title' => 'New Post',
'slug' => 'new-post',
'content' => 'Content here',
'is_published' => false,
'user_id' => 1,
]);
// Update a post
$post = Post::find(1);
$post->update(['title' => 'Updated Title']);
// Delete a post (soft delete)
$post->delete();
// Restore soft-deleted post
Post::withTrashed()->find(1)->restore();
// Eager load relationship
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->title . ' by ' . $post->user->name;
}
- Output:
$posts
is aCollection
ofPost
model instances. - Note: Timestamps are automatic, and relationships are accessed via methods.
Advanced Eloquent Features
- Relationships:
$userPosts = User::find(1)->posts()->where('is_published', true)->get();
- Accessors:
public function getTitleAttribute($value)
{
return strtoupper($value);
}
- Scopes:
public function scopePublished($query)
{
return $query->where('is_published', true);
}
// Usage
$posts = Post::published()->get();
- Soft Deletes:
$trashed = Post::onlyTrashed()->get();
Query Builder vs. Eloquent: Detailed Comparison
1. Syntax and Readability
- Query Builder:
- Procedural, table-focused:
DB::table('posts')->where('id', 1)
. - More verbose for complex queries involving joins.
- Clear for raw SQL or simple queries.
- Eloquent:
- Object-oriented:
Post::find(1)
. - Cleaner for relationship-based queries.
- More intuitive for developers familiar with ORM.
Example (Get published posts by a user):
- Query Builder:
$posts = DB::table('posts')
->join('users', 'posts.user_id', '=', 'users.id')
->where('posts.is_published', true)
->where('users.id', 1)
->select('posts.*')
->get();
- Eloquent:
$posts = Post::where('is_published', true)
->where('user_id', 1)
->with('user')
->get();
2. Performance
- Query Builder:
- Lightweight, no ORM overhead (e.g., no model instantiation).
- Faster for simple queries or bulk operations.
- Ideal for raw SQL or complex joins.
- Eloquent:
- Slightly slower due to model instantiation, relationship handling, and event dispatching.
- Can cause N+1 query issues if relationships aren’t eager-loaded.
- Optimized with techniques like
with()
or caching.
Example (N+1 Issue in Eloquent):
// Bad: N+1 queries
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // Queries user for each post
}
// Good: Eager loading
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // Single query for users
}
3. Relationships
- Query Builder:
- Requires manual joins:
php $posts = DB::table('posts') ->join('users', 'posts.user_id', '=', 'users.id') ->select('posts.title', 'users.name') ->get();
- More control but verbose for complex relationships.
- Eloquent:
- Built-in relationship methods:
php $posts = Post::with('user')->get();
- Simplifies one-to-many, many-to-many, etc.
4. Maintainability
- Query Builder:
- Better for ad-hoc queries or scripts.
- Less maintainable for large applications with complex logic.
- No built-in support for model-specific features like accessors or scopes.
- Eloquent:
- Ideal for maintainable, model-driven applications.
- Encapsulates table logic in models (e.g., scopes, relationships).
- Easier to extend with custom methods or traits.
5. Error Handling
- Query Builder:
- Returns
null
or empty collections for missing records. - Manual validation for data integrity.
- Eloquent:
- Methods like
findOrFail()
throwModelNotFoundException
. - Built-in validation via mass assignment (
$fillable
).
When to Use Query Builder
- Simple Queries: Fetch data from tables without needing models (e.g., reporting or analytics).
- Non-Model Tables: Query tables without corresponding models (e.g., pivot tables, logs).
- Performance-Critical Operations: Bulk inserts, updates, or complex joins where ORM overhead is undesirable.
- Raw SQL Needs: Use raw queries for database-specific features not supported by Eloquent.
- Scripts or Migrations: Quick queries in seeders or maintenance scripts.
Example Use Case: Aggregate report from a logs table:
$stats = DB::table('logs')
->select(DB::raw('DATE(created_at) as date, COUNT(*) as count'))
->groupBy('date')
->get();
When to Use Eloquent
- Model-Driven Applications: Apps with clear entities (e.g., users, posts, comments).
- Relationships: Projects requiring one-to-many, many-to-many, or polymorphic relationships.
- CRUD Operations: Standard create, read, update, delete tasks with model logic.
- Soft Deletes and Timestamps: Automatic handling of
deleted_at
orcreated_at
. - Maintainable Code: Large applications where model encapsulation improves readability.
Example Use Case: Blog with posts and users:
$posts = Post::with('user')
->where('is_published', true)
->orderBy('published_at', 'desc')
->paginate(10);
Combining Query Builder and Eloquent
You can mix Query Builder and Eloquent when needed, as Eloquent is built on top of Query Builder.
Example: Use Query Builder within an Eloquent model:
class Post extends Model
{
public function scopeComplexQuery($query)
{
return $query->whereIn('id', function ($subQuery) {
$subQuery->select('post_id')
->from('post_tag')
->where('tag_id', 1);
});
}
}
// Usage
$posts = Post::complexQuery()->get();
Example: Use Eloquent model with raw Query Builder:
$posts = DB::table('posts')
->where('is_published', true)
->get()
->map(fn ($post) => Post::make((array) $post));
Best Practices
- Choose Based on Needs:
- Use Query Builder for simple, performance-critical, or non-model queries.
- Use Eloquent for model-driven apps with relationships.
- Avoid N+1 Issues in Eloquent:
- Always eager-load relationships with
with()
:php Post::with(['user', 'tags'])->get();
- Secure Queries:
- Use parameter binding to prevent SQL injection:
php DB::table('posts')->where('title', '=', $title)->get();
- Use
$fillable
in Eloquent for mass assignment.
- Optimize Performance:
- Cache queries with Laravel’s cache system for frequently accessed data.
- Use
select()
to retrieve only needed columns:php Post::select(['id', 'title'])->get();
- Debug Queries:
- Use
toSql()
or query logging:php echo Post::where('is_published', true)->toSql(); DB::enableQueryLog(); dd(DB::getQueryLog());
- Keep Models Clean:
- Move complex query logic to repositories or query scopes in Eloquent.
- Test Queries:
- Test in a local environment with
php artisan tinker
or unit tests.
Debugging Query Builder and Eloquent
- Query Errors:
- Check
.env
database settings and test withDB::connection()->getPdo()
. - Verify table/column names in Query Builder or model configuration.
- Eloquent Model Issues:
- Ensure
$table
,$primaryKey
, and$fillable
are correct. - Check relationship foreign keys (e.g.,
user_id
exists). - Performance Bottlenecks:
- Use Laravel Debugbar or query logging to identify slow queries.
- Optimize with indexes or eager loading.
- Logs:
- Check
storage/logs/laravel.log
for errors.
Example: Query Builder vs. Eloquent
Scenario: Fetch published posts with user names
Database:
posts
:id
,title
,content
,is_published
,user_id
,created_at
,updated_at
.users
:id
,name
,email
.
Query Builder:
$posts = DB::table('posts')
->join('users', 'posts.user_id', '=', 'users.id')
->where('posts.is_published', true)
->select('posts.title', 'posts.content', 'users.name as user_name')
->orderBy('posts.created_at', 'desc')
->get();
// Access data
foreach ($posts as $post) {
echo "{$post->title} by {$post->user_name}";
}
Eloquent:
// Post model
class Post extends Model
{
protected $fillable = ['title', 'content', 'is_published', 'user_id'];
public function user()
{
return $this->belongsTo(User::class);
}
public function scopePublished($query)
{
return $query->where('is_published', true);
}
}
// Query
$posts = Post::published()
->with('user')
->orderBy('created_at', 'desc')
->get();
// Access data
foreach ($posts as $post) {
echo "{$post->title} by {$post->user->name}";
}
Comparison:
- Query Builder: More verbose for joins, returns plain objects, manual timestamp handling.
- Eloquent: Cleaner syntax, model instances, automatic relationships, reusable scopes.
Conclusion
Laravel’s Query Builder and Eloquent ORM are both powerful tools for database interactions, but they serve different purposes. Query Builder is lightweight and ideal for simple, performance-critical, or non-model queries, while Eloquent excels in model-driven applications with relationships and complex logic. By understanding their strengths and combining them when needed, you can build efficient and maintainable Laravel applications.
Next Steps:
- Experiment with Query Builder:
DB::table('posts')->get()
. - Create an Eloquent model and define relationships.
- Test queries in
php artisan tinker
.
For deeper insights, explore Laravel’s official documentation or connect with the Laravel community on platforms like X. Start querying your database effectively today!
Laravel Query Builder vs Eloquent Example
This artifact demonstrates fetching published posts with user names using both Query Builder and Eloquent.
Migration (database/migrations/2025_05_19_123456_create_posts_table.php
)
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title', 255);
$table->text('content');
$table->boolean('is_published')->default(false);
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
Model (app/Models/Post.php
)
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'content', 'is_published', 'user_id'];
public function user()
{
return $this->belongsTo(User::class);
}
public function scopePublished($query)
{
return $query->where('is_published', true);
}
}
Query Builder Example
use Illuminate\Support\Facades\DB;
$posts = DB::table('posts')
->join('users', 'posts.user_id', '=', 'users.id')
->where('posts.is_published', true)
->select('posts.title', 'posts.content', 'users.name as user_name')
->orderBy('posts.created_at', 'desc')
->get();
foreach ($posts as $post) {
echo "{$post->title} by {$post->user_name}\n";
}
Eloquent Example
use App\Models\Post;
$posts = Post::published()
->with('user')
->orderBy('created_at', 'desc')
->get();
foreach ($posts as $post) {
echo "{$post->title} by {$post->user->name}\n";
}
Usage
- Ensure your
.env
file is configured and theusers
table exists. - Save the migration and model files.
- Run the migration:
php artisan migrate
- Seed sample data (e.g., using a factory or seeder).
- Test both queries in a controller or
php artisan tinker
.
This example highlights the procedural, table-focused approach of Query Builder versus the object-oriented, model-driven approach of Eloquent, showcasing their syntax and handling of relationships.