窗口函数在千万级表上直接开窗极易导致内存溢出,因其不走索引、无法下推过滤,需将整个分区结果集载入内存排序;应先用where过滤缩小数据集,再套窗口函数,并优先使用索引+limit替代全量row_number()。

窗口函数在千万级表上直接开窗,基本等于主动申请内存溢出。 它不走索引、不支持游标分页、默认把整个分区结果集拉进内存排序——哪怕你只想要第1行,数据库也得先把全部数据读进来排好序,再切一刀。这不是SQL写得不够巧的问题,是执行模型决定的硬限制。
为什么 ROW_NUMBER() OVER (ORDER BY x) 会 OOM
MySQL 8.0+ 和 PostgreSQL 的窗口函数执行逻辑类似:先完成 WHERE / JOIN / GROUP BY 阶段输出中间结果集,再对这个结果集按 OVER 子句指定的分区和排序字段做全量内存排序。一旦中间结果超百万行,sort_buffer_size(MySQL)或 work_mem(PostgreSQL)很快被撑爆,触发磁盘临时表甚至直接报 Out of memory。
- EXPLAIN 显示
WindowAgg节点出现在最外层,且没有Index Scan支撑排序字段 → 基本确认走的是 filesort - PostgreSQL 中
EXPLAIN (ANALYZE)会明确标出Sort Method: external merge Disk: XXXkB,这就是溢出信号 - MySQL 8.0 查看
SHOW STATUS LIKE 'Sort%',若Sort_merge_passes持续上升,说明 sort_buffer 不够用
先 WHERE 过滤,再套窗口,别在全表上开窗
窗口函数本身不能下推过滤条件,但你可以手动把它“包”进一个已过滤的子查询里。关键不是避免用窗口函数,而是确保它作用的数据集已经足够小。
- 错误写法:
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC) rn FROM orders→ 全表扫描 + 全表排序 - 正确写法:
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC) rn FROM (SELECT id, user_id, created_at FROM orders WHERE status = 'shipped' AND created_at >= '2026-04-01') t - 如果业务允许,WHERE 条件尽量命中复合索引前缀,例如
(status, created_at),让子查询走range扫描而非ALL - PostgreSQL 可进一步加
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW限定窗口范围,但 MySQL 8.0+ 不支持该语法,别白费劲
用索引 + LIMIT 替代 ROW_NUMBER() 分页
90% 的分页场景其实根本不需要全局序号。比如「查最新100笔已发货订单」,ORDER BY created_at DESC LIMIT 100 走 created_at 索引就能秒出,比算全量 ROW_NUMBER() 快两个数量级。
- 深分页(如第1000页)才考虑游标式分页:
WHERE created_at ,? 值取上一页最后一条的 <code>created_at - 如果必须返回页码(如「第N页共M页」),别在SQL里 COUNT(*) 全表——改用近似统计(
TABLE_ROWS从information_schema查)或物化总数表 - 复合排序需严格匹配索引顺序:想
ORDER BY user_id, created_at DESC,索引必须是(user_id, created_at),反过来无效
真正难处理的不是窗口函数本身,而是业务方坚持要「任意页码 + 全局唯一序号 + 实时数据」三者同时满足。这时候与其硬调参数扛内存,不如拉产品一起砍需求:要么接受游标分页(无页码),要么接受异步预计算(非实时),要么接受前端自己拼序号(不依赖SQL)。数据库不是万能缓冲区。











