Laravel9 Eloquent orderBy子查询报错 按用户取2条关联Post
实现方案
你遇到的两个问题都是对Eloquent对应能力的逻辑边界理解偏差导致的,直接按下面的步骤实现即可,全程仅产生3条SQL查询,完全无N+1问题。
问题根因说明
- 排序场景使用的子查询属于SQL标量子查询,本身要求必须返回1行1列的单个值作为排序权重,所以保留
limit(1)是完全正确的,这个子查询的作用只是取每个用户最新已发布Post的更新时间用来排序,和你需要预加载2条Post的需求没有关联,不需要修改这个子查询的limit配置,之前的1242报错就是因为子查询返回多行违反了标量子查询的规则。 - 原生
with()预加载闭包中添加的take()是对整个关联查询的结果集做全局条数限制,不会自动按外键(user_id)分组为每个父模型单独限制条数,所以才会出现总共只返回2条Post的问题。
可直接运行的代码
第一步:先在User模型中定义复用的已发布Post关联
// app/Models/User.php public function publishedPosts() { return $this->hasMany(Post::class) ->where('state', PostStateEnum::PUBLISHED) ->latest('updated_at'); }
第二步:业务查询逻辑
// 第1条SQL:按规则取排序后前4个用户的ID $userIds = User::query() ->whereHas('publishedPosts') ->orderByDesc( Post::select('updated_at') ->where('state', PostStateEnum::PUBLISHED) ->whereColumn('user_id', 'users.id') ->latest('updated_at') ->limit(1) ) ->take(4) ->pluck('id'); // 第2条SQL:查询4个目标用户的基础信息,保持排序顺序 $users = User::whereIn('id', $userIds) ->orderByRaw("FIELD(id, " . $userIds->join(',') . ")") ->get(); // 第3条SQL:一次性查询所有用户对应的最新2条已发布Post $posts = Post::query() ->whereIn('user_id', $userIds) ->where('state', PostStateEnum::PUBLISHED) ->latest('updated_at') ->whereRaw(' (SELECT COUNT(*) FROM posts AS sub_p WHERE sub_p.user_id = posts.user_id AND sub_p.state = ? AND sub_p.updated_at >= posts.updated_at) <= 2 ', [PostStateEnum::PUBLISHED]) ->get(); // 手动将Post挂载到User模型,无额外查询 $users->each(fn ($user) => $user->setRelation('posts', $posts->where('user_id', $user->id)->values()) );
逻辑说明
- 全程仅3次数据库查询,没有循环查库的N+1问题,性能和原生with预加载一致。
- Post查询中的相关子查询会自动按用户分组,只返回每个用户最新的2条已发布Post,不会拉取冗余数据。
- 用
setRelation手动挂载关联的方式,和原生with预加载的结果结构完全一致,前端/接口层不需要做任何适配。 - 如果你的数据库版本支持MySQL 8.0+ / PostgreSQL,也可以把Post查询里的相关子查询替换为窗口函数
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC)做行号过滤,大表场景下性能会更好。
内容的提问来源于stack exchange,提问作者Andre
相关产品推荐
相关产品推荐

