应先在右表中按分组计算最新记录标识(如最大时间戳或ROW_NUMBER()),再与左表关联;MySQL 8.0+推荐用窗口函数ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY created_at DESC, id DESC)取rn=1,5.7则用自连接找“不存在更新记录”的行,避免GROUP BY丢失非聚合字段。

Left Join 后怎么只取右表每组的最新一条?
直接在 LEFT JOIN 里用 ORDER BY ... LIMIT 1 是无效的——SQL 标准不支持在 JOIN 子句里写排序和限制。常见错误是写出类似 LEFT JOIN (SELECT * FROM logs ORDER BY created_at DESC LIMIT 1),结果要么报错,要么逻辑错(只取了全局最新一条,而非每组最新)。
真正可行的思路是:先在右表内按分组算出最新记录的标识(比如最大时间戳或自增 ID),再用这个结果去关联左表。关键不是“限制”,而是“找每组极值”。
用窗口函数 ROW_NUMBER() 最直观
如果你用的是 PostgreSQL、SQL Server、MySQL 8.0+ 或 Oracle,ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) 是最贴近直觉的解法。它给每个 user_id 组内的记录按时间倒序编号,取 rn = 1 就是最新那条。
实操建议:
- 别在
JOIN的ON条件里写窗口函数——必须先封装成子查询或 CTE -
PARTITION BY字段必须和LEFT JOIN关联字段一致,否则会出现空匹配或重复 - 如果
created_at可能重复,加个id DESC作为次级排序,确保结果确定
示例(MySQL 8.0+):
SELECT u.name, l.content, l.created_at FROM users u LEFT JOIN ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) AS rn FROM logs ) l ON u.id = l.user_id AND l.rn = 1;
兼容老版本 MySQL(5.7)怎么办?
没有窗口函数时,常用两种替代:自连接找最大时间戳,或用相关子查询。前者性能通常更好,后者更易读但可能全表扫描。
推荐自连接方案(注意别漏掉 IS NULL 判断):
- 对右表
logs自连接:找“不存在比它更新的同 user_id 记录”的那条 - 必须写
l2.user_id = l1.user_id AND l2.created_at > l1.created_at,然后WHERE l2.id IS NULL - 如果
created_at有重复,仅靠时间无法唯一确定最新,得补上id比较
示例(MySQL 5.7):
SELECT u.name, l1.content, l1.created_at FROM users u LEFT JOIN logs l1 ON u.id = l1.user_id LEFT JOIN logs l2 ON l1.user_id = l2.user_id AND (l2.created_at > l1.created_at OR (l2.created_at = l1.created_at AND l2.id > l1.id)) WHERE l2.id IS NULL;
为什么不能用 GROUP BY + 聚合函数?
很多人第一反应是 LEFT JOIN (SELECT user_id, MAX(created_at) FROM logs GROUP BY user_id),但这只能拿到时间,拿不到对应那行的 content 或其他字段——MySQL 5.7 默认开启 ONLY_FULL_GROUP_BY,会直接报错;即使关掉,content 值也是不确定的(非聚合字段未定义行为)。
除非你只关心时间戳本身,否则这条路走不通。真要聚合,得用 MAX() 配合 CONCAT() 或 JSON 函数拼接整行,但可读性和维护性急剧下降,不推荐。
复杂点在于:不同数据库对“最新一条”的定义隐含歧义——是按时间?按主键?是否允许并列?这些必须在写 PARTITION BY 和 ORDER BY 时明确,否则上线后数据对不上很难排查。

















