上亿数据怎么玩深度分页?兼容MySQL + ES + MongoDB 的 TaoToken 统一接入实践
1. 上亿数据深度分页为什么一翻就崩MySQL、ES、MongoDB 的真实瓶颈上亿数据怎么玩深度分页这个问题我在生产环境里踩过不止一次。先说结论深度分页本身能做但深度随机跳页必须禁止。你打开后台管理页面看到分页器上写着「共 142360 页」手一抖点了最后一页服务大概率直接给你表演一个超时或者 OOM。这不是危言耸听是真实发生过的。深度分页的核心矛盾在于数据库或搜索引擎为了返回第 N 页的 20 条数据往往需要先扫描并丢弃前面 (N-1)×20 条记录。当 N 达到几万甚至几十万时扫描量就是百万、千万级别。MySQL 和 MongoDB 作为专业数据库处理不好最多是慢但 ElasticSearch 不一样它本质是搜索引擎深度分页会触发分片广播和内存聚合写得不优雅直接内存溢出。三种存储的瓶颈各有不同。MySQL 的LIMIT offset, size会扫描 offsetsize 行然后扔掉前 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替代fromsize{ 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或refkey显示用到了联合索引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为IXSCANnReturned为 20totalKeysExamined接近 20。如果 stage 是COLLSCAN说明全表扫描。实测首页 8ms深页 15ms内存占用平稳。三种存储的对比表格如下存储首页响应深页响应扫描行数内存占用MySQL12ms18ms约 20几 MBES25ms35ms约 20/分片平稳MongoDB8ms15ms约 20平稳验证动作的关键是不要只看响应时间一定要看执行计划里的扫描行数。响应快但扫描百万行在高并发下照样崩。另外深页请求要连续跑多次观察内存是否持续增长排除游标泄漏。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 报错对照接入和验证过程中最容易卡在报错上。我整理了几个真实遇到的错误和排查方法。第一个是 401 Unauthorized。这个通常是 API Key 没填对或者 Base URL 和 Key 不匹配。检查你的配置文件里base_url是不是https://taotoken.net/apiapi_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 曲线确认没有缓慢增长。分页改造的验收标准不是「能返回数据」而是「扫描行数可控、内存平稳、无全表扫描」。