如何用JOOQ/SQL按外键barId筛选Foo表中最小time的全字段数据?
这问题我太熟悉了!你碰到的错误是因为SQL的分组规则:当你用GROUP BY的时候,SELECT列表里的列要么得是GROUP BY里的分组字段,要么得用聚合函数(比如MIN()、MAX())处理。但你要的是每个barId分组里整个行的数据,直接GROUP BY barId肯定满足不了,因为id和time既不在分组字段里,也没被聚合,数据库不知道该返回哪一行的这两个值。
下面给你几个实用的解决办法,适配不同的数据库场景:
方法一:通用窗口函数法(推荐,大部分数据库支持)
用ROW_NUMBER()窗口函数给每个barId分组里的行按time排序,然后取排序第一的行:
SELECT id, time, barId FROM ( SELECT id, time, barId, -- 按barId分组,每组内按time升序排,给行编号 ROW_NUMBER() OVER (PARTITION BY barId ORDER BY time ASC) AS row_num FROM Foo ) ranked_foo WHERE row_num = 1;
针对你的示例数据,这个查询会给barId=1的两行编号1和2(对应10am和11am),barId=2的两行编号1和2(对应9am和10am),筛选row_num=1就正好拿到行1和4,完全符合预期。
如果你的数据里存在同一个barId有多个相同的最小time,想要返回所有这些行的话,可以把ROW_NUMBER()换成RANK()或者DENSE_RANK(),这样相同最小时间的行都会得到编号1。
方法二:子查询关联法
先通过子查询找出每个barId对应的最小time,再和原表关联匹配出完整行:
SELECT f.id, f.time, f.barId FROM Foo f INNER JOIN ( -- 先拿到每个barId的最小时间 SELECT barId, MIN(time) AS min_time FROM Foo GROUP BY barId ) min_times ON f.barId = min_times.barId AND f.time = min_times.min_time;
这个方法逻辑直观,同样能得到你要的结果。和窗口函数不同的是,它会返回所有time等于该barId最小时间的行,适合需要保留重复最小时间行的场景。
方法三:PostgreSQL专属简洁写法
如果你用的是PostgreSQL,可以用DISTINCT ON语法,一行搞定:
SELECT DISTINCT ON (barId) id, time, barId FROM Foo ORDER BY barId, time ASC;
DISTINCT ON (barId)会确保每个barId只返回一行,配合ORDER BY barId, time ASC,就会取每个分组里time最小的那一行,非常简洁。
再说说你遇到的错误原因
当你尝试直接GROUP BY barId并SELECT id, time, barId时,数据库无法确定要返回该分组下哪一个id和time——毕竟一个barId对应多行数据。所以必须要么把id也加入GROUP BY(但这样就变成按barId+id分组,不符合你的需求),要么对id用聚合函数(比如MIN(id)),但这样拿到的id不一定是time最小的那行的id,这就是为什么会抛出那个错误。
内容的提问来源于stack exchange,提问作者BossmanT

