
1. 深翻页为什么越翻越慢从一次线上慢查询说起很多后端同学第一次真正意识到分页选型的重要性往往不是在写代码的时候而是在某个深夜被慢查询告警叫醒的时候。业务方反馈「订单列表翻到后面几页就转圈」你打开慢日志一看LIMIT 20 OFFSET 200000这条 SQL 扫了两百万行只为了吐出二十条记录。这就是偏移分页在深翻页场景下的经典表现偏移量越大数据库需要扫描并丢弃的行数越多耗时几乎线性增长。分页这件事看起来简单实际上它同时牵扯三个维度性能翻到第几页耗时是否恒定、一致性翻页过程中数据增删会不会导致重复或漏读、交互用户能不能直接跳到第 N 页。偏移分页、游标分页、键集分页这三种主流方案本质上就是在这三个维度上做不同的取舍。没有一种方案能同时拿满分选型的核心是搞清楚你的业务更在意哪一项。这篇内容聚焦后端 API 分页选型把三种方案的工作原理、SQL 类比、参数骨架都摊开讲清楚并且结合 TaoToken 统一 Key 与 API 通道给出一套可以直接复制去验证翻页一致性的配置骨架。适合正在设计列表接口、或者被深翻页性能问题困扰的后端与全栈开发者。读完之后你应该能对着自己的表结构判断出该用哪种分页并且知道怎么用统一通道快速跑通验证。2. TaoToken 统一 Key 前置把分页验证的调用通道先打通在真正对比分页方案之前得先解决一个工程上的现实问题验证分页行为需要反复发请求、改参数、看响应如果每个模型或每个服务都要单独配一套鉴权和地址光是环境切换就够烦的。TaoToken 在这里扮演的角色是一个统一的 API 通道你用同一个 Key 就能访问多种模型能力分页验证脚本不用为每个后端改一遍鉴权逻辑。先明确几个地址后面配置骨架里会反复用到官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基地址https://taotoken.net/api 这个不加 UTM直接作为请求前缀模型对话页https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel-chatCoding Planhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan控制台https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsoleAPI Keys 管理https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys接入文档https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdocClaudeCodeAnthropic 相关https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentclaudecode-anthropic拿到 Key 的路径很直接进 API Keys 页面创建一个复制出来存到环境变量里。我习惯用TAOTOKEN_API_KEY这个变量名后面所有脚本都从环境变量读避免把密钥硬编码进代码。export TAOTOKEN_API_KEYsk-你的key注意Key 只存在服务端环境变量或本地 shell 会话里不要提交到 Git也不要写进前端代码。分页验证脚本属于服务端工具天然适合放在后端跑。通道打通之后你就有了一条稳定的调用链路可以专心去对比分页参数的行为差异而不用在鉴权上反复折腾。如果你后续要做长期的编码或 Agent 类任务Coding Plan 那条通道会更合适只是临时验证分页用 API Keys 直接调就够了。3. 三种分页的可复制配置骨架这一节是全文的核心。我把偏移分页、游标分页、键集分页的参数结构、SQL 类比、以及对应的请求骨架都写出来你可以直接对着改。3.1 偏移分页limit offset 骨架偏移分页是最经典也最好理解的方案客户端传limit每页大小和offset偏移量服务端跳过前 offset 条返回 limit 条。SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 40;对应的 HTTP 请求骨架GET /api/v1/products?limit20offset40页码和偏移量的换算关系是offset (page_num - 1) * limit。它的优点是简单直观、支持随机跳页后台管理系统里用户想直接跳到第 5 页算一下 offset 就行。缺点也很明确offset 很大时数据库要扫描并丢弃大量行性能急剧下降而且两次查询之间如果有数据被删除整体前移会导致重复或漏读。3.2 游标分页limit cursor 骨架游标分页是现代 API 的首选客户端传一个不透明的cursor和limit服务端返回该游标之后的数据并在响应里带上下一页和上一页的游标。GET /api/v1/posts?limit20cursoreyJpZCI6MjQwfQ这个 cursor 通常是 Base64 编码的解码后可能代表最后一条记录的 ID比如 240。SQL 类比SELECT * FROM posts WHERE id 240 ORDER BY id LIMIT 20;它的优势是性能恒定无论翻到第几页都走索引不需要扫描跳过数据一致性也好游标之后查询不受之前数据增删影响。代价是不支持随机跳页只能顺序导航而且游标的生成和解析逻辑要自己设计通常要求唯一、有序、不透明。3.3 键集分页limit last_id 骨架键集分页可以看作游标分页的具体实现直接用有序的列值当游标实现起来比不透明令牌更简单。GET /api/v1/orders?limit20last_id589 GET /api/v1/orders?limit20last_created_at2023-10-01T12:00:00ZSQL 类比SELECT * FROM orders WHERE id 589 ORDER BY id LIMIT 20;它继承了游标分页的高性能和一致性实现门槛更低。但要注意排序字段必须唯一如果只用created_at而同一秒有多条记录就会出问题通常要组合created_at和id两个字段来保证顺序唯一。3.4 三种方案参数对照方法核心参数深翻页性能数据一致性随机跳页最佳场景偏移分页limit, offset差随 offset 线性劣化弱易重复漏读支持数据量小、需跳页的管理后台游标分页limit, cursor恒定走索引强不支持无限滚动、社交媒体流键集分页limit, last_id恒定走索引强不支持按时间戳/自增 ID 排序的列表另外还有时间范围分页since/until和 ID 范围分页since_id/max_id前者适合日志监控这类时间序列数据后者是键集分页的特例实现极简但依赖自增主键。页码分页page/size本质是偏移分页的另一种表现形式对前端友好但继承了全部缺点。4. 验证请求与成功结果用统一通道跑通翻页一致性光看参数不够得实际发请求验证。下面这段 Python 脚本用 TaoToken 统一通道做分页请求重点验证两件事翻页过程中数据变动是否导致重复以及深翻页耗时是否恒定。import os import time import requests API_BASE https://taotoken.net/api API_KEY os.environ[TAOTOKEN_API_KEY] headers { Authorization: fBearer {API_KEY}, Content-Type: application/json, } def fetch_page(method, limit20, offsetNone, cursorNone, last_idNone): params {limit: limit} if offset is not None: params[offset] offset if cursor is not None: params[cursor] cursor if last_id is not None: params[last_id] last_id start time.time() resp requests.get(f{API_BASE}/v1/items, headersheaders, paramsparams) elapsed time.time() - start resp.raise_for_status() return resp.json(), elapsed # 偏移分页翻到深页看耗时 for page in [1, 100, 1000]: offset (page - 1) * 20 data, cost fetch_page(offset, offsetoffset) print(foffset分页 page{page} offset{offset} 耗时{cost:.3f}s 返回{len(data.get(items, []))}条) # 键集分页顺序翻页记录 last_id last_id None seen set() for i in range(5): data, cost fetch_page(keyset, last_idlast_id) items data.get(items, []) ids [it[id] for it in items] dup seen set(ids) print(fkeyset第{i1}页 耗时{cost:.3f}s 重复{dup}) seen.update(ids) if items: last_id items[-1][id]跑通之后你会看到两个关键现象偏移分页在 page1000 时耗时明显高于 page1而键集分页每页耗时基本持平如果在翻页中途删掉一条记录偏移分页的下一页会出现重复 ID键集分页则不会。这就是选型时最该关注的实测证据。成功结果的判断标准很简单键集/游标分页各页耗时方差很小且seen集合里没有重复偏移分页在深页耗时上升且数据变动后出现重复。把这两个现象跑出来你对分页的理解就从纸面落到了真实接口上。5. 本篇常见错排查5.1 游标分页返回空但明明还有数据最常见的原因是游标编码/解码时丢了排序字段。比如你用id排序但游标里只存了created_at解码后WHERE created_at xxx可能跳过一批同时间的记录。排查方法把游标 Base64 解码打印出来确认里面包含了完整的排序键。键集分页同理排序字段不唯一时一定要组合多个字段。5.2 偏移分页深翻页超时如果暂时不能改成分页方案可以先加一层约束限制最大 offset超过就返回错误提示用户用筛选条件缩小范围。或者用「延迟关联」优化先走覆盖索引查出主键再回表SELECT * FROM products INNER JOIN (SELECT id FROM products ORDER BY id LIMIT 20 OFFSET 200000) AS t ON products.id t.id;但这只是缓解根治还是得换键集分页。5.3 键集分页排序字段有重复值created_at精确到秒时同一秒的多条记录会导致WHERE created_at xxx漏掉部分数据。解决办法是用复合游标同时比较两个字段SELECT * FROM orders WHERE (created_at, id) (2023-10-01T12:00:00Z, 589) ORDER BY created_at, id LIMIT 20;5.4 统一通道请求 401 或 403先确认TAOTOKEN_API_KEY环境变量在当前 shell 里真的存在echo $TAOTOKEN_API_KEY看一下。如果 Key 没问题检查请求头是不是Bearer前缀漏了空格。接入文档里有完整的鉴权示例对照一下就能定位。5.5 翻页结果顺序不稳定如果 SQL 里ORDER BY的字段不唯一数据库返回顺序可能每次不同导致翻页错乱。任何分页方案都要保证排序键唯一最稳妥的是在排序里加上主键作为最后一级。6. 选型落地与后续动作把上面的内容收拢成一句可执行的判断面向公众、数据量大的列表接口直接上键集分页或游标分页性能和一致性都稳内部管理后台、数据量不大且需要跳页的偏移分页够用别过度设计。时间序列数据日志、监控优先考虑时间范围分页配合 limit。落地时的顺序建议是先确定排序键是否唯一再选分页方案最后用第 4 节的脚本跑一遍一致性和耗时验证。验证通过再上生产比拍脑袋选型靠谱得多。如果你在接入或排障过程中卡住了可以直接去 API Keys 页面确认密钥状态或者翻接入文档对照请求格式API Keys 管理在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys 接入文档在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 。需要快速验证模型返回是否符合预期用模型对话页 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel-chat 最省事。而如果你接下来要做长期的编码或 Agent 任务分页验证只是其中一环Coding Plan 通道 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 更适合持续调用。