group by无法直接获取每组最新一条完整记录,因非聚合字段不保证与max()同源;应通过子查询+join匹配(category, created_at)组合,并处理null、重复及精度问题。

GROUP BY 无法直接拿到“每组最新一条”的完整记录
很多人以为 GROUP BY category 配合 MAX(created_at) 就能取出最新那条的全部字段,但这是错的。MySQL(尤其是 5.7 及以前)和大多数 SQL 引擎不保证非聚合字段与 MAX() 对应同一行——你得到的 id、title 很可能是随机某条,不是最新那条的。
用 IN 子查询匹配 (id, created_at) 组合最稳妥
核心思路:先查出每个分类下最大的时间戳,再用 (category, created_at) 这个组合去原表里精确匹配。前提是 created_at 在同一分类内不重复;如果可能重复,得加 id 或其他唯一字段兜底。
实操建议:
- 确保
(category, created_at)上有联合索引,否则子查询会全表扫描 - 如果
created_at有重复,改用(category, created_at, id)组合,并在子查询里用ORDER BY created_at DESC, id DESC LIMIT 1拿最大 ID - PostgreSQL/SQL Server 可用窗口函数更简洁,但 MySQL 5.7 必须靠子查询
SELECT t1.* FROM posts t1 WHERE (t1.category, t1.created_at) IN ( SELECT category, MAX(created_at) FROM posts GROUP BY category );
IN 子查询在 NULL 或重复时间时会失效
如果 created_at 允许为 NULL,MAX(created_at) 会跳过它,而 (category, NULL) 无法被 IN 匹配——整条记录就丢了。重复时间同理:多个记录共享同一 MAX(created_at),IN 会返回所有这些记录,不是“一条”。
应对方式:
- 过滤掉
created_at IS NULL:子查询和主查询都加上WHERE created_at IS NOT NULL - 要严格保“一条”,必须引入确定性排序依据,比如
id:子查询改用SELECT category, MAX(id) FROM posts GROUP BY category(假设id递增且与时间强相关) - 或者用
ROW_NUMBER()窗口函数(MySQL 8.0+)替代,更可控
性能差?别让子查询反复执行
上面的 IN 写法在某些旧版 MySQL 中,子查询可能对每行外层记录都执行一次(即“相关子查询”),实际变成 O(n²)。虽然优化器通常会自动物化子查询结果,但不能赌。
更稳的做法是用 JOIN 替代 IN:
SELECT t1.* FROM posts t1 INNER JOIN ( SELECT category, MAX(created_at) AS max_time FROM posts GROUP BY category ) t2 ON t1.category = t2.category AND t1.created_at = t2.max_time;
这个写法明确告诉优化器:先算好中间结果,再关联——可读性、可控性和兼容性都更好。
真正容易被忽略的是时间精度和时区。如果 created_at 是 DATETIME 且没设时区,跨服务器同步或夏令时切换时,MAX() 可能选错行。上线前务必确认字段类型是 TIMESTAMP 或显式带时区的 DATETIME。











