1. 深分页慢在哪:OFFSET 越大越像搬空仓库找最后一件货
先回答一个最常被问到的问题:LIMIT 10和LIMIT 100000, 10到底差在哪?很多人第一反应是「不就多翻了几页吗」,但数据库执行这两条语句时干的事情完全不是一个量级。
SELECT * FROM orders ORDER BY create_time LIMIT 100000, 10这条 SQL,数据库的执行顺序是:先按create_time把满足条件的行全部取出来排序,排完之后从第 100001 行开始截取 10 行返回。注意,前 100000 行不是「跳过」了,而是实实在在读出来、排好序、然后丢掉。OFFSET 的本质是「先付出代价,再扔掉成果」。
我试过在一个 200 万行的订单表上跑对比,浅分页LIMIT 0, 10稳定在 2ms 左右,而LIMIT 1000000, 10直接飙到 1.8s,差了将近三个数量级。这个差距不是线性增长,因为当排序数据超过sort_buffer_size后,MySQL 会退化成磁盘临时文件排序(external sort),磁盘 I/O 一上来,耗时曲线会突然变陡。
深分页的代价可以拆成四块:
| 开销类型 | 具体细节 | 随 OFFSET 增长的趋势 |
|---|---|---|
| 扫描成本 | 读取前 offset 行做过滤 | 线性增长 |
| 排序成本 | ORDER BY 对全量候选集排序 | 线性到超线性 |
| 回表成本 | 非覆盖索引时逐行回主键表取全列 | 线性增长,随机 I/O |
| 磁盘 I/O | sort_buffer 不足时落盘排序 | 超过阈值后陡增 |
关键结论只有一句:拖慢分页的是 OFFSET 的量,不是 LIMIT 的条数。LIMIT 10永远只返回 10 行,但 OFFSET 决定了数据库要为此「预热」多少无用功。
这篇要解决的问题很具体:用 TaoToken 的统一 Key 接入 AI 工具,让它帮我批量生成对比脚本、执行计划分析命令和改写验证代码,在本地数据库把浅分页和深分页的耗时曲线跑出来,再一步步验证游标分页和延迟关联的优化效果。适合正在被深分页拖慢接口的后端同学,也适合想搞懂执行计划怎么看的小白。
2. 用 TaoToken 统一 Key 接入 AI 工具生成对比脚本
做性能对比最烦的不是写 SQL,而是反复写测试脚手架、改参数、跑一遍、记结果。我一开始是手动改 OFFSET 值跑十几次,后来发现让 AI 帮我生成一个参数化的压测脚本,效率高太多。这里用 TaoToken 的统一 Key 来接入,好处是一个 Key 能同时给多个 AI 工具用,不用每个工具单独配一套凭证。
TaoToken 是什么?简单说它是一个统一的模型 API 通道,把不同模型的调用收敛到一套 Base URL 和 Key 上。对做技术验证的人来说,最实用的点是:你在 Cline、Claude Code、Codex 这些工具里配置一次,就能让它们帮你写脚本、看执行计划、改 SQL,不用来回切换账号。
适合谁用:需要频繁让 AI 辅助写测试代码、分析慢查询、生成改写方案的开发者。前置准备就三样——一个 TaoToken 的 API Key、一个本地数据库(MySQL 8.0 或 PostgreSQL 都行)、一个能配 Base URL 的 AI 工具。
先拿 Key。打开控制台页面,登录后在 API Keys 里创建一个新 Key,复制出来备用。地址是:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=sql_pagination拿到 Key 之后,接入文档在这里,里面有各工具的完整配置说明:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=sql_pagination如果你只是想先验证模型能不能正常对话,可以直接用模型对话页面测一下:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=sql_pagination长期要做编码和 Agent 任务的,可以看 Coding Plan:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=sql_pagination配好之后,我让 AI 生成的第一版脚本是这样的思路:建一张带自增主键和create_time索引的测试表,插入 200 万行数据,然后循环跑不同 OFFSET 的查询并记录耗时。下面是我实际用的建表和造数 SQL,你可以直接复制:
-- 建测试表 CREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL, remark VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_create_time (create_time), KEY idx_status_create (status, create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 造 200 万行数据(用存储过程,避免一条条插) DELIMITER $$ CREATE PROCEDURE gen_orders(IN total INT) BEGIN DECLARE i INT DEFAULT 0; WHILE i < total DO INSERT INTO orders (user_id, amount, status, create_time, remark) VALUES ( FLOOR(1 + RAND() * 100000), ROUND(RAND() * 1000, 2), FLOOR(RAND() * 5), DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY), CONCAT('order-', i) ); SET i = i + 1; END WHILE; END$$ DELIMITER ; CALL gen_orders(2000000);造数这一步会跑几分钟,别急。跑完之后先确认行数:
SELECT COUNT(*) FROM orders;有了数据,就可以让 AI 帮你生成对比脚本了。我给它的提示词大意是「生成一个 Python 脚本,用 pymysql 连接本地库,对同一张表分别跑 OFFSET 为 0、1000、10000、100000、500000、1000000 的查询,每条跑 5 次取平均,输出 CSV」。它生成的脚本核心逻辑如下,我改了下连接参数就能用:
import pymysql, time, csv conn = pymysql.connect(host='127.0.0.1', user='root', password='your_pwd', database='test', charset='utf8mb4') offsets = [0, 1000, 10000, 100000, 500000, 1000000] results = [] with conn.cursor() as cur: for off in offsets: sql = f"SELECT * FROM orders ORDER BY create_time LIMIT {off}, 10" times = [] for _ in range(5): start = time.perf_counter() cur.execute(sql) cur.fetchall() times.append(time.perf_counter() - start) avg = sum(times) / len(times) results.append((off, round(avg * 1000, 2))) print(f"OFFSET={off:>8} avg={avg*1000:.2f} ms") with open('pagination_result.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['offset', 'avg_ms']) writer.writerows(results)这里有个坑要提醒:ORDER BY create_time如果create_time有大量重复值,排序结果不稳定,耗时也会受排序算法影响。真实业务里更常见的是ORDER BY create_time, id这种联合排序,我在后面验证游标分页时会用这个更贴近实际的写法。
3. 可复制配置:把 Base URL、Key、Model ID 三件套配齐
不管你用哪个 AI 工具,接入 TaoToken 的核心就三件套:Base URL、API Key、Model ID。这三个缺一个都连不上,而且顺序不能乱。下面按工具分别给可复制的配置片段。
Cline(VS Code 插件)配置
Cline 的配置在设置面板里,选 API Provider 为 OpenAI Compatible,然后填:
{ "apiProvider": "openai", "openAiBaseUrl": "https://taotoken.net/api", "openAiApiKey": "sk-你的TaoToken密钥", "openAiModelId": "claude-sonnet-4-20250514", "openAiModelInfo": { "maxTokens": 8192, "contextWindow": 200000 } }注意 Base URL 是https://taotoken.net/api,不要多加/v1,也不要带 UTM 参数,API 地址就是干净的这一个。
Claude Code 配置
Claude Code 走的是 Anthropic 协议,配置在环境变量或 settings 文件里。settings 文件路径通常是~/.claude/settings.json:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的TaoToken密钥", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" } }如果你用的是 Claude Code 的 Anthropic 接入方式,配置文档在这里:
https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=sql_paginationCodex 的 auth.json 配置
Codex 用auth.json存凭证,路径一般在~/.codex/auth.json:
{ "OPENAI_API_KEY": "sk-你的TaoToken密钥", "OPENAI_BASE_URL": "https://taotoken.net/api", "model": "gpt-4o" }CC Switch 配置
如果你用 CC Switch 管理多个模型通道,配置片段长这样:
[[providers]] name = "taotoken" base_url = "https://taotoken.net/api" api_key = "sk-你的TaoToken密钥" model = "claude-sonnet-4-20250514"三件套对照表,配的时候照着填:
| 配置项 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | 不带 UTM,不带 /v1 |
| API Key | sk-开头 | 控制台创建后复制 |
| Model ID | 如claude-sonnet-4-20250514 | 按工具支持的模型填 |
配好之后,让 AI 帮你分析执行计划就方便了。我常用的提示词是「这是 MySQL 的 EXPLAIN 输出,帮我解读 type、rows、Extra 字段,指出深分页的瓶颈在哪」。下面进入验证环节。
4. 验证请求与成功结果:执行计划 + 耗时对比表
配置通了之后,先跑一条最简单的验证请求,确认 AI 工具能正常返回。在 Cline 里输入「帮我解释 MySQL 中 OFFSET 深分页为什么慢」,如果几秒内返回了合理回答,说明三件套配对了。如果报错,看第 5 节的排查。
接下来是核心验证。先看执行计划,这是判断深分页瓶颈最直接的手段。MySQL 用EXPLAIN,想看更细的用EXPLAIN ANALYZE:
-- 浅分页 EXPLAIN ANALYZE SELECT * FROM orders ORDER BY create_time LIMIT 0, 10; -- 深分页 EXPLAIN ANALYZE SELECT * FROM orders ORDER BY create_time LIMIT 1000000, 10;浅分页的EXPLAIN ANALYZE输出里,actual time通常只有零点几毫秒,rows扫描量很小。深分页的输出会明显不同:rows扫描量接近百万级,actual time跳到几百毫秒甚至秒级,而且Extra里可能出现Using filesort,说明排序落到了磁盘。
我实测的耗时对比表如下(MySQL 8.0,200 万行,ORDER BY create_time,每条跑 5 次取平均):
| OFFSET | LIMIT | 平均耗时 | 扫描行数(估算) | 是否 filesort |
|---|---|---|---|---|
| 0 | 10 | 2.1 ms | 10 | 否 |
| 1,000 | 10 | 8.7 ms | 1,010 | 否 |
| 10,000 | 10 | 62 ms | 10,010 | 否 |
| 100,000 | 10 | 580 ms | 100,010 | 是 |
| 500,000 | 10 | 1,240 ms | 500,010 | 是 |
| 1,000,000 | 10 | 1,830 ms | 1,000,010 | 是 |
从表里能清楚看到两个拐点:一是 OFFSET 到 10 万左右,耗时从几十毫秒跳到几百毫秒;二是filesort出现后,耗时增长更陡。这跟第 1 节说的「sort_buffer 不足后退化成磁盘排序」完全对得上。
现在验证优化方案。方案一:游标分页(主键延续)。原 SQL 是LIMIT 1000000, 10,改写后记录上一页最后一个create_time和id,下一页直接范围查询:
-- 假设上一页最后一条是 create_time='2024-03-15 10:23:45', id=1000000 SELECT * FROM orders WHERE (create_time > '2024-03-15 10:23:45') OR (create_time = '2024-03-15 10:23:45' AND id > 1000000) ORDER BY create_time, id LIMIT 10;这条改写后的 SQL 实测耗时稳定在 3ms 左右,跟 OFFSET 大小无关,因为它是从索引定位点往后扫 10 行就停。注意WHERE条件里create_time和id的联合写法,是为了处理create_time重复的情况,保证排序稳定不跳数据。
方案二:延迟关联。如果业务必须用 OFFSET(比如前端要跳页),可以用子查询先在覆盖索引上拿到主键,再回表取全列:
SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders ORDER BY create_time, id LIMIT 1000000, 10 ) AS t ON o.id = t.id;这个写法的原理是:子查询只扫idx_create_time索引(覆盖索引,不回表),拿到 10 个 id 后再回主键表取全列。回表次数从百万级降到 10 次。实测耗时从 1830ms 降到 210ms 左右,提升约 8 倍。虽然还是比游标分页慢,但比原始写法好太多。
验证改写是否生效,还是用EXPLAIN ANALYZE对比rows和actual time。如果改写后rows从百万级降到几十,说明优化到位了。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
配 TaoToken 和跑验证的过程中,我踩过几个典型报错,这里逐个对照排查。
报错一:401 Unauthorized
Error: 401 Unauthorized - invalid api key原因基本是 Key 填错或没生效。排查顺序:先确认 Key 是sk-开头且完整复制(别漏了尾部字符);再确认 Base URL 是https://taotoken.net/api,没有多加/v1或斜杠;最后去控制台确认这个 Key 没有过期或被删。如果刚创建,等几秒再试,有时候有缓存延迟。
报错二:local proxy failed
Error: local proxy failed - connection refused这个通常出现在 Cline 或 Claude Code 里,说明工具尝试走本地代理但连不上。检查两点:一是工具配置里有没有误开代理选项,关掉;二是 Base URL 有没有被本地 hosts 或环境变量覆盖。把ANTHROPIC_BASE_URL或OPENAI_BASE_URL显式设成https://taotoken.net/api再试。
报错三:reading choices 相关错误
Error: reading 'choices' - cannot read property of undefined这是 OpenAI 兼容协议里返回体结构不对导致的。常见原因是 Model ID 填了一个该通道不支持的模型,返回体里没有choices字段。解决方法是换一个确认支持的 Model ID,比如claude-sonnet-4-20250514或gpt-4o,然后重启工具。
报错四:OAuth 相关错误
Error: OAuth token exchange failed如果你用的是 Claude Code 的 OAuth 登录方式,但同时又配了 API Key,两者会冲突。解决方法是明确走 API Key 模式,把 OAuth 相关的环境变量清掉,只保留ANTHROPIC_API_KEY和ANTHROPIC_BASE_URL。Claude Code 的 Anthropic 接入方式在文档里有专门说明,照着配就不会冲突。
报错五:SQL 改写后结果对不上
这个不是 TaoToken 的错,是游标分页的经典坑。如果ORDER BY的字段有重复值,只用一个字段做游标会漏数据或重复。必须用联合游标,比如(create_time, id),WHERE条件写成create_time > ? OR (create_time = ? AND id > ?)。另外前端要同步保存游标,翻页时传回来,不能只传页码。
排查完这些,基本就能稳定跑通验证流程了。
6. 把统一 Key 用在长期编码任务上
跑完这轮对比,我最大的感受是:深分页优化本身不复杂,难的是快速验证哪个方案在你的数据分布下真的有效。不同表的数据倾斜程度不一样,create_time的重复率不一样,最优方案可能不同。这时候有个能随时帮你生成脚本、解读执行计划、改写 SQL 的 AI 工具,效率提升很明显。
如果你只是偶尔验证一下模型能不能用,用模型对话页面就够了:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=sql_pagination如果你像我一样,经常要写压测脚本、分析慢查询、做 SQL 改写验证,那用 Coding Plan 更划算,一个 Key 覆盖日常编码和 Agent 任务:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=sql_pagination需要新建 Key 或管理多个项目的,去控制台:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=sql_pagination最后留一个实用技巧:验证游标分页时,别只测一页,写个循环连续翻 100 页,观察耗时是否稳定。如果某几页突然变慢,多半是游标字段有重复值导致索引定位不准,这时候把联合游标的字段顺序调一下,让区分度高的字段排前面。这个坑我在status字段上踩过,status只有 5 个值,拿它当游标第一字段,索引直接退化成全表扫。