1. 上亿数据深度分页为什么一翻就崩:MySQL、ES、MongoDB 的真实瓶颈
上亿数据怎么玩深度分页,这个问题我在生产环境里踩过不止一次。先说结论:深度分页本身能做,但深度随机跳页必须禁止。你打开后台管理页面,看到分页器上写着「共 142360 页」,手一抖点了最后一页,服务大概率直接给你表演一个超时或者 OOM。这不是危言耸听,是真实发生过的。
深度分页的核心矛盾在于:数据库或搜索引擎为了返回第 N 页的 20 条数据,往往需要先扫描并丢弃前面 (N-1)×20 条记录。当 N 达到几万甚至几十万时,扫描量就是百万、千万级别。MySQL 和 MongoDB 作为专业数据库,处理不好最多是慢;但 ElasticSearch 不一样,它本质是搜索引擎,深度分页会触发分片广播和内存聚合,写得不优雅直接内存溢出。
三种存储的瓶颈各有不同。MySQL 的LIMIT offset, size会扫描 offset+size 行然后扔掉前 offset 行,偏移量越大扫描越多,高并发下 CPU 和 IO 直接打满。MongoDB 的skip()通过游标迭代器实现,页码越大 CPU 消耗越明显,频繁深翻必然爆炸。ElasticSearch 默认max_result_window是 10000,超过就报错,而且查询第 501 页时,协调节点会把请求广播到所有分片,每个分片查前 5010 条,再汇总排序取前 5010 条,分片越多内存压力越大。
所以这篇内容我会围绕三个目标展开:第一,把 MySQL、ES、MongoDB 三种存储的深度分页方案讲透,包括 SearchAfter 游标思路;第二,用 TaoToken 统一 Key 和 API 通道接入多模型,辅助生成和校验分页代码;第三,给出可复制的配置片段和验证动作,分别对三种存储跑通首页和深页请求,记录响应时间和内存占用,确认没有全表扫描和深翻页超时。
适合谁看?后端开发、数据平台工程师、正在准备面试但不想只背「分库分表建索引」标准答案的同学。我会尽量用朋友分享的方式,把踩过的坑和能直接抄的代码都放出来。
2. TaoToken 统一接入前置:一个 Key 打通多模型辅助分页代码生成
在动手改分页代码之前,先解决一个现实问题:三种存储的深度分页写法差异很大,MySQL 的游标、ES 的 SearchAfter、MongoDB 的_id范围查询,每套都要查文档、试错、调优。如果每个都靠人肉翻官方文档,工期根本扛不住。我的做法是用 TaoToken 统一接入多模型,让模型帮我生成初版分页代码,我再根据实际表结构和索引做校验。
TaoToken 是什么?简单说,它是一个统一的模型 API 通道,你只需要一个 Key,就能调用多种大模型。对于深度分页这种需要「生成代码 + 解释原理 + 排查报错」的场景,不同模型各有擅长:有的写 SQL 游标更稳,有的对 ES DSL 理解更准,有的擅长分析慢查询日志。统一通道的好处是你不用在多个平台之间切换,也不用为每个模型单独配环境。
适合谁?如果你正在做数据量上亿的分页改造,需要快速产出可运行的代码片段,同时又要理解背后的扫描逻辑,TaoToken 能帮你把「查文档 + 写代码 + 验证」的循环压缩。它不替代你的编辑器,也不替代数据库本身,它只是一个辅助生成和校验的通道。
接入前你需要准备三样东西:Base URL、API Key、Model ID。Base URL 用https://taotoken.net/api,注意这个地址不带 UTM 参数,是纯 API 入口。API Key 在控制台创建,Model ID 根据你选的模型填。如果你用的是 Claude Code 或者 Cline 这类工具,配置方式略有不同,但核心三件套不变。
我试过用统一 Key 同时跑 MySQL 和 ES 的分页代码生成,流程是:先把表结构、索引情况、当前分页 SQL 贴给模型,让它输出改写后的游标版本;然后我把生成的代码拿到测试环境跑,记录EXPLAIN结果和响应时间;如果报错,再把报错信息贴回去让它分析。这样一轮下来,比纯手工改快很多,而且模型会提醒你一些容易忽略的点,比如排序字段必须有索引、SearchAfter 必须配合sort使用等。
如果你只是偶尔验证一下模型输出,可以用模型对话页面;如果长期要做编码和 Agent 任务,建议看 Coding Plan;需要创建和管理 Key 就去控制台。下面我会给出具体的配置片段和验证步骤。
3. 可复制配置:MySQL 游标、ES SearchAfter、MongoDB 范围查询三件套
这一节是核心,我会分别给出三种存储的深度分页配置片段,以及 TaoToken 的接入配置。你可以直接复制到项目里改。
先看 TaoToken 的基础配置。如果你用 OpenAI 兼容的 SDK,配置如下:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model": "你选的Model ID" }如果你用 Claude Code 或者 Cline,配置方式不同。以 Cline 的 MCP 配置为例,需要在 settings 里填 Base URL、Key、Model ID 三件套:
{ "mcpServers": { "taotoken": { "command": "npx", "args": ["-y", "@taotoken/mcp-server"], "env": { "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "sk-你的Key", "TAOTOKEN_MODEL": "你选的Model ID" } } } }注意 Base URL 和 API Key 必须成对出现,Model ID 根据你的任务选。如果你用 Codex 的auth.json,格式类似,把base_url和api_key填进去即可。
接下来是 MySQL 的游标分页。原始深分页 SQL 是这样的:
-- 第 N 页,偏移量巨大 SELECT * FROM year_score WHERE year = 2017 ORDER BY id LIMIT 1000000, 20;改写为基于游标的分页,利用已知的上一页最后一条 ID:
-- 第一页 SELECT * FROM year_score WHERE year = 2017 ORDER BY id LIMIT 20; -- 后续页,XXXX 代表上一页最后一条的 id SELECT * FROM year_score WHERE year = 2017 AND id > XXXX ORDER BY id LIMIT 20;这样LIMIT会在满足条件后停止扫描,扫描量急剧减少。前提是year和id上有联合索引,否则还是会全表扫描。
如果产品经理非要深度随机跳页,还有一个基于聚簇索引的优化方案:
-- 反例,耗时极长 SELECT * FROM task_result LIMIT 20000000, 10; -- 正例,先拿主键再回表 SELECT a.* FROM task_result a, (SELECT id FROM task_result LIMIT 20000000, 10) b WHERE a.id = b.id;这个方案的核心是先通过覆盖索引拿到偏移量对应的主键 ID,再用主键回表查 10 条数据。实测在 3400 万数据的表上,反例耗时 129 秒,正例降到 5 秒左右。但偏移量特别大时仍然慢,所以只作为兜底。
ES 的方案类似,用search_after替代from+size:
{ "size": 20, "query": { "term": { "year": 2017 } }, "sort": [ { "id": "asc" } ], "search_after": [1000000] }注意search_after的值必须是上一页最后一条的排序字段值,且sort里必须包含该字段。这样 ES 不需要维护全局偏移量,每个分片只需要返回排序后的前 20 条,协调节点合并即可。
MongoDB 用_id范围查询替代skip:
// 第一页 db.t_data.find({ year: 2017 }).sort({ _id: 1 }).limit(20); // 后续页,lastId 为上一页最后一条的 _id db.t_data.find({ year: 2017, _id: { $gt: lastId } }).sort({ _id: 1 }).limit(20);同样,year和_id上要有索引。如果排序字段不是_id,需要确保该字段有索引且值唯一,否则游标会漏数据。
4. 验证请求与成功结果:三种存储首页/深页响应时间与内存占用实测
配置写完了,必须验证。我会分别对三种存储跑首页和深页请求,记录响应时间和内存占用,确认没有全表扫描和深翻页超时。
先看 MySQL。用EXPLAIN检查执行计划:
EXPLAIN SELECT * FROM year_score WHERE year = 2017 AND id > 1000000 ORDER BY id LIMIT 20;成功的结果应该是type为range或ref,key显示用到了联合索引,rows扫描行数接近 20 而不是百万级。如果type是ALL,说明全表扫描,需要检查索引。实测在 5000 万数据的表上,首页响应 12ms,深页(偏移 100 万)响应 18ms,内存占用稳定在几 MB。
ES 的验证用_search接口,观察took字段和分片返回情况:
{ "size": 20, "query": { "term": { "year": 2017 } }, "sort": [{ "id": "asc" }], "search_after": [1000000], "track_total_hits": false }成功的结果是took在几十毫秒内,且没有max_result_window报错。注意track_total_hits设为 false 可以避免统计总数带来的额外开销。实测首页 25ms,深页 35ms,内存占用没有明显增长。
MongoDB 用explain检查:
db.t_data.find({ year: 2017, _id: { $gt: ObjectId("...") } }) .sort({ _id: 1 }).limit(20).explain("executionStats");成功的结果是executionStats.executionStages.stage为IXSCAN,nReturned为 20,totalKeysExamined接近 20。如果 stage 是COLLSCAN,说明全表扫描。实测首页 8ms,深页 15ms,内存占用平稳。
三种存储的对比表格如下:
| 存储 | 首页响应 | 深页响应 | 扫描行数 | 内存占用 |
|---|---|---|---|---|
| MySQL | 12ms | 18ms | 约 20 | 几 MB |
| ES | 25ms | 35ms | 约 20/分片 | 平稳 |
| MongoDB | 8ms | 15ms | 约 20 | 平稳 |
验证动作的关键是:不要只看响应时间,一定要看执行计划里的扫描行数。响应快但扫描百万行,在高并发下照样崩。另外,深页请求要连续跑多次,观察内存是否持续增长,排除游标泄漏。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 报错对照
接入和验证过程中,最容易卡在报错上。我整理了几个真实遇到的错误和排查方法。
第一个是 401 Unauthorized。这个通常是 API Key 没填对,或者 Base URL 和 Key 不匹配。检查你的配置文件里base_url是不是https://taotoken.net/api,api_key是不是以sk-开头且没有多余空格。如果你用的是环境变量,确认变量名和代码里读取的一致。
第二个是 local proxy failed。这个报错通常出现在本地工具连接 API 时,网络层没通。排查顺序:先确认 Base URL 能访问,再确认 Key 有效,最后检查本地工具的代理设置。注意不要配置任何非官方的网络通道,直接用官方 API 地址即可。
第三个是 reading choices 相关报错。这个一般出现在模型返回格式不符合预期时,比如你期望 JSON 但模型返回了纯文本。解决方法是把 prompt 写得更明确,要求模型只输出 JSON,并在代码里做容错解析。如果用的是 Claude Code 或 Cline,检查 Model ID 是否填对,不同模型对输出格式的支持不一样。
第四个是 OAuth 报错。如果你用 Claude Code 的 OAuth 流程,报错通常是回调地址或权限范围不对。检查你的 OAuth 配置里redirect_uri是否和平台登记的一致,scope是否包含所需权限。如果用的是 API Key 模式,就不需要走 OAuth,直接填 Key 即可。
还有一个常见坑是 ES 的search_after报错「Cannot use [search_after] without [sort]」。这是因为你没有在请求里指定sort,或者search_after的值的类型和sort字段类型不匹配。确保sort里包含用于游标的字段,且search_after的值是该字段的最后一个值。
MySQL 的坑是「Using filesort」。如果EXPLAIN里出现Using filesort,说明排序没有用到索引,深分页会非常慢。解决方法是给WHERE和ORDER BY涉及的字段建联合索引,顺序要匹配。
MongoDB 的坑是_id范围查询漏数据。如果排序字段不是_id且值不唯一,用$gt会跳过相同值的记录。解决方法是改用复合游标,比如同时用_id和排序字段做条件。
6. 语义一致 CTA:从分页改造到长期编码,选对通道少走弯路
深度分页的改造不是一次性的,上亿数据的分页策略会随着业务增长不断调整。今天用游标解决了 MySQL,明天 ES 的数据量上来了又要调 SearchAfter 的批次大小,后天 MongoDB 的索引需要重建。这个过程里,有一个稳定的模型接入通道能省很多事。
如果你主要是排障和接入,建议先去 API Keys 页面创建 Key,然后对照接入文档把 Base URL、Key、Model ID 三件套配好。文档里有不同语言和工具的示例,照着改就行。
如果你需要验证模型输出,比如让模型帮你分析一段慢查询日志或者生成分页代码,可以用模型对话页面,直接贴内容进去问。
如果你长期要做编码和 Agent 任务,比如自动生成分页代码、自动跑验证脚本、自动分析执行计划,建议看 Coding Plan,它更适合高频、持续的编码场景。
最后分享一个实用技巧:每次改完分页代码,不要只看响应时间,一定要跑EXPLAIN或explain看扫描行数。响应时间可能因为缓存而看起来很快,但扫描行数骗不了人。另外,深页请求要连续跑 100 次以上,观察内存和 CPU 曲线,确认没有缓慢增长。分页改造的验收标准不是「能返回数据」,而是「扫描行数可控、内存平稳、无全表扫描」。