MySQL按用户分组取MAX(created_at)无法返回对应正确元组的问题求助
嘿,我来帮你捋清楚这个问题的根源和解决办法!
问题原因
你遇到的这个问题,核心在于MySQL的非标准GROUP BY行为(当ONLY_FULL_GROUP_BY模式未开启时)。
看你写的子查询:
SELECT max(mb.created_at), message_id FROM `messages` mb WHERE mb.created_at <= '2021-07-24' group by user_id
这里message_id既没有出现在GROUP BY子句中,也没有被聚合函数(比如MAX、MIN)包裹。在默认的宽松模式下,MySQL不会报错,但它会随机从该用户的分组记录里挑选一个message_id返回,而不是和MAX(created_at)对应的那条记录的ID——这就是为什么user_id=1时,结果里的message_id是5,而不是你预期的6。
解决方案
这里有几种靠谱的方法,能精准获取每个用户最后创建的那条完整消息:
方法1:关联子查询(兼容性好,适合所有MySQL版本)
先通过子查询算出每个用户的最大created_at,再和原表关联匹配对应的完整记录:
SELECT ma.* FROM messages ma INNER JOIN ( SELECT user_id, MAX(created_at) AS latest_created FROM messages WHERE created_at <= '2021-07-24' GROUP BY user_id ) mc ON ma.user_id = mc.user_id AND ma.created_at = mc.latest_created
这个方法逻辑直白,能确保拿到和最新创建时间对应的那条记录。
方法2:窗口函数(MySQL 8.0及以上版本推荐)
如果你的MySQL是8.0或更新的版本,用ROW_NUMBER()窗口函数会更简洁高效:
SELECT message_id, user_id, date_sent, created_at FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS row_rank FROM messages WHERE created_at <= '2021-07-24' ) ranked_messages WHERE row_rank = 1
这里PARTITION BY user_id按用户分组,ORDER BY created_at DESC把每个用户最新的消息排在第一位,最后筛选row_rank=1的记录就是你要的结果。
方法3:单用户场景的简化写法
如果只是查询单个用户的最新消息,可以直接用ORDER BY + LIMIT:
SELECT * FROM messages WHERE user_id = 1 AND created_at <= '2021-07-24' ORDER BY created_at DESC LIMIT 1
额外提示
建议开启ONLY_FULL_GROUP_BY SQL模式,这样MySQL会对不符合标准SQL规范的GROUP BY语句直接报错,避免再出现这种“随机返回字段值”的意外情况。临时开启可以执行:
SET sql_mode = 'ONLY_FULL_GROUP_BY';
如果需要永久生效,要修改MySQL的配置文件(my.cnf或my.ini),在[mysqld]段添加sql_mode = ONLY_FULL_GROUP_BY后重启服务。
内容的提问来源于stack exchange,提问作者Rob

