Subqueries — whereIn with subquery, fromSub()
Concept
Subqueries embed one query inside another, allowing complex data access patterns that would otherwise require multiple round-trips or application-level merging.
whereIn with subquery: Filter rows where a column matches values from another query.
->whereIn('user_id', fn($q) => $q->from('admins')->select('user_id'))fromSub(Closure|Builder, string $alias): Use a subquery as the table in FROM. Allows operating on pre-aggregated data.
selectSub(Closure|Builder, string $alias): Embed a subquery as a selected column. The subquery must return a single row/column.
whereExists(Closure): Correlated subquery returning WHERE EXISTS (SELECT ...). More efficient than whereIn for large sets because it short-circuits.
whereNotExists(Closure): Inverse — rows where the subquery returns no results.
Correlated subqueries: Subqueries that reference the outer query's table. Used in whereExists, selectSub, and whereRaw. The inner query runs for each outer row.
Performance notes:
whereIn(subquery)with many values: the DB computes the inner query once, then filters. Usually fine.whereExists: More efficient when the inner set is large, because it stops at first match.selectSub(correlated): Runs once per outer row — can be N+1 in SQL. Use with caution on large datasets; consider JOINs instead.
Code Example
<?php
use Illuminate\Support\Facades\DB;
// whereIn with subquery — users in the 'premium' plan
$premiumUsers = DB::table('users')
->whereIn('id', function($q) {
$q->from('subscriptions')
->select('user_id')
->where('plan', 'premium')
->whereNull('cancelled_at');
})
->get();
// whereNotIn with subquery — users who have never ordered
$neverOrdered = DB::table('users')
->whereNotIn('id', function($q) {
$q->from('orders')->select('user_id')->distinct();
})
->get();
// whereExists — users with at least one completed order (efficient)
$buyers = DB::table('users')
->whereExists(function($q) {
$q->from('orders')
->whereColumn('orders.user_id', 'users.id')
->where('orders.status', 'completed');
})
->get();
// fromSub — query a derived table
$averagesByCategory = DB::table(function($q) {
$q->from('products')
->select('category', DB::raw('AVG(price) as avg_price'))
->groupBy('category');
}, 'category_averages')
->where('avg_price', '>', 50)
->get();
// selectSub — embed correlated subquery as column
$users = DB::table('users')
->select('id', 'name')
->selectSub(function($q) {
$q->from('orders')
->selectRaw('COUNT(*)')
->whereColumn('user_id', 'users.id');
}, 'order_count')
->selectSub(function($q) {
$q->from('orders')
->selectRaw('SUM(total)')
->whereColumn('user_id', 'users.id');
}, 'total_spent')
->get();
// Caution: 2 correlated subqueries run per user row