窗口函数row_number()配合join取最新记录时,必须先用over(partition by order_id order by created_at desc, id desc)编号,再where rn=1过滤,才能确保获取每组完整最新行;直接join后group by无法保证返回最新记录。

用窗口函数 ROW_NUMBER() 配合 JOIN 取最新记录
直接在 JOIN 后再 GROUP BY 是行不通的——SQL 标准里,GROUP BY 之后不能保证返回的是“最新”那条,除非显式排序并截断。真正可靠的做法是先用窗口函数标序,再过滤出序号为 1 的行。
常见错误是写成这样:SELECT a.*, b.* FROM orders a JOIN (SELECT order_id, MAX(created_at) FROM items GROUP BY order_id) b ON a.id = b.order_id —— 这只能拿到时间,拿不到对应那条完整记录(比如 item_name 或 status 就丢了)。
- 必须用
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY created_at DESC)给每组内的行编号 -
PARTITION BY字段要和你要关联的外键一致(比如order_id),否则分组错位 -
ORDER BY必须含确定性排序字段;如果created_at可能重复,建议追加主键如id DESC避免随机取值 - 子查询或 CTE 中完成编号后,外层加
WHERE rn = 1才能稳稳拿到每组第一条
MySQL 8.0+ 和 PostgreSQL 直接支持,但旧版 MySQL 要绕开
MySQL 5.7 及更早版本不支持窗口函数,强行用会报错 This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery' 或直接语法错误。这时候得用相关子查询或自连接模拟。
- 相关子查询写法:
SELECT * FROM items i1 WHERE created_at = (SELECT MAX(created_at) FROM items i2 WHERE i2.order_id = i1.order_id),但注意:如果有多个同时间记录,会返回多条,不是“一条” - 更稳妥的自连接:
SELECT i1.* FROM items i1 LEFT JOIN items i2 ON i1.order_id = i2.order_id AND (i1.created_at —— 利用“找不到更大时间(或同时间更大 id)的记录”来反推最大者 - PostgreSQL 和 MySQL 8.0+ 推荐统一走
ROW_NUMBER(),语义清晰、性能通常更好(尤其有索引时)
JOIN 前还是 JOIN 后做“取最新”?顺序很关键
如果你要查订单 + 每个订单下最新的一条商品,应该先对 items 表单独处理出最新记录,再和 orders 表 JOIN;而不是先 JOIN 再分组——后者会导致中间结果集爆炸,且 GROUP BY orders.id 无法控制 items 字段的取值来源。
- 正确姿势:把带
ROW_NUMBER()的items查询封装成子查询或 CTE,然后JOIN orders - 错误姿势:
SELECT orders.*, items.* FROM orders JOIN items ON ... GROUP BY orders.id——items.*中非聚合字段的值是未定义的(MySQL 5.7 strict mode 下直接报错) - 如果还要加
WHERE orders.status = 'paid',务必放在最外层,避免在子查询里提前过滤掉本该参与分组的订单
索引没建对,ROW_NUMBER() 也会慢得明显
窗口函数本身不慢,但排序成本高。如果 PARTITION BY order_id ORDER BY created_at DESC 没走索引,数据量一过万就明显卡顿。
- 必须建联合索引:
CREATE INDEX idx_order_created ON items (order_id, created_at DESC)(MySQL 8.0+/PostgreSQL 支持DESC索引;MySQL 5.7 只能建(order_id, created_at),然后靠优化器倒排扫描) - 别只建
created_at单列索引——它无法加速PARTITION BY order_id的分组定位 - 如果经常按其他字段查最新(比如
status = 'shipped'),索引要考虑覆盖过滤条件,例如(order_id, status, created_at)
实际跑之前,用 EXPLAIN 看一眼是否用了索引、是否出现 Using filesort。没索引的 ROW_NUMBER() 在百万级表上可能秒变分钟级。










