MySQL 5.7+中GROUP BY后不能直接选非聚合字段,需用子查询先求各分类MAX(created_at),再JOIN取整行;存在时间重复时应加id筛选或改用窗口函数ROW_NUMBER();注意NULL和时区影响。

用子查询先找出每个分类的MAX时间
直接在 GROUP BY 后选非聚合字段会报错,所以不能写 SELECT category, id, MAX(created_at) FROM logs GROUP BY category —— MySQL 5.7+ 默认 SQL mode 下会拒绝这种“不明确”的字段混用。正确做法是先用子查询算出每个 category 对应的最新时间点:
SELECT category, MAX(created_at) AS max_time FROM logs GROUP BY category
这一步只负责“定位时间”,不涉及具体哪条记录。
用JOIN关联原始表取完整行数据
拿到每个分类的 max_time 后,必须和原表再次关联才能取出对应那条完整记录。注意:仅靠 created_at = max_time 可能撞上同分类下多条记录时间完全一致的情况,导致重复返回。稳妥做法是加上唯一字段(如 id)二次筛选:
- 如果
created_at在同一分类内**严格唯一**,可用JOIN ... ON t1.category = t2.category AND t1.created_at = t2.max_time - 如果存在时间重复,建议改用窗口函数(见下一条),或在 JOIN 后加
ROW_NUMBER() OVER (PARTITION BY category ORDER BY created_at DESC, id DESC)预处理 - MySQL 8.0+ 或 PostgreSQL 推荐直接走窗口函数,避免自连接性能抖动
MySQL 8.0+ 推荐用 ROW_NUMBER() 替代 MAX + JOIN
当分类量大、日志表行数多时,MAX 子查询 + JOIN 会产生临时表和全表扫描,而窗口函数一次扫描即可完成排序与标记:
SELECT category, id, created_at, content
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY created_at DESC, id DESC
) AS rn
FROM logs
) ranked
WHERE rn = 1;
这里 ORDER BY created_at DESC, id DESC 是关键:时间相同时,取 id 更大的那条(通常代表插入更晚),避免随机返回。
注意 NULL 和时区对 MAX(created_at) 的影响
MAX() 会自动忽略 NULL 值,但如果某分类所有 created_at 都是 NULL,该分类不会出现在结果中——这点容易被忽略,尤其当业务允许未设置时间戳时。另外,若数据库时区与应用时区不一致,MAX(created_at) 返回的是数据库本地时间,可能和你预期的“最新”不符。检查方式:
- 执行
SELECT @@time_zone, NOW(), SYSDATE()确认当前会话时区 - 确保
created_at字段类型是DATETIME或TIMESTAMP,且写入时已做时区归一化(例如全转为 UTC 存储) - 必要时在子查询里用
CONVERT_TZ()统一比较基准
真正难的不是写出 MAX + JOIN,而是确认时间字段是否干净、是否可比、是否覆盖全部业务场景。

















