最稳方案是用row_number()窗口函数按组排序后取序号为1的行。需明确排序字段(如created_at)、加id desc二级排序防并列,且必须用子查询或cte包裹,因窗口函数不能直接用于where。

用 ROW_NUMBER() 窗口函数取每组最新记录最稳
直接在 GROUP BY 里用 MAX(created_at) 再关联原表,看似简单,实际容易拿错整行数据(比如时间最新但其他字段对不上)。真正可靠的做法是用窗口函数给每组按时间排序,再筛序号为 1 的行。
关键点在于:必须明确「最新」依据哪个字段(通常是 created_at 或 id),且该字段在组内需有足够区分度。如果存在时间相同的情况,建议加上 id DESC 做二级排序,避免结果不稳定。
-
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC)—— 注意PARTITION BY是分组字段,ORDER BY决定谁算“最新” - 不能用
RANK()或DENSE_RANK(),它们会对并列值分配相同序号,导致取多条 - 子查询或 CTE 是必需的,因为窗口函数不能直接出现在
WHERE中
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY created_at DESC, id DESC
) AS rn
FROM orders
) t WHERE rn = 1;
MySQL 8.0+ 可用 LATERAL(JOIN LATERAL)简化逻辑
如果你用的是 MySQL 8.0.14+,LATERAL 能让子查询依赖外层分组,写起来更贴近直觉,也避免了全表加窗口函数的开销。
它本质是为每个分组执行一次相关子查询,所以性能取决于分组数量和子查询效率。如果分组极多(比如百万级用户),可能比窗口函数慢;但分组少、单组数据多时,反而更快——因为它不用对全表排序。
- 必须搭配
JOIN使用,不能单独FROM - 子查询里必须用
LIMIT 1,否则报错 - 注意别名作用域:
u.id在子查询里可直接引用
SELECT u.*, o.* FROM users u JOIN LATERAL ( SELECT * FROM orders o2 WHERE o2.user_id = u.id ORDER BY o2.created_at DESC, o2.id DESC LIMIT 1 ) o ON TRUE;
旧版 MySQL(5.7)只能靠相关子查询或自连接
没有窗口函数和 LATERAL 时,最容易踩的坑是用 IN (SELECT MAX(...)) 导致重复匹配(多个订单同为最大时间就全被捞出来)。正确做法是确保「最新」能唯一定位一行。
推荐自连接方案:让每条记录去比同一组里有没有更新的记录,没有则留下。虽然写法略绕,但语义清晰、兼容性好、结果确定。
- 自连接条件要同时匹配分组字段和「更新」逻辑:
t1.user_id = t2.user_id AND t1.created_at - 必须用
LEFT JOIN+WHERE t2.id IS NULL找出没被超越的记录 - 如果
created_at允许 NULL,得额外过滤,否则 NULL 会干扰比较结果
SELECT t1.* FROM orders t1 LEFT JOIN orders t2 ON t1.user_id = t2.user_id AND (t1.created_at <h3>PostgreSQL 里 DISTINCT ON 是个快捷选项</h3><p><code>DISTINCT ON</code> 不是标准 SQL,但 PostgreSQL 支持得很好,语法简洁,性能通常优于窗口函数——尤其当只需要每组头一条且排序字段有索引时。</p><p>它只保证「按指定字段去重后的第一条」,不保证是否真按时间最新。所以必须配合 <code>ORDER BY</code> 显式排序,且 <code>DISTINCT ON</code> 字段必须是 <code>ORDER BY</code> 的前缀,否则报错。</p>
-
DISTINCT ON (user_id)后必须紧跟ORDER BY user_id, created_at DESC, id DESC - 不能写成
ORDER BY created_at DESC, user_id,顺序错就失效 - 如果业务允许忽略严格「最新」而只要「某个最新代表」,这个方案最省事
SELECT DISTINCT ON (user_id) * FROM orders ORDER BY user_id, created_at DESC, id DESC;
真正麻烦的不是写法本身,而是「最新」的定义是否覆盖所有边界情况:时间相同怎么排、NULL 怎么处理、并发写入时时间精度够不够。这些细节一旦漏掉,线上就可能查出意料之外的“最新”数据。











