1. 从一次线上分页接口超时说起Node.js 里 MongoDB 查询分页到底该怎么写分页这件事看起来就是skip加limit两行代码但真正放到生产环境里问题往往出在数据量上来之后。我遇到过最典型的一次是一个订单列表接口前几个月一直很稳等单表过了两百万行运营翻到第 300 页时接口直接超时日志里executionTimeMillis飙到四位数。后来排查下来根因就是深分页时skip要扫描并丢弃前面所有文档页码越深越慢。这篇内容聚焦 Node.js MongoDB 查询分页的完整链路从最基础的skip/limit到大数据量下的游标分页取舍再到索引设计和explain验证最后把模型调用侧的鉴权配置也用 TaoToken 统一 Key 跑通。适合正在写 Node.js 后端、用 Mongoose 操作 MongoDB、并且已经感受到分页性能压力的开发者。如果你还在用find().skip().limit()一把梭或者不确定什么时候该换游标分页下面的步骤可以直接跟着做。我会用一个「文章列表」集合作为例子字段包括title、author、createdAt、status模拟真实业务里按时间倒序翻页的场景。整个过程分三块先把基础分页写对再用索引和explain验证最后处理深分页和模型调用鉴权。每一步都有可复制的代码和命令不玩虚的。先说结论方向小数据量、页码浅skip/limit完全够用数据量大、页码深优先游标分页排序字段一定要有索引否则skip再优化也白搭。下面按顺序展开。2. TaoToken 前置准备统一 Key 与 API 通道配置让分页链路里的模型调用也能鉴权分页接口本身是数据库操作为什么这里要提 TaoToken因为很多分页场景不只是查库还要在返回前做摘要、打标签、生成推荐语这些都要调模型。如果每个服务各自维护一套 Key配置散落各处排查问题时非常痛苦。用 TaoToken 统一 Key 和 API 通道可以把模型调用侧的鉴权收敛到一处和数据库分页逻辑解耦。TaoToken 是一个统一模型调用入口官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。它的作用是让你用一套 Key 访问不同模型省去在多个平台之间来回切换配置。对于分页接口这种需要「查库 调模型」的组合场景统一鉴权能减少环境变量管理的混乱。前置准备分三步。第一步拿到 API Key。登录后进入控制台在 API Keys 页面创建地址是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。创建后复制保存后面写进.env。第二步确认你要用的模型 ID可以在模型对话页面先试跑一次地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 确认模型能正常返回再写进代码。第三步如果你用的是 Claude Code 这类编码工具接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Base URL、Key、Model ID 三件套的填法。这里要强调一个原则数据库连接串和模型 API Key 都放.env不要硬编码。分页接口的鉴权配置和数据库配置分开管理出问题时能快速定位是库的问题还是模型通道的问题。下面给一个.env示例路径就是项目根目录的.env# .env MONGO_URImongodb://localhost:27017/blogdb TAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODEL_ID你的模型ID注意TAOTOKEN_BASE_URL只写到/api不要带多余路径。Key 不要提交到 Git.gitignore里加上.env。这一步做完后面分页接口里如果要调模型做摘要直接读环境变量即可不用再关心鉴权细节。3. 可复制配置Mongoose 分页查询、索引与 settings 片段一次给全这一节是核心直接给可复制的代码和配置。先建模型再写分页函数然后补索引最后给一个 JSON 形式的配置片段方便你对照。先看模型定义文件路径models/Article.js// models/Article.js const mongoose require(mongoose); const articleSchema new mongoose.Schema({ title: { type: String, required: true }, author: { type: String, index: true }, status: { type: String, enum: [draft, published], default: published }, createdAt: { type: Date, default: Date.now } }); // 复合索引支撑按状态过滤 按时间倒序的分页 articleSchema.index({ status: 1, createdAt: -1 }); module.exports mongoose.model(Article, articleSchema);这个复合索引是关键。分页查询通常是find({ status: published }).sort({ createdAt: -1 })索引字段顺序要和查询条件、排序字段一致status在前、createdAt倒序在后才能让 MongoDB 走索引扫描而不是全表扫描。接着写基础分页函数文件路径services/pagination.js// services/pagination.js const Article require(../models/Article); // 基础 skip/limit 分页 async function listBySkip(page 1, pageSize 10) { const skip (page - 1) * pageSize; const [list, total] await Promise.all([ Article.find({ status: published }) .sort({ createdAt: -1 }) .skip(skip) .limit(pageSize) .lean(), Article.countDocuments({ status: published }) ]); return { list, total, page, pageSize }; } module.exports { listBySkip };.lean()返回普通对象而不是 Mongoose 文档减少序列化开销列表接口建议都加上。countDocuments和find用Promise.all并发减少一次往返等待。然后是游标分页适合深分页场景文件路径同上// services/pagination.js 追加 async function listByCursor(cursor, pageSize 10) { const query { status: published }; if (cursor) { // 游标是上一页最后一条的 createdAt query.createdAt { $lt: new Date(cursor) }; } const list await Article.find(query) .sort({ createdAt: -1 }) .limit(pageSize) .lean(); const nextCursor list.length ? list[list.length - 1].createdAt.toISOString() : null; return { list, nextCursor }; }游标分页不返回总页数只返回下一页游标翻页时把nextCursor传回来即可。它不会随着页码加深而变慢因为每次都是createdAt 游标的范围查询走索引。如果你用 Claude Code 或 Cline 这类工具辅助写代码模型接入的配置片段可以写成这样路径~/.claude/settings.json或对应工具的 settings 文件{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的key, ANTHROPIC_MODEL: 你的模型ID } }三件套就是 Base URL、Key、Model ID缺一不可。填完后工具里的模型请求会走统一通道和你的分页服务互不干扰。4. 验证请求与成功结果用 explain 和真实请求确认分页走索引代码写完不能只看返回有没有数据要确认查询计划走了索引。MongoDB 的explain是最直接的手段。先连上库在mongosh里执行db.articles.find({ status: published }) .sort({ createdAt: -1 }) .skip(0) .limit(10) .explain(executionStats)重点看三个字段stage应该是IXSCAN而不是COLLSCANtotalKeysExamined和nReturned应该接近说明没有扫描大量无用文档executionTimeMillis在浅分页时应该是个位数。如果看到COLLSCAN说明索引没生效回去检查索引字段顺序。再验证深分页的差异。把skip改成 200000db.articles.find({ status: published }) .sort({ createdAt: -1 }) .skip(200000) .limit(10) .explain(executionStats)你会看到totalKeysExamined大幅上升executionTimeMillis明显变长。这就是深分页的代价。换成游标查询db.articles.find({ status: published, createdAt: { $lt: ISODate(2024-01-01T00:00:00Z) } }) .sort({ createdAt: -1 }) .limit(10) .explain(executionStats)这个查询的totalKeysExamined会稳定在limit附近不随数据位置变化。实测下来两百万行数据里skip到 20 万页耗时是游标查询的几十倍具体倍数取决于机器但趋势一致。接口层面用 curl 验证分页返回curl http://localhost:3000/api/articles?page1pageSize10成功返回结构类似{ list: [{ title: 示例文章, createdAt: 2024-06-01T10:00:00.000Z }], total: 2000000, page: 1, pageSize: 10 }游标接口则是curl http://localhost:3000/api/articles/cursor?cursor2024-06-01T10:00:00.000ZpageSize10返回里带nextCursor前端拿这个值请求下一页。两种接口都跑通说明分页链路完整。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 逐个对照分页链路里报错分两类数据库侧和模型调用侧。数据库侧常见的是查询超时、索引没生效模型侧常见的是鉴权失败。下面按真实报错逐个对照。第一个401 Unauthorized。如果你在分页接口里调模型做摘要返回 401基本是 Key 没配对或没加载。检查.env里TAOTOKEN_API_KEY是否写对代码里是否用了dotenv加载。注意 Key 前后不要有空格复制时容易带上。如果用的是 Claude Code 这类工具检查 settings 里的ANTHROPIC_API_KEY是否和 Base URL 匹配。第二个local proxy failed。这个报错通常出现在工具配置了本地代理但代理没启动或者 Base URL 填成了本地地址。检查你的 Base URL 是不是https://taotoken.net/api不要填localhost或带端口。如果你在 settings 里配了ANTHROPIC_BASE_URL确认没有多余斜杠或路径。第三个reading choices相关报错。这类报错一般是模型返回结构不符合预期常见于模型 ID 填错或者请求体格式不对。先确认TAOTOKEN_MODEL_ID是有效的模型 ID可以在模型对话页面试跑确认。如果分页接口里拼请求体检查messages字段格式是否正确。第四个OAuth相关报错。如果你用 Codex 或类似工具鉴权走的是auth.json路径通常在~/.codex/auth.json。这个文件里的配置要和 Base URL、Key 对应。如果报 OAuth 失败检查文件权限和内容格式不要手动改乱结构。三件套 Base URL、Key、Model ID 要一致。数据库侧还有一个高频坑countDocuments在大数据量下也慢。如果分页接口不需要精确总数可以改成估算或者缓存总数。另外skip和sort同时用时如果排序字段没索引MongoDB 会在内存里排序数据量大时可能触发内存限制报错。解决办法就是加复合索引字段顺序和查询一致。排查顺序建议先看数据库explain确认查询计划再看模型调用日志确认鉴权和模型 ID最后看接口层参数确认page、pageSize、cursor传参正确。这样能快速定位是库的问题还是通道的问题。6. 语义一致 CTA分页跑通后把模型调用和编码工具也接上统一通道分页接口跑通只是第一步。真实业务里列表返回后往往还要做内容摘要、标签生成、推荐排序这些都要调模型。与其在每个服务里散落配置不如把模型调用统一到 TaoToken 通道。如果你主要做模型调用验证想先确认模型能不能正常返回可以去模型对话页面试跑地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。在这里选模型、发请求确认通道通畅后再写进代码。如果你在写分页服务时需要管理 Key去 API Keys 页面创建和查看地址是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。创建后写进.env和数据库连接串分开管理。如果你长期做编码和 Agent 开发需要稳定的模型通道可以看 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。它适合把模型调用固化到日常开发流程里减少反复配置。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Base URL、Key、Model ID 的完整说明。Claude Code 的接入参考 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite 按文档填三件套即可。最后给一个实用技巧分页接口的pageSize设个上限比如最大 100防止有人传pageSize100000把库拖垮。游标分页的cursor要做格式校验非法值直接返回 400不要让它进查询。索引建好后定期用explain抽查慢查询尤其是数据量增长后查询计划可能变化。这些细节做好分页接口才能长期稳定。