ROW_NUMBER()是最可靠方案,需在OVER中写全PARTITION BY和ORDER BY,外层用子查询或CTE筛选rn=1;漏ORDER BY会导致结果不可靠,GROUP BY无法返回整行。

用 ROW_NUMBER() 窗口函数最可靠
绝大多数场景下,ROW_NUMBER() 是唯一能稳定实现“每个分组取第一行”的标准方案。它不依赖排序稳定性,也不受重复值干扰,只要明确指定 ORDER BY 子句,结果就可预期。
常见错误是漏写 ORDER BY —— 比如写成 ROW_NUMBER() OVER (PARTITION BY user_id),这在 PostgreSQL 和 SQL Server 会报错,MySQL 8.0+ 虽允许但行为未定义,实际返回顺序不可靠。
实操建议:
- 必须在
OVER()中提供确定性排序,例如ORDER BY created_at DESC, id ASC - 若业务逻辑允许任意一行,仍需显式写
ORDER BY,可用ORDER BY (SELECT NULL)(SQL Server)或ORDER BY RANDOM()(PostgreSQL),但注意性能开销 - 避免用
RANK()或DENSE_RANK()替代 —— 它们对相同排序值会给出相同序号,导致“第一行”可能变成多行
WHERE 子句中嵌套窗口函数要绕开语法限制
直接写 WHERE rn = 1 会报错,因为窗口函数不能出现在 WHERE 阶段。必须用子查询或 CTE 提前计算序号。
推荐写法是 CTE,清晰且兼容性好:
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC) AS rn FROM products ) SELECT id, name, category, score FROM ranked WHERE rn = 1;
注意点:
- MySQL 8.0+、PostgreSQL、SQL Server 都支持 CTE;SQLite 3.8.3+ 也支持,但旧版本只能用派生表
- 不要把整个原表塞进子查询再
JOIN—— 这容易引发重复扫描,CTE 在多数引擎中会被优化为物化或内联,更高效 - 如果只查少量字段,记得在 CTE 内就
SELECT必需列,别用*拖全表字段
GROUP BY + 聚合函数无法真正替代
有人试图用 SELECT category, MAX(score), MIN(id) FROM products GROUP BY category 来“取第一行”,这是典型误解:它返回的是各字段的极值,未必来自同一行。
比如某 category 下有两行:(id=100, score=95) 和 (id=200, score=87),MAX(score) 返回 95,MIN(id) 返回 100 —— 表面看没问题,但若换成 MAX(id) 就错配了。
更危险的是字符串字段:MAX(name) 按字典序取最大名,和你想要的“最早创建”或“最高分对应的名字”完全无关。
所以:
-
GROUP BY只适合聚合统计,不适合“取完整行” - 即使加了
ORDER BY和LIMIT 1,也无法在分组维度上生效 ——LIMIT是全局的 - 某些 MySQL 5.7 的
ONLY_FULL_GROUP_BY关闭状态下允许非聚合列混用,但结果随机且不可迁移
MySQL 5.7 及更早版本没有窗口函数怎么办?
只能靠相关子查询或自连接,性能差但别无选择。核心思路是:对每组找一个“没有更优同行”的记录。
例如按 score DESC 取每 category 最高分那行:
SELECT p1.*
FROM products p1
WHERE NOT EXISTS (
SELECT 1 FROM products p2
WHERE p2.category = p1.category
AND (p2.score > p1.score OR (p2.score = p1.score AND p2.id < p1.id))
);关键细节:
- 复合条件
(p2.score > p1.score OR (p2.score = p1.score AND p2.id 保证严格“更优”,解决分数相同时取 id 更小的行 - 索引必须覆盖
category、score、id,否则全表扫描代价极高 - 数据量超过几千行时,这种写法会明显变慢,应尽快升级到 MySQL 8.0+
真实业务里,分组键多、排序规则复杂、数据量大时,ROW_NUMBER() 的写法几乎不可替代;而那些看似“简单”的替代方案,往往在边界 case 上突然崩掉。

















