group by无法直接获取每组最新完整记录,因不保留行序且max()仅返回时间值;需用关联子查询或窗口函数实现。

为什么直接用 GROUP BY 拿不到每组最新一条
因为 GROUP BY 本身不保留原始行顺序,MAX(created_at) 只能返回时间值,没法把整条记录(比如 id、status)一起带出来。你用 SELECT *, MAX(created_at) FROM t GROUP BY category 会报错或返回不可控的行——MySQL 5.7+ 默认开启 sql_mode=only_full_group_by,直接拒绝这种写法。
关联子查询怎么写才安全可靠
核心思路是:对主表每一行,用子查询查出它所属分组的最新时间,再让主表这行的 created_at 等于那个最大值。这样既避免了 GROUP BY 的歧义,又不用窗口函数(兼容 MySQL 5.6/5.7 等老版本)。
常见错误写法:WHERE created_at = (SELECT MAX(created_at) FROM t t2 WHERE t2.category = t.category) —— 如果同一组有两条记录时间完全相同,就会返回多条,不是“每组一条”。
更稳妥的做法是加上唯一字段二次判重:
SELECT t1.*
FROM orders t1
WHERE t1.created_at = (
SELECT MAX(t2.created_at)
FROM orders t2
WHERE t2.category = t1.category
)
AND t1.id = (
SELECT id FROM orders t3
WHERE t3.category = t1.category
AND t3.created_at = t1.created_at
ORDER BY t3.id DESC
LIMIT 1
);
- 第一层子查询确保只取本组最大时间
- 第二层子查询在时间相同的情况下,靠
id(或自增主键)兜底,保证结果确定性 - 必须给
(category, created_at)或(category, created_at, id)加联合索引,否则性能极差
什么时候该换用窗口函数而不是关联子查询
如果你用的是 MySQL 8.0+、PostgreSQL 或 SQL Server,优先用 ROW_NUMBER()。它逻辑清晰、性能好、天然去重。
典型写法:
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY created_at DESC, id DESC
) AS rn
FROM orders
) ranked
WHERE rn = 1;
-
PARTITION BY category对应“分组” -
ORDER BY created_at DESC, id DESC确保时间最新且冲突时取 id 最大者(即最后插入的那条) - 没有子查询嵌套,执行计划更可控;加索引后基本能走
index range scan - 注意:SQLite 3.25+ 也支持,但旧版不支持;MariaDB 10.2+ 支持有限
容易被忽略的边界情况
真实业务里,created_at 可能为 NULL,或者精度到秒但应用写入时钟不同步,导致“最新”判断失真。
- NULL 值会被
MAX()忽略,如果某组全为 NULL,子查询返回 NULL,外层= NULL判断永远为 false,该组记录直接消失 - 如果时间字段是
DATETIME且没设默认值,旧数据可能全是'0000-00-00 00:00:00',MAX()会把它当最小值处理,但比较时又可能被当成 NULL - 用
id排序兜底的前提是它严格递增且与业务“最新”语义一致;如果用 UUID 或雪花 ID,就得换用created_at+updated_at组合排序
上线前务必用实际数据验证 NULL、零值、重复时间这三类 case 是否覆盖完整。










