ROW_NUMBER()是窗口函数,必须配合OVER子句使用,语法为ROW_NUMBER() OVER (PARTITION BY 分组字段 ORDER BY 排序字段),不可省略;需用子查询或CTE过滤rn=1,且分区与排序字段应建联合索引以提升性能。

ROW_NUMBER() 必须配合 OVER 才能用,否则直接报错
很多人写 ROW_NUMBER() 时漏掉 OVER,结果执行就抛 ERROR: window function ROW_NUMBER requires an OVER clause。它不是普通函数,是窗口函数,必须声明分区和排序逻辑。
常见错误写法:SELECT id, category, ROW_NUMBER() FROM t —— 这语法不合法,连解析都过不去。
正确结构只有这一种:
ROW_NUMBER() OVER (PARTITION BY <em>分组字段</em> ORDER BY <em>排序字段</em>)
-
PARTITION BY category表示按分类切片,每个分类内独立编号 -
ORDER BY price ASC决定谁排第 1 —— 要“最小记录”,就得升序;要最大就降序 - 如果排序字段有重复值(比如多个相同最小 price),
ROW_NUMBER()仍会强制给唯一序号,不保证稳定(可用id二次排序收尾)
取每类最小记录,得用子查询或 CTE 套一层
窗口函数不能直接在 WHERE 里过滤序号,因为 WHERE 执行顺序早于 SELECT 和窗口计算。所以必须把 ROW_NUMBER() 放进子查询或 CTE,再对外层查 rn = 1。
典型写法:
SELECT id, category, price
FROM (
SELECT id, category, price,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY price ASC, id ASC) AS rn
FROM products
) t
WHERE rn = 1;
- 这里加了
id ASC是为了当 price 相同时,选 id 更小的那条,避免结果随机波动 - 别用
LIMIT 1替代,它只限制最终结果总数,不是每类一条 - MySQL 8.0+、PostgreSQL、SQL Server 都支持;老版本 MySQL(5.7 及之前)不支持窗口函数,得用自连接或变量模拟
性能关键:PARTITION BY 字段一定要有索引
分区字段没索引,PARTITION BY category 会导致全表扫描 + 内存排序,数据量一过百万,查询明显变慢。
尤其当 ORDER BY 字段也参与排序时(如 ORDER BY price),最好建联合索引:
CREATE INDEX idx_category_price ON products(category, price);- 如果还要取
id或其他字段,索引可扩展为(category, price, id),避免回表 - 注意:PostgreSQL 对索引顺序更敏感,MySQL 8.0+ 的优化器相对宽松些
替代方案:用 DISTINCT ON(仅 PostgreSQL)更简洁
PostgreSQL 用户可以不用窗口函数,改用 DISTINCT ON,语义更直白,性能通常也更好:
SELECT DISTINCT ON (category) id, category, price FROM products ORDER BY category, price ASC, id ASC;
-
DISTINCT ON (category)表示“每个 category 只取第一行” -
ORDER BY必须以category开头,否则报错 —— 这是硬约束 - 它比
ROW_NUMBER()少一层嵌套,但只限 PostgreSQL;其他数据库不识别该语法
真正容易被忽略的是:分区字段和排序字段的组合是否覆盖了业务唯一性需求。比如价格相同、时间相同、ID 也相同的情况要不要去重,得看具体场景,而不是默认相信 rn = 1 就万事大吉。

















