row_number()必须配合over(partition by order by)使用,且需嵌套子查询或cte才能在where中过滤rn≤n;单独使用、省略order by或错放执行顺序均会报错。

MySQL 8.0+ 用 ROW_NUMBER() 配合 PARTITION BY
这是最直接、语义最清晰的解法,但只适用于 MySQL 8.0+、PostgreSQL、SQL Server 等支持窗口函数的数据库。核心是给每组内行按某字段排序编号,再筛出编号 ≤ N 的记录。
常见错误是把 ROW_NUMBER() 放在 WHERE 或 GROUP BY 后面——窗口函数必须在 WHERE 之后、ORDER BY 之前执行,所以得套一层子查询或 CTE:
SELECT user_id, product_id, amount
FROM (
SELECT user_id, product_id, amount,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY amount DESC
) AS rn
FROM orders
) t
WHERE rn
-
PARTITION BY user_id定义分组维度,和 GROUP BY 逻辑一致但不聚合 -
ORDER BY amount DESC决定“前N条”的依据,必须显式指定,否则结果不确定 - 别名
rn不能在同级 WHERE 中直接引用,必须嵌套;CTE 写法(WITH ranked AS (...) SELECT * FROM ranked WHERE rn )更易读
MySQL 5.7 及更早版本只能靠自关联或变量模拟
老版本不支持窗口函数,GROUP BY 又无法直接返回多行,硬要用 LIMIT 会报错或只返回单条。这时得用相关子查询数“当前行在组内排第几”:
SELECT o1.user_id, o1.product_id, o1.amount
FROM orders o1
WHERE (
SELECT COUNT(*)
FROM orders o2
WHERE o2.user_id = o1.user_id
AND o2.amount > o1.amount
)
- 这个写法等价于“组内比当前行 amount 更大的记录少于 3 条”,即当前行是 top 3 之一
- 性能差:对每行都跑一次子查询,数据量稍大(比如每组百条以上)就明显变慢
- 注意边界:用
得到前 3 名,若存在并列(相同 amount),可能返回超过 3 行;要严格限制条数得加额外去重或用变量方案
PostgreSQL 可用 array_agg() + LIMIT 快速取前N,但只适合简单场景
如果只要每个分组的若干字段(比如只取 ID 列表),不用展开成多行,array_agg() 配合 ORDER BY 和 LIMIT 是轻量解法:
SELECT user_id,
(ARRAY_AGG(product_id ORDER BY amount DESC))[1:3] AS top_products
FROM orders
GROUP BY user_id;
- 返回每组一个数组,最多含 3 个
product_id,自动截断 - 不适用于需要展开为独立行的场景(比如后续还要 JOIN 或计算),因为结果是聚合结构
-
[1:3]是 PostgreSQL 数组切片语法,MySQL/SQL Server 不支持;SQL Server 对应的是STRING_AGG+TOP子句,逻辑不同
ORDER BY 和 NULL 值会影响“前N”的实际结果
很多人忽略排序字段含 NULL 时的行为:不同数据库默认把 NULL 当最大还是最小值,会导致前N条意外漏掉或混入空值。
- MySQL 默认
NULL最小(ORDER BY ... ASC时排最前),PostgreSQL 默认NULL最大 - 显式控制:用
ORDER BY amount DESC NULLS LAST(PostgreSQL)或 MySQL 的ORDER BY amount DESC, id DESC补充二级排序 - 如果业务上
amount IS NULL意味着无效数据,应在 WHERE 中先过滤:WHERE amount IS NOT NULL
真正麻烦的是分组内排序字段重复且没有唯一键兜底——这时候 ROW_NUMBER() 仍会强行编号,但结果不稳定;生产环境建议总带上一个确定性次序字段(比如主键)。










