row_number() + partition by 是按字段分组并取每组前n条的标准解法,它为每组内记录按排序规则生成连续序号,再通过外层查询筛选序号≤n的记录。

ROW_NUMBER() + PARTITION BY 是最直接的解法
想按某个字段分组、每组取前 N 条,ROW_NUMBER() 配合 PARTITION BY 是标准答案。它给每组内记录按排序规则打上 1、2、3… 的序号,再用外层查询过滤 rn 即可。
注意:必须用子查询或 CTE 包一层,因为窗口函数不能直接出现在 WHERE 中。
- 排序字段决定“前 N 条”的依据,漏写
ORDER BY会导致结果不稳定(不同执行可能顺序不同) -
PARTITION BY字段要和业务分组逻辑严格一致,比如按user_id分组就别错写成category - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也行,但旧版不支持
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>为什么不用 LIMIT 或 TOP?</h3>
<p><code>LIMIT</code>、<code>TOP</code> 是全表限制,没法做“每组各取 N 条”。有人试过 <code>GROUP BY</code> 配合 <code>ARRAY_AGG</code> 或 <code>STRING_AGG</code> 再截断,但写法复杂、可读性差、性能通常更弱,且不是所有数据库都支持聚合截断语法。</p>
- MySQL 5.7 及更早版本不支持窗口函数,只能靠自连接或变量模拟,容易出错且难维护
- 用
RANK()或DENSE_RANK()替代ROW_NUMBER()会改变“并列时是否跳号”,导致实际返回条数超过 N(比如两个第 1 名,RANK()都是 1,下一个是 3) - 某些 ORM(如 Django ORM)对窗口函数支持有限,生成的 SQL 可能不合法,得手写原生查询
性能关键点:索引怎么建?
没索引时,ROW_NUMBER() 要对每组数据全排序,代价很高。最优索引应覆盖 PARTITION BY 和 ORDER BY 字段:
- 例如
PARTITION BY user_id ORDER BY amount DESC,建复合索引(user_id, amount)(注意顺序) - 如果
ORDER BY有多个字段,比如ORDER BY status, created_at DESC,索引也要对应为(user_id, status, created_at) - WHERE 条件中的过滤字段(如
WHERE created_at > '2024-01-01')建议放在索引最左列之前——但实际需结合查询频率权衡,不是无脑堆字段
容易被忽略的 NULL 处理
ORDER BY 字段含 NULL 时,不同数据库默认行为不一致:NULLS FIRST 还是 NULLS LAST?这直接影响第 1 条是不是 NULL 记录。
- PostgreSQL 默认
NULLS LAST(升序)或NULLS FIRST(降序),但可显式写ORDER BY amount DESC NULLS LAST - MySQL 8.0 默认把
NULL当最小值,ORDER BY ... DESC时NULL排最前 - 如果业务要求
NULL不参与排名,可在子查询里先WHERE amount IS NOT NULL过滤掉
没意识到这点,上线后可能发现“前 3 名”里混进一堆 NULL,而你根本没测试过空值场景。










