rank()易取重复第一条,因其对相同排序值赋予相同名次并跳过后续名次(如两行并列第1,下行为第3),导致where rn=1匹配多行;真正稳定取唯一首行须用row_number()配合含唯一字段的order by(如created_at desc, id desc)及子查询/cte嵌套。

为什么用 RANK() 容易拿到重复的第一条?
RANK() 会给相同排序值的行分配相同名次,然后跳过后续名次。比如两行并列第1,下一行就是第3——这会导致 RANK() = 1 匹配到多条数据,根本达不到“每个类别只取一条”的目的。
真正稳定取“第一条”的关键是:必须有明确、唯一的排序依据,且不能容忍并列。
- 用
ROW_NUMBER()替代RANK(),它强制按指定顺序逐行编号,不跳号也不并列 - 排序字段必须包含能打破平局的列,例如
created_at或id;仅按category排序毫无意义 - 如果业务上真允许“任意一条”,也要显式加
ORDER BY category, id,否则不同执行可能返回不同结果
正确写法:用 ROW_NUMBER() + 子查询/CTE
窗口函数不能直接出现在 WHERE 中,所以必须嵌套一层。最常用的是 CTE(可读性好)或内联子查询(兼容老版本 MySQL)。
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY created_at DESC, id DESC
) AS rn
FROM products
)
SELECT * FROM ranked WHERE rn = 1;
-
PARTITION BY category是分组依据,不是GROUP BY,不压缩行数 -
ORDER BY created_at DESC, id DESC确保最新创建、ID 最大的那条优先被标为rn = 1 - 如果想取最早一条,把
DESC全换成ASC
MySQL 8.0+ 和 PostgreSQL 的小差异
语法基本一致,但注意两个细节:
- PostgreSQL 支持在
ORDER BY里用表达式,比如ORDER BY LOWER(name);MySQL 8.0 要求该列必须在SELECT列表中出现(除非是确定性函数) - MySQL 在
ROW_NUMBER()的ORDER BY中若引用别名(如ORDER BY updated_time),需确保该别名已在SELECT中定义,否则报错Unknown column 'updated_time' in 'window order clause' - 两者都不支持在窗口定义里用子查询,例如
ORDER BY (SELECT MAX(price) FROM sales)会直接报错
性能关键:别忘了给排序字段建索引
没有索引时,ROW_NUMBER() OVER (PARTITION BY category ORDER BY created_at DESC) 可能触发全表扫描 + 内存排序,尤其数据量大时延迟明显。
- 最优索引是复合索引:
CREATE INDEX idx_category_created ON products(category, created_at DESC); - 如果经常按
category分组再按id排序,索引应为(category, id)—— 注意顺序,category必须在前 - PostgreSQL 中,
DESC索引方向要和ORDER BY严格一致才生效;MySQL 8.0+ 支持降序索引,但老版本会忽略DESC关键字
实际执行前用 EXPLAIN 看一眼是否走了索引,比凭感觉调优靠谱得多。










