最可靠方法是用 row_number() 配合子查询:先用窗口函数按组编号,再在外层筛选 rn≤n;limit/top 不支持分组内重置计数,无法正确实现每组 top n。

用 ROW_NUMBER() 配合子查询取每组 TOP N 最可靠
直接上结论:别用 LIMIT 或 TOP 做分组取前 N,它们不支持按组重置计数。必须靠窗口函数 ROW_NUMBER() 先给每组内行编号,再在外层筛选 rn 。
常见错误是写成 GROUP BY + ORDER BY ... LIMIT N,结果只返回 1 组的 N 行,不是每组都返回 N 行。
-
ROW_NUMBER()必须配合PARTITION BY指定分组字段,否则全局编号就废了 - 排序依据(
ORDER BY)要写在窗口函数里,不是外层ORDER BY - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;MySQL 5.7 及更早版本不支持,得换变量模拟或用自连接
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
<h3>MySQL 5.7 怎么绕过没窗口函数的限制</h3>
<p>没有 <code>ROW_NUMBER()</code> 就得靠变量“手搓”序号,但要注意执行顺序不可靠,必须用子查询强制排序后再赋值。</p>
<p>典型翻车点:直接在 <code>SELECT</code> 里写 <code>@rn := @rn + 1</code>,但 MySQL 不保证变量更新和行处理的顺序,结果随机。</p>
- 先
ORDER BY排好序,再进变量计算,否则序号错乱 - 变量初始化必须在同一个语句里完成,不能依赖上一条
SET - 性能比窗口函数差,大数据量时明显变慢
SELECT user_id, product_id, amount
FROM (
SELECT user_id, product_id, amount,
@rn := IF(@prev = user_id, @rn + 1, 1) AS rn,
@prev := user_id
FROM (
SELECT user_id, product_id, amount
FROM orders
ORDER BY user_id, amount DESC
) t,
(SELECT @rn := 0, @prev := '') r
) ranked
WHERE rn
<h3>
<code>RANK()</code> 和 <code>DENSE_RANK()</code> 在并列时行为不同</h3>
<p>如果同组内有相同排序值(比如两个订单金额都是 999),<code>ROW_NUMBER()</code> 会强行分出 1、2、3;而 <code>RANK()</code> 给并列者同名次,跳过后续名次(1、1、3);<code>DENSE_RANK()</code> 也是同名次,但不跳(1、1、2)。</p>
<p>选哪个取决于业务定义:“TOP 3” 是指最多取 3 行,还是允许并列后超过 3 行?比如用户 A 有 4 笔 100 元订单,你希望全拿还是只拿前 3 条?</p>
- 严格控制行数上限 → 用
ROW_NUMBER() - 允许并列且不跳名次 → 用
DENSE_RANK() - 并列后跳空位(如奥运奖牌榜)→ 用
RANK()
WHERE 里不能直接用窗口函数结果
这个错误太常见:SELECT ..., ROW_NUMBER() OVER (...) AS rn WHERE rn ,报错 <code>Invalid use of window function。窗口函数只能出现在 SELECT 或 ORDER BY 子句,不能进 WHERE 或 HAVING。
原因很简单:SQL 执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY,窗口函数在 SELECT 阶段才计算,WHERE 阶段它根本不存在。
- 必须套一层子查询或 CTE,把窗口函数结果暴露为普通列
- CTE 写法更清晰,但老版本 MySQL 不支持,得用子查询
- 别试图用
HAVING替代,它只对聚合结果生效,对窗口函数无效










