row_number()能保留原始行,group by会丢字段;前者通过partition by分组+order by排序生成序号,再用子查询where rn=1精准取每组首行,后者本质是聚合压缩,无法返回完整明细行。

ROW_NUMBER() 能保留原始行,GROUP BY 会丢字段
GROUP BY 的本质是聚合,它强制把每组压缩成一行。一旦写了 GROUP BY user_id,再想 SELECT user_id, order_time, amount, id 就会报错(MySQL 8.0+、PostgreSQL 等默认开启严格模式),因为 order_time 和 id 不在分组键里,也不是聚合函数结果。你只能写 MAX(order_time) 或 ANY_VALUE(id),但后者不可靠——数据库随便挑一条,和“最新”无关。
而 ROW_NUMBER() 是窗口函数,它不改变行数,只给每行加个编号。你仍能拿到原表所有字段,包括 id、created_at、status,再用外层 WHERE rn = 1 精准捞出那条“最新”的完整记录。
ORDER BY 在窗口内生效,GROUP BY 的 ORDER BY 是全局的
很多人误以为 GROUP BY ... ORDER BY created_at DESC 能让每组里“最新”的那行排第一——其实不能。ORDER BY 在 GROUP BY 后才执行,它只影响最终结果集顺序,不影响分组内部哪一行被选中。
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) 中的 ORDER BY 是窗口内的排序,直接决定编号顺序:时间最新的排第 1,时间相同时 id DESC 保证唯一性。这个排序发生在分组内部,语义清晰、结果可控。
- 漏写
PARTITION BY→ 全表编号,不是分组 - 只写
ORDER BY created_at ASC→ 拿到的是最早,不是最新 - 没加二级排序(如
id DESC)→ 时间相同可能每次取不同行
WHERE 不能用窗口函数,但可以套子查询绕过
WHERE rn = 1 直接写在主查询里会报错:ERROR 3593: Window function 'row_number' is not allowed in this context。因为 SQL 执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY,窗口函数在 SELECT 阶段才计算,WHERE 根本看不见 rn。
正确做法是用一层子查询或 CTE:
SELECT user_id, order_id, created_at
FROM (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC, order_id DESC
) AS rn
FROM orders
) t
WHERE rn = 1;
这层嵌套不增加计算量,只是逻辑隔离。别在子查询里写 SELECT *,只选真正需要的列,减少内存和网络开销。
性能差异取决于你要什么,不是绝对快慢
如果只要前 10 条最新记录,GROUP BY 配合索引(如 (user_id, created_at))可能更快——它能利用“提前终止”,扫到够数就停。而 ROW_NUMBER() 必须对全量分组数据排序编号,哪怕最后只取 rn = 1 的几行。
但如果你要的是“每个用户最新的一条”,且必须返回完整行(比如带 payment_status、shipping_address),ROW_NUMBER() 是唯一通用解法。GROUP BY 在这种场景下要么报错,要么返回错误字段值。
容易被忽略的一点:ROW_NUMBER() 的 ORDER BY 子句里,如果有字段允许 NULL(比如 updated_at 为 NULL),不同数据库默认处理方式不同(PostgreSQL 默认 NULLS FIRST,MySQL 默认 NULLS LAST)。业务上若 NULL 表示“未更新”,你可能得显式写 ORDER BY updated_at DESC NULLS LAST,否则 NULL 行可能被排到最前。










