必须用 ROW_NUMBER() 而非 RANK() 或 DENSE_RANK(),因其严格按排序顺序赋予唯一序号,确保每组恰好返回前三行;RANK() 并列时重复编号且跳号,DENSE_RANK() 并列不跳号但仍可能返回超三条记录。

用 ROW_NUMBER() 是最稳妥的选择,尤其当你明确要求“每组最多三条记录”时——它不因并列而多返回,也不跳号,逻辑可控。
为什么必须用 ROW_NUMBER() 而不是 RANK() 或 DENSE_RANK()
业务上说“前三名”,多数场景指物理上的前三行,不是“排名 ≤ 3”的所有行。RANK() 遇到并列会重复编号(比如两个第1名),接着跳到第3名;DENSE_RANK() 并列后不跳号,但同样可能让某组返回4条甚至更多(例如:三个95分 → 全是第1名)。只有 ROW_NUMBER() 严格按排序顺序给唯一序号,保证 rn 真正筛出最多3条。
- 误用
RANK()的典型现象:WHERE rk 返回5条记录,只因前四条分数全相同 -
DENSE_RANK()适合“同薪同名、不跳号”场景(如职级榜),但不适合硬性 Top-3 数量控制 - MySQL 8.0+、PostgreSQL、SQL Server 都支持,SQLite 需 ≥ 3.25 且编译启用窗口函数
ROW_NUMBER() 必须带 OVER(),且 PARTITION BY 和 ORDER BY 缺一不可
单独写 ROW_NUMBER() 会报错:Window function 'ROW_NUMBER' requires an OVER clause。漏掉 PARTITION BY 就变成全表连续编号;漏掉 ORDER BY 则直接语法错误。
-
PARTITION BY category决定“按哪列分组”,不能写成GROUP BY—— 后者会聚合行数,破坏原始记录 -
ORDER BY total_amount DESC, order_id ASC中的order_id ASC是防并列时排序不稳定,避免每次查询结果不一致 - 如果
total_amount含NULL,默认排最前(MySQL)或最后(PostgreSQL),建议加NULLS LAST或用COALESCE(total_amount, -1)
窗口函数不能出现在 WHERE,必须套子查询或 WITH CTE
直接写 WHERE ROW_NUMBER() OVER (...) 会报错:<code>window function is not allowed in WHERE clause。这是 SQL 标准限制,不是 MySQL 特有。
- 子查询写法最通用:
SELECT category, order_id, total_amount FROM ( SELECT category, order_id, total_amount, ROW_NUMBER() OVER (PARTITION BY category ORDER BY total_amount DESC, order_id ASC) AS rn FROM orders ) t WHERE rn <= 3;
- CTE 更易读,尤其当基础数据需过滤(如只查
status = 'completed'):WITH ranked AS ( SELECT category, order_id, total_amount, ROW_NUMBER() OVER (PARTITION BY category ORDER BY total_amount DESC) AS rn FROM orders WHERE status = 'completed' ) SELECT category, order_id, total_amount FROM ranked WHERE rn <= 3;
- 子查询必须加别名(如
AS t),否则 MySQL 报ERROR 1248 (42000):“Every derived table must have its own alias”
性能和兼容性容易被忽略的点
大表跑 ROW_NUMBER() 会全量扫描分组数据,没索引时慢得明显。另外,PARTITION BY 字段含 NULL 会被归为同一组——这常导致“未知分类”的数据意外挤进某组前三。
- 在
(category, total_amount)上建联合索引,能显著加速排序和分组 - 若
category是字符串但混了数字类型(如 '1', 1),隐式转换可能导致分组错乱,建议显式CAST(category AS CHAR) - MySQL 5.7 及更早不支持窗口函数,强行运行报
ERROR 1064 (42000);先执行SELECT VERSION();确认版本 -
PARTITION BY字段为NULL时,所有NULL行归一组——如需排除,加WHERE category IS NOT NULL
真正麻烦的从来不是写对那几行 SQL,而是分组字段的 NULL 处理、排序字段的稳定性、以及旧版本兼容性——这些地方一漏,线上查出来的“前三名”就 quietly 错了。

















