1. 游标查询为什么突然变慢:从一次真实慢查询说起
PostgreSQL 的游标(CURSOR)在存储过程和分批取数场景里用得很多,但很多人会遇到一个诡异现象:同一条 SQL 单独跑 EXPLAIN 走的是索引 A,一旦包进DECLARE ... CURSOR FOR就换成了索引 B,而且慢得离谱。这个问题的核心检索词就是PostgreSQL 游标索引选择偏差,它属于优化器在执行计划层面的判断失误,不是 SQL 写错了。
先说清楚游标是什么、能做什么、适合谁。游标本质上是把一个大结果集拆成多次 FETCH 的机制,适合在存储过程里逐行处理、或者应用层分批拉取百万级数据避免一次性占满内存。适合做报表导出、批量对账、数据迁移的开发者。但游标有个隐藏行为:它默认只按“前 10% 的行”来估算代价,这个比例由cursor_tuple_fraction控制,默认值 0.1。
为什么这会出问题?优化器在生成普通查询计划时,假设需要取回全部行,会老老实实按整体代价选索引。但游标场景下,它认为你大概率只取前面一小部分就停了,于是偏向选择“启动快”的计划,比如按主键顺序扫描再过滤,而不是先走选择性高的复合索引再排序。当数据分布不均匀——比如你要查的那批记录恰好堆在表的末尾——这个“前 10%”的假设就彻底错了,执行计划自然选错。
我试过在一个千万级账单表上复现这个问题:单条SELECT走idx_tbl_2复合索引,耗时几百毫秒;改成游标后走了idx_tbl_1主键索引,全表过滤,直接飙到几十秒。根因不在 SQL,而在优化器对数据分布的默认均匀假设,加上游标的快速启动偏好,两者叠加就翻车了。
要定位这类问题,第一步永远是看执行计划。普通查询用EXPLAIN (ANALYZE, BUFFERS),游标场景要先用BEGIN开启事务,再EXPLAIN DECLARE ... CURSOR FOR ...,对比两者的Index Scan节点差异。下面几节我会给出可复制的建表、造数据、索引调整、游标改写,以及把验证动作统一到 TaoToken 通道后的执行计划对比方法,让你能独立复现并确认优化是否生效。
2. TaoToken 前置准备:统一 Key 与 API 通道
在动手改 SQL 之前,先把验证环境统一起来。做 PostgreSQL 优化时经常需要在多个工具之间切换——命令行 psql、图形化客户端、脚本化的执行计划采集工具,如果每个工具都单独配一套连接和密钥,排查效率会很低。TaoToken 提供统一的 Key 和 API 通道,可以把模型对话、代码辅助、执行计划分析这些动作收敛到一个入口,减少环境差异带来的干扰。
你需要先拿到一个可用的 API Key。访问控制台创建密钥,地址是 https://taotoken.net/api-keys ,创建后复制保存,注意 Key 只在创建时完整显示一次。如果你还要用命令行工具或脚本调用,接入文档在 https://taotoken.net/doc ,里面有 Base URL、鉴权头、请求格式的完整说明。
这里要强调三件套的概念:无论你用哪种客户端,接入任何兼容 OpenAI 协议的工具,都必须同时配好Base URL + API Key + Model ID,缺一不可。Base URL 统一填https://taotoken.net/api,注意这个地址不带任何查询参数。API Key 就是你刚创建的那串。Model ID 按你实际要用的模型填,比如做代码分析和执行计划解读时选一个擅长推理的模型。
如果你用的是 Claude Code 这类编码工具,接入方式略有不同,需要走 Anthropic 兼容通道,具体路径参考 https://taotoken.net/claude-code 。而如果你打算长期做数据库优化、写脚本、跑 Agent 自动分析执行计划,建议直接上 Coding Plan,地址是 https://taotoken.net/coding-plan ,它更适合高频、长期的编码与 Agent 场景,比按次调用更划算。
配好之后,你可以先用模型对话页面做一次连通性验证,地址 https://taotoken.net/chat ,随便问一句确认返回正常。这一步不是走形式——后面第五节排查报错时,很多问题(401、连接失败、返回体解析异常)都要靠这个基础通道是否正常来快速定位。环境统一了,接下来所有执行计划的采集、对比、解读才能在一个稳定的通道里完成。
3. 可复制配置:建表、造数据与索引调整
这一节给你一套可以直接粘贴执行的 SQL,完整复现游标索引选择偏差。先建测试表,字段设计成能体现数据分布不均的场景:
-- 建表 CREATE TABLE tbl ( id int, c1 int, c2 int, c3 int, c4 int ); -- 第一批:1000 万条随机数据,c1/c2 分布在 0~100 INSERT INTO tbl SELECT generate_series(1, 10000000), (random()*100)::int, (random()*100)::int, (random()*100)::int, (random()*100)::int; -- 第二批:100 万条,c1=c2=200,全部堆在表末尾 INSERT INTO tbl SELECT generate_series(10000001, 11000000), 200, 200, 200, 200; -- 两个候选索引 CREATE INDEX idx_tbl_1 ON tbl(id); CREATE INDEX idx_tbl_2 ON tbl(c1, c2, c3, c4); -- 收集统计信息,这一步不能省 VACUUM ANALYZE tbl;关键点在于第二批数据的c1=200 AND c2=200全部集中在 id 较大的位置,而优化器默认认为数据均匀分布,这就是偏差的根源。先看普通查询的执行计划:
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tbl WHERE c1 = 200 AND c2 = 200 ORDER BY id;正常情况会走Bitmap Index Scan on idx_tbl_2,因为复合索引能直接过滤掉 1000 万行里的大部分数据。再看游标场景:
BEGIN; EXPLAIN DECLARE tt CURSOR FOR SELECT * FROM tbl WHERE c1 = 200 AND c2 = 200 ORDER BY id;你会发现计划变成了Index Scan using idx_tbl_1,走主键索引逐行扫描再 Filter。原因就是cursor_tuple_fraction默认 0.1,优化器只按前 10% 的行估算,认为快速启动的主键扫描更划算。
调整手段有三种,按场景选。第一种是临时改参数,在会话级别把比例调回全量估算:
SET cursor_tuple_fraction = 1.0; BEGIN; EXPLAIN DECLARE tt CURSOR FOR SELECT * FROM tbl WHERE c1 = 200 AND c2 = 200 ORDER BY id;第二种是改写游标 SQL,用 CTE 或子查询强制物化,让优化器按完整结果集估算:
BEGIN; DECLARE tt CURSOR FOR WITH filtered AS MATERIALIZED ( SELECT * FROM tbl WHERE c1 = 200 AND c2 = 200 ) SELECT * FROM filtered ORDER BY id;第三种是索引层面调整,如果这类查询高频出现,可以建一个更贴合排序需求的复合索引:
CREATE INDEX idx_tbl_3 ON tbl(c1, c2, id);这样WHERE c1=200 AND c2=200 ORDER BY id可以直接走索引有序扫描,省掉 Sort 节点。如果你要把这些配置和验证脚本固化到项目里,可以用一份 JSON 描述连接与模型参数,方便脚本读取:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的密钥", "model_id": "你的模型ID", "pg_conn": "host=127.0.0.1 port=5432 dbname=bill user=postgres", "explain_options": "ANALYZE, BUFFERS, FORMAT JSON" }注意 Base URL 不带 UTM 参数,Key 不要硬编码进版本库,用环境变量注入。这套配置配好后,执行计划的采集和对比就能脚本化,下一节讲怎么验证。
4. 验证请求与成功结果:执行计划对比
改完之后必须验证,不能凭感觉说“好像快了”。验证的核心动作是对比优化前后的执行计划,重点看三个指标:访问路径(Index Scan 类型)、估算行数 vs 实际行数、以及 Buffers 命中情况。
先采集优化前的基线。用 JSON 格式输出,方便脚本解析:
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM tbl WHERE c1 = 200 AND c2 = 200 ORDER BY id;游标场景的基线要单独采:
BEGIN; EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) DECLARE tt CURSOR FOR SELECT * FROM tbl WHERE c1 = 200 AND c2 = 200 ORDER BY id;然后应用上一节的调整,比如设置cursor_tuple_fraction = 1.0后重新采集,对比两次 JSON 输出里的Node Type和Actual Rows。成功的标志是:游标场景的访问路径从Index Scan using idx_tbl_1变成Bitmap Index Scan on idx_tbl_2或Index Scan using idx_tbl_3,并且Actual Rows与Plan Rows的差距明显缩小。
如果你把执行计划分析接到 TaoToken 通道,可以用脚本把 JSON 计划发给模型做解读,请求体大致如下:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "你的模型ID", "messages": [ {"role": "user", "content": "分析这份 PostgreSQL 执行计划,指出索引选择是否合理:<粘贴JSON>"} ] }'返回正常时你会拿到一段结构化的分析,指出哪个节点代价最高、估算偏差在哪。这一步的价值在于:人工看 JSON 计划容易漏掉rows估算偏差,模型能快速定位到Plan Rows=93984但Actual Rows差异巨大的节点。
实测下来,优化生效的完整证据链是这样的:优化前游标计划走idx_tbl_1,Actual Rows在 Filter 后骤降,Buffers 显示大量堆块读取;优化后走idx_tbl_2,Index Cond直接命中,Buffers 的 shared hit 占比提升。把这两份 JSON 并排贴出来,就是最有说服力的验证结果。别忘了COMMIT或ROLLBACK结束事务,游标不关会占着资源。
5. 本篇常见错排查:401、连接失败与计划异常
排查环节按真实报错来。第一类,调用 TaoToken 接口时返回401 Unauthorized。原因通常是 Key 没带上、带错、或者环境变量没生效。检查Authorization: Bearer $TAOTOKEN_API_KEY里的变量是否真的导出,echo $TAOTOKEN_API_KEY确认非空。注意 Base URL 必须是https://taotoken.net/api,多一个斜杠或少一段路径都会导致鉴权失败。
第二类,local proxy failed或连接超时。这类报错多半是本地网络配置或代理设置干扰了请求,检查你的 HTTP_PROXY / HTTPS_PROXY 环境变量是否指向了不可用的地址,临时unset掉再试。同时确认目标地址拼写正确,不要手动加端口或路径后缀。
第三类,返回体解析异常,报reading choices之类的字段缺失。这通常是请求体格式不对,比如messages数组为空、model字段没填、或者 Content-Type 没设成application/json。对照接入文档 https://taotoken.net/doc 里的请求示例逐字段核对。如果你用的是 Claude Code 通道,报OAuth相关错误,说明鉴权方式用错了,Claude Code 走的是 Anthropic 兼容协议,参考 https://taotoken.net/claude-code 的配置说明,别混用 OpenAI 的 Bearer 头。
第四类,执行计划本身异常。如果EXPLAIN DECLARE ... CURSOR报语法错误,检查是不是漏了BEGIN,游标必须在事务块内声明。如果改了cursor_tuple_fraction但计划没变,确认SET是在同一个会话里执行的,跨会话不生效。如果建了idx_tbl_3但优化器还是不走,跑一次ANALYZE tbl更新统计信息,新索引刚建完统计可能没跟上。
第五类,三件套缺失导致的隐性失败。无论你用 CC Switch、Cline MCP 还是 Codex 的 auth.json,只要涉及模型调用,就必须同时配齐Base URL + API Key + Model ID。少任何一个,表现可能是静默失败或返回空结果,而不是明确报错,这种最难查。建议在脚本启动时先做一次健康检查,确认三件套都能正常返回再跑正式任务。
6. 把验证流程固化下来
优化做完不是终点,把验证动作固化成可重复的流程才有长期价值。我的做法是写一个 shell 脚本,参数化表名和查询条件,自动采集优化前后的 JSON 执行计划,调用 TaoToken 通道做差异解读,最后输出一份对比报告。这样每次改索引或调参数,跑一遍脚本就知道有没有回退。
具体落地时,把连接信息、Key、模型 ID 都放进环境变量或配置文件,脚本里只引用变量名。执行计划采集用psql -c "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ...",输出重定向到文件,再用jq提取关键字段做 diff。模型解读那一步,把两份 JSON 拼进 prompt,让它只输出“访问路径是否变化、估算偏差是否收敛、建议保留哪个索引”三行结论,避免长篇大论。
长期做数据库优化的,建议把 Coding Plan 用起来,地址 https://taotoken.net/coding-plan ,它适合这种高频、脚本化、带 Agent 自动分析的场景。日常临时验证模型输出,用模型对话页面 https://taotoken.net/chat 就够了。Key 管理和文档分别在 https://taotoken.net/api-keys 和 https://taotoken.net/doc ,收藏好,下次排查直接翻。
最后留一个实用技巧:cursor_tuple_fraction不要全局改,用SET LOCAL在事务内临时调整,事务结束自动恢复,避免影响其他会话。索引也不是越多越好,idx_tbl_3这类为特定排序建的索引会增加写入开销,确认查询高频且稳定后再建。执行计划对比时,优先看Actual Rows和Plan Rows的比值,偏差超过一个数量级就说明统计信息或分布假设有问题,这才是根因所在。