Laravel 高级 Eloquent 查询:用 withCount、whereHas 与子查询把统计留在数据库层

test12026-10-173 次阅读

问题

列表页常需要“每个用户有多少订单”“筛选出有未完成订单的用户”。如果在 PHP 里循环查,会写出 N+1 甚至更糟的查询。把这些统计交给数据库,用 Eloquent 的聚合方法解决。

关联计数:withCount

use App\Models\User;

// 每个用户带一个 orders_count 字段,一条 SQL 搞定
$users = User::withCount('orders')->get();

foreach ($users as $user) {
    echo $user->name . ':' . $user->orders_count;
}

// 带条件的计数
$users = User::withCount(['orders' => function ($q) {
    $q->where('status', 'paid');
}])->get();

withCount 会在主查询里用子查询算出数量,结果作为 orders_count 挂在模型上,不再为每个人单独发 SQL。

用 whereHas 筛选存在性

use App\Models\User;

// 只要“下过订单”的用户
$paidUsers = User::whereHas('orders', function ($q) {
    $q->where('status', 'paid');
})->get();

// 反过来:没有任何未完成订单的用户
$clean = User::whereDoesntHave('orders', function ($q) {
    $q->where('status', 'pending');
})->get();

子查询:select 里嵌一个聚合

use Illuminate\Support\Facades\DB;
use App\Models\Order;

$latest = Order::select('user_id')
    ->selectRaw('MAX(created_at) as last_order_at')
    ->groupBy('user_id');

$users = User::query()
    ->joinSub($latest, 'latest', function ($join) {
        $join->on('users.id', '=', 'latest.user_id');
    })
    ->orderByDesc('latest.last_order_at')
    ->paginate(20);

工程建议

  • 列表统计优先用 withCount / withSum,避免循环里查库。
  • whereHas 的闭包里加索引友好的条件,子查询越轻量越好。
  • 复杂聚合用 joinSub 把子查询当成临时表,可读性比手写 DB::raw 好。
  • 给关联外键和状态字段加索引,否则 join 与 exists 会在数据量大时变慢。
T

test1

文章作者

问题 列表页常需要“每个用户有多少订单”“筛选出有未完成订单的用户”。如果在 PHP 里循环查,会写出 N+1 甚至更...

分类
技术
发布时间
2026-10-17
字数
约 1343 字
阅读
3 次