left join 后取右表最新一条需用窗口函数row_number()配合partition by分组排序,再通过on l.rn=1关联;不可用limit 1或相关子查询,否则性能差或语法错误。

LEFT JOIN 后只取右表最新一条,核心靠窗口函数
直接在 LEFT JOIN 里加 LIMIT 1 是无效的——SQL 标准不支持对 JOIN 右侧子句单独限行。真正可靠的做法,是先用窗口函数(如 ROW_NUMBER())给右表每组记录标序,再筛出序号为 1 的那条。
用 ROW_NUMBER() 配合 PARTITION BY 实现“每组最新”
假设左表是 orders,右表是 order_logs(含 order_id、created_at、status),要为每个订单关联其最新的日志记录:
SELECT o.*, l.status, l.created_at
FROM orders o
LEFT JOIN (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY order_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM order_logs
) l ON o.id = l.order_id AND l.rn = 1
-
PARTITION BY order_id确保按订单分组编号 -
ORDER BY created_at DESC, id DESC解决时间相同时的不确定性(比如批量插入导致秒级时间相同) -
AND l.rn = 1必须写在ON条件里,不是WHERE,否则会把无日志的订单也过滤掉
MySQL 8.0+ 可用 CTE 简化可读性,但逻辑不变
如果用的是 MySQL 8.0+ 或 PostgreSQL,可以把子查询提成 CTE,语义更清晰,但执行计划和结果完全等价:
WITH latest_logs AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY order_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM order_logs
)
SELECT o.*, l.status, l.created_at
FROM orders o
LEFT JOIN latest_logs l ON o.id = l.order_id AND l.rn = 1
- CTE 不改变性能,只是写法更模块化
- 别误以为 CTE 是物化视图——它仍是逻辑视图,优化器可能内联展开
- 若右表数据量大,务必在
(order_id, created_at, id)上建联合索引
替代方案:相关子查询虽直观但性能差,慎用
有人会写 (SELECT ... FROM order_logs l2 WHERE l2.order_id = o.id ORDER BY ... LIMIT 1) 放在 SELECT 列表里,语法上可行,但问题明显:
- 每行左表记录都会触发一次独立子查询,复杂度是 O(n×m),远不如窗口函数单次扫描
- MySQL 5.7 及更早版本不支持
LIMIT在子查询中(除非包在派生表里) - 无法利用索引高效跳过非最新记录,尤其当某订单有数百条日志时,响应明显变慢
除非右表极小(
关键点其实就一个:窗口函数必须出现在子查询或 CTE 中,且 rn = 1 要进 ON 条件;PARTITION 和 ORDER BY 的字段顺序,决定了“最新”的定义是否稳定——比如用 updated_at 还是 id 做第二排序依据,得看业务是否允许时间重复。










