应优先使用窗口函数row_number()在右表中为每组记录按时间倒序编号,取rn=1后再left join;mysql 8.0+/postgresql支持该写法,需确保order by含确定性字段(如created_at desc, id desc),且r.rn=1必须置于on条件中以保留左表全量记录。

LEFT JOIN 配合窗口函数取最新记录最可靠
直接用 GROUP BY + MAX(created_at) 无法拿到整行数据,容易错配字段;用子查询关联也易出性能问题。真正稳妥的做法是先用窗口函数标出每组最新记录,再和主表 LEFT JOIN。
核心思路:在关联表上用 ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY created_at DESC) 打标记,只保留 rn = 1 的行,再与主分类表连接。
- 必须用
ROW_NUMBER(),不能用RANK()或DENSE_RANK()—— 否则时间相同时会返回多行,破坏“一条”的前提 -
ORDER BY要包含确定性字段(如id)防并列:写成ORDER BY created_at DESC, id DESC - 子查询必须是带别名的派生表,否则多数数据库不支持在
ON条件里引用窗口结果
MySQL 8.0+ 和 PostgreSQL 的标准写法
这两者都支持窗口函数,语法一致,可直接复用:
SELECT c.name AS category_name,
r.title AS latest_review_title,
r.created_at
FROM categories c
LEFT JOIN (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY category_id
ORDER BY created_at DESC, id DESC
) AS rn
FROM reviews
) r ON c.id = r.category_id AND r.rn = 1;
注意:r.rn = 1 必须写在 ON 里,不能写在 WHERE —— 否则会把无关联记录(r 为 NULL)过滤掉,失去 LEFT JOIN 意义。
MySQL 5.7 或更老版本只能靠相关子查询
没有窗口函数时,得用 (SELECT ... LIMIT 1) 模拟,但性能差、不可索引优化,且 LIMIT 在子查询中需配合 ORDER BY 显式声明:
SELECT c.name,
(SELECT title FROM reviews r2
WHERE r2.category_id = c.id
ORDER BY r2.created_at DESC, r2.id DESC
LIMIT 1) AS latest_title,
(SELECT created_at FROM reviews r2
WHERE r2.category_id = c.id
ORDER BY r2.created_at DESC, r2.id DESC
LIMIT 1) AS latest_created_at
FROM categories c;
- 每个字段都要单独写一遍子查询,无法一次取出整行,字段多时冗余严重
- 必须给
r2.category_id = c.id建联合索引:INDEX(category_id, created_at, id),否则全表扫描 - SQLite 支持类似写法,但 Oracle 旧版要用
KEEP (DENSE_RANK LAST...),语法差异大
为什么不能用 GROUP BY + MAX(id) 硬凑
常见错误是这样写:
SELECT c.name, r.title, MAX(r.created_at) FROM categories c LEFT JOIN reviews r ON c.id = r.category_id GROUP BY c.id, c.name, r.title;
这会导致:r.title 和 MAX(r.created_at) 完全不匹配 —— MySQL 5.7+ 会报错,低版本虽能跑但结果不可靠。
-
GROUP BY r.title把同一分类下不同评论全拆开了,违背“每个分类一条”的目标 - 想靠
MAX(r.id)取最新,前提是id严格递增且和时间一致,业务上不总成立 - 哪怕加了
HAVING r.id = MAX(r.id),语法也不合法,HAVING不能引用非聚合字段
窗口函数不是炫技,是解决这类“分组取首行”问题的事实标准。没它时妥协方案代价高,有它就别绕路。










