Laravel: Merge two query builders
Stefan Bogdanescu
Founder & Senior Architect · 2026-06-29
Laravel: Merging Two Query Builders for Efficient Data Retrieval
As a senior developer working with Laravel, one of the most common challenges we face is efficiently combining data from multiple related relationships into a single, coherent result set. In our scenario—where a user needs to see courses they have direct access to (free) and courses they have pending access to (invited)—we need to perform this merge using the database as efficiently as possible.
This post will dive into how you can successfully merge two distinct Eloquent Query Builder instances, leveraging Laravel's features to achieve a seamless data retrieval process.
The Scenario: Combining Public and Invited Courses
We have two primary data sources that a user needs to aggregate:
- Public Courses: Courses where
invite_onlyis false (direct access). - Invited Courses: Courses for which the user has an approved entry in the pivot table (
course_user).
The goal is to implement a method on the User model that combines these two sets into one list, ideally minimizing database hits.
Database Schema Context
To understand the query building process, let's look at our hypothetical tables:
courses Table:
| id | title | invite_only |
|---|---|---|
| 1 | free course | 0 |
| 2 | private course | 1 |
course_user Pivot Table:
| id | user_id | course_id | approved | declined |
|---|---|---|---|---|
| 1 | 3 | 2 | 1 | 0 |
The Strategy: Merging Query Builder Instances
The core of the problem lies in combining two different ways of querying related data: one based on a direct foreign key (hasMany) and one based on a many-to-many pivot table relationship (belongsToMany). While Laravel's merge() method exists on query builders, directly merging these complex relationships can sometimes lead to unexpected Cartesian products or performance bottlenecks if not structured correctly.
The most robust approach is often to fetch the IDs from both sets separately and then use Eloquent's collection methods for a final merge in PHP memory, or, alternatively, structure the initial queries to perform an efficient UNION operation at the database level. However, since you specifically asked to merge the query builders, we will explore that method first, while also showing the most practical alternative.
Implementation using Eloquent Relationships
Let's refine your model implementation to structure these two separate queries cleanly:
class User extends Model
{
public function myCourses(): \Illuminate\Database\Eloquent\Collection
{
// 1. Get all publicly available courses (invite_only = false)
$publicCourses = $this->publicCourses; // This returns a Query Builder instance
// 2. Get all invited/approved courses
$invitedCourses = $this->invitedCourses; // This returns a Query Builder instance
// 3. Merge the results
return $publicCourses->merge($invitedCourses);
}
public function publicCourses(): \Illuminate\Database\Eloquent\Builder
{
return $this
->hasMany('App\Course')
->where('invite_only', false);
}
public function invitedCourses(): \Illuminate\Database\Eloquent\Builder
{
return $this
->belongsToMany("\App\Course")
->using('App\CourseUser')
->wherePivot('approved', 1);
}
}
Analysis and Best Practices
When you call $publicCourses->merge($invitedCourses), Eloquent attempts to combine the resulting collections. For this specific scenario, where publicCourses returns Course models and invitedCourses returns Course models (via the pivot), the merge operation works effectively by combining the underlying result sets into a single collection of course models.
Why This Works (and When Not To)
This approach is clean because it keeps the filtering logic separate:
- The public query handles simple constraints (
invite_only). - The invited query handles relationship constraints (
approved = 1).
Performance Consideration: While merge() successfully combines the results, be mindful of complex joins. If your relationships involved multiple nested joins or complex aggregations, executing two separate queries and merging them in PHP might be more readable and sometimes allow the database optimizer to perform better than a single, highly complex UNION query.
For truly massive datasets, consider using Laravel's Eloquent features like Eager Loading (with()) first, and only resort to this type of direct query builder merging when you are explicitly filtering disparate sets that must be combined before final presentation. For deep dives into optimizing database interaction in the Laravel ecosystem, I highly recommend exploring resources from Laravel Company.
Conclusion
By separating the logic for fetching public courses and invited courses into distinct relationships on the User model, we create reusable query builders. The use of $publicCourses->merge($invitedCourses) provides an elegant way to combine these two filtered sets into a single collection, fulfilling the requirement using logical operators provided by the Query Builder. This method keeps your code clean, maintainable, and efficient for handling complex user permissions.