Laravel查询构建器按p_id分组取每组前2条数据报错求助
首先得说你原来的写法有两个核心问题:一是触发了MySQL严格模式下的1055报错,二是逻辑上根本达不到「每组取前2条」的目的。
先解释报错原因:MySQL现在默认开启严格模式,GROUP BY子句要求SELECT里的字段要么是分组字段(这里就是p_id),要么是用聚合函数处理过的字段(比如MAX(time))。你原来的SELECT直接放了u_id和time,既不是分组字段也没加聚合函数,数据库自然会抛出语法违规的错误。
而且就算关掉严格模式,你的逻辑也走不通——ORDER BY在GROUP BY之前执行的话,分组操作只会把每组的所有行合并成一行,根本没法保留组内的前2条数据,完全不符合你的需求。
下面给你两种可行的解决方案,根据你的MySQL版本选择就行:
方案一:用窗口函数(推荐,MySQL 8.0+支持)
窗口函数是处理这类「分组取前N条」需求的最优解,用ROW_NUMBER()就能轻松实现:
// 先给每组数据按time升序编号,再筛选出编号<=2的行 $result = DB::table('a16s') ->select( 'p_id', 'u_id', 'time', DB::raw('ROW_NUMBER() OVER (PARTITION BY p_id ORDER BY time ASC) as row_num') ) ->having('row_num', '<=', 2) ->orderBy('p_id', 'asc') ->orderBy('time', 'asc') ->get(); // 如果不需要返回row_num字段,再做个简单处理 $finalResult = $result->map(function ($item) { unset($item->row_num); return $item; }); echo '<pre>'; print_r($finalResult);
这里的PARTITION BY p_id相当于按p_id分组,ORDER BY time ASC是每组内按时间从小到大排序,ROW_NUMBER()会给每组里的行从1开始编号,最后筛选出编号≤2的行,正好就是你要的每组前2条数据。
方案二:子查询方式(兼容MySQL 5.7及更早版本)
如果你的MySQL版本还不支持窗口函数,可以用子查询统计每行在组内的排名:
$result = DB::table('a16s as main') ->select('main.p_id', 'main.u_id', 'main.time') ->whereRaw('(SELECT COUNT(*) FROM a16s as sub WHERE sub.p_id = main.p_id AND sub.time <= main.time) <= 2') ->orderBy('main.p_id', 'asc') ->orderBy('main.time', 'asc') ->get(); echo '<pre>'; print_r($result);
这个思路是:对于每一行,统计同一p_id下time小于等于当前行time的行数,如果这个行数≤2,说明当前行是组内time排前2的(因为time越小,统计出来的行数越少)。
两种方案都能得到你期望的结果:
p_id u_id time
1 2 0
1 1 1
2 1 0
2 3 1
3 4 8
3 2 11
内容的提问来源于stack exchange,提问作者robspin

