You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel HasOne关联预加载全部模型,如何优化仅加载最早未完成预约?

优化Laravel关联查询:仅加载每个模型的最早未完成预约

问题场景

原本SomeModel与Appointment是HasMany关联,为获取每个SomeModel对应的最早未完成预约,定义了HasOne关联方法。该方法功能正常,但预加载时会通过WHERE IN查询所有符合条件的Appointment记录(本地测试加载100+条),而非每个模型仅加载一条目标数据。

当前关联代码

public function nextAppointment(): HasOne
{
    return $this->hasOne(Appointment::class, 'some_model_id')
        ->whereNull('completed_at')
        ->oldest('starts_at');
}

生成的SQL

select * from "appointments" where "completed_at" is null and "appointments"."some_model_id" in ('00cb2664-2aec-4600-a3ca-873dbb5f81f3', '04b62cc7-9ec7-4613-af13-3bc53f9b3538', '109fce77-0fd4-4478-b30d-0c95468d1037', '11b28a27-020d-46f8-b498-51ec152192a2', '11dee373-ec59-4804-897e-2bc5094a3785', '15614002-2414-488d-b639-6410f1c32004', '19d43627-10c5-4708-861d-9394a6ee9b69', 'fffc6b29-fbac-4b6a-80bf-781a2e720c38') order by "starts_at" asc

控制器代码

return SomeModel::with(['nextAppointment'])->paginate(request()->input('size', 10));

优化方案

核心思路是在数据库层面完成筛选,而非查询所有记录后在内存中匹配,从而减少内存占用并提升查询效率。

方法一:子查询匹配最早预约时间

通过子查询找到每个some_model_id对应的最早未完成预约时间,再关联筛选出对应记录:

public function nextAppointment(): HasOne
{
    $subquery = Appointment::selectRaw('MIN(starts_at)')
        ->whereColumn('some_model_id', 'appointments.some_model_id')
        ->whereNull('completed_at');

    return $this->hasOne(Appointment::class, 'some_model_id')
        ->whereNull('completed_at')
        ->whereColumn('starts_at', $subquery);
}

也可使用原生SQL写法:

public function nextAppointment(): HasOne
{
    return $this->hasOne(Appointment::class, 'some_model_id')
        ->whereNull('completed_at')
        ->whereRaw('starts_at = (SELECT MIN(starts_at) FROM appointments WHERE some_model_id = appointments.some_model_id AND completed_at IS NULL)');
}

方法二:窗口函数筛选分组第一条

利用MySQL 8.0+支持的窗口函数ROW_NUMBER(),按some_model_id分组并按starts_at排序,取每组的第一条记录:

public function nextAppointment(): HasOne
{
    return $this->hasOne(Appointment::class, 'some_model_id')
        ->selectRaw('appointments.*, ROW_NUMBER() OVER (PARTITION BY some_model_id ORDER BY starts_at ASC) as rn')
        ->whereNull('completed_at')
        ->having('rn', 1);
}

原理说明

原方案中,Laravel预加载HasOne关联时,会先查询所有主模型ID,再通过WHERE IN拉取所有符合条件的关联记录,最后在内存中为每个主模型匹配第一条。优化方案则直接在数据库层面完成分组筛选,返回结果仅包含每个主模型对应的目标预约记录,大幅减少内存占用。

内容的提问来源于stack exchange,提问作者Jaquarh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 04:51:12