LIMIT分页在大数据量下变慢是因为需扫描offset前所有行,导致I/O和临时表开销剧增;应改用基于主键/时间戳的游标分页(如WHERE id > ?),配合fetchmany和SSCursor避免内存溢出。
为什么直接 LIMIT 分页在大数据量下会越来越慢
因为 limit offset, size 本质是让数据库先跳过 offset 行,再取 size 行——哪怕你只要最后 10 条,它也得扫描前面几百万行。索引可能失效,磁盘 i/o 和临时表开销陡增。
常见错误现象:SELECT * FROM orders ORDER BY id DESC LIMIT 1000000, 10 执行超时或触发 OOM;SHOW PROCESSLIST 显示状态长期卡在 Copying to tmp table。
- 用主键/唯一有序字段(如
id、created_at)做游标分页,比LIMIT+OFFSET快一个数量级 - 避免
ORDER BY RAND()或非覆盖索引字段排序,否则无法利用索引定位起始点 - 如果业务允许,优先用「上次查到的最后一条记录值」作为下次查询条件,例如
WHERE id > 12345678
用 WHERE id > ? 替代 LIMIT offset 的实操要点
这是最常用、最稳定的分段策略,前提是表有单调递增/递减的主键或时间戳字段。
使用场景:导出全量订单、同步历史日志、ETL 抽取冷数据。
- 第一次查:
SELECT * FROM events WHERE created_at >= '2023-01-01' ORDER BY id ASC LIMIT 1000,记下结果中最大的id(比如last_id = 98765) - 下一批:
SELECT * FROM events WHERE id > 98765 AND created_at >= '2023-01-01' ORDER BY id ASC LIMIT 1000 - 务必在
WHERE中复用原始过滤条件(如时间范围),否则可能漏数据或重复 - 不要依赖
ORDER BY created_at做游标——同一秒可能有多个记录,created_at不唯一会导致跳行或重读
内存不足时,如何控制单次查询不崩掉 Python 进程
即使 SQL 分段了,Python 一次性 fetchall() 几万行仍可能吃光内存,尤其字段多或含 TEXT/BLOB。
常见错误现象:Python 报 MemoryError;系统 kill 掉进程;数据库连接被长时间占用未释放。
- 用
cursor.fetchmany(size)代替fetchall(),每次只拿 100–1000 行处理 - 确保在循环里及时
del row或不保留整批引用,避免 Python 引用计数延迟释放 - MySQLdb / PyMySQL 默认启用缓存,加参数
cursorclass=pymysql.cursors.SSCursor(服务器端游标),让结果集不全载入内存 - PostgreSQL 用户用
cursor.name声明命名游标,配合FETCH FORWARD 1000流式拉取
OFFSET 没法改怎么办?只能硬扛的几个现实情况
有些场景确实绕不开 OFFSET:第三方 ORM 自动生成分页、遗留接口不能动 SQL 结构、前端传的是页码不是游标。
这时候不是“能不能”,而是“怎么撑住”。关键在降载+限流。
- 给大 offset 查询加超时:
cursor.execute("SET SESSION max_execution_time = 5000")(MySQL 5.7+) - 用
EXPLAIN确认是否走了索引;没走就加联合索引,比如(status, id)配合WHERE status=1 ORDER BY id LIMIT ... - 把高频大 offset 查询结果缓存到 Redis,过期时间设短(如 30 秒),避免反复压库
- 真正要导千万级数据时,别在应用层拼 SQL,直接用
mysqldump --where="..."或pg_dump --table=... --inserts更稳
游标分页不是银弹——当主键不连续、存在大量删除、或需要严格按页码返回时,OFFSET 仍是唯一选择。这时候性能瓶颈不在代码,在要不要换存储方案或加物化视图。










