一次关于子查询的优化:用 TaoToken 统一 Key 打通 SQL 调优工作流
1. 一条分页 SQL 跑了 113 秒问题出在哪子查询是 SQL 优化里最容易被低估的一类性能瓶颈。它写起来顺手逻辑清晰但在分页场景下经常被数据库反复执行Starts 一栏的数字能吓人一跳。这篇面向后端和 DBA 的日常调优场景讲一个真实案例一条带 8 个子查询的分页 SQL 执行接近两分钟通过执行计划定位到子查询被重复调用 4368 次把子查询提到分页外层后降到 0.75 秒。同时我会把整个排查过程沉淀成一套可复用的工作流并用 TaoToken 的统一 Key 打通 AI 工具调用通道让「看执行计划 → 让模型给改写方案 → 回库验证」这条链路不用在多个平台之间来回切。适合正在处理慢 SQL、又想把调优经验固化成流程的同学。核心检索词先摆出来子查询优化、SQL 调优、执行计划分析、dbms_xplan、10046 trace、TaoToken 统一 Key。这几个词贯穿全文你按顺序跟下来就能复现。2. 原问题与场景分页 SQL 里子查询被放大了 4368 倍原始 SQL 的结构是典型的三层嵌套分页SELECT * FROM (SELECT unpaged_.*, rownum rn_ FROM (SELECT t2.*, (SELECT cs.system_name FROM cfms_sys cs WHERE cs.sys_id t2.system_id) AS system_name, (SELECT m.name FROM cfms_module m WHERE m.module_id t2.module_id) AS module_name, (SELECT count(1) FROM cfms_replys cr WHERE cr.question_id t2.id) reply_count, (SELECT v.version_no FROM cfms_versions v WHERE v.version_id t2.ps_online_version) AS ps_online_version_no, (SELECT to_char(wmsys.wm_concat(t.tag_id || ; || t.name)) FROM cfms_tag t, cfms_tag_question tq WHERE t2.id tq.question_id AND t.tag_id tq.tag_id) AS tags, (SELECT u.name FROM v_user u WHERE u.user_id t2.service_id) AS service_name, (SELECT max(m.modify_at) FROM cfms_question_modify m WHERE m.question_id t2.id) AS modify_at, decode((SELECT count(1) FROM cfms_questions cq, cfms_question_workflow cqw, bpms_ru_todo_task brtt WHERE cq.id cqw.question_id AND cqw.process_ins_id brtt.cur_process_ins_id AND cq.id t2.id AND brtt.trans_actor_id N00251.sz), 0, 0, 1) AS can_handle FROM cfms_questions t2 WHERE t2.state -1 AND EXISTS (SELECT 1 FROM cfms_questions cq, cfms_question_workflow cqw, bpms_ru_todo_task brtt WHERE cq.id cqw.question_id AND cqw.process_ins_id brtt.cur_process_ins_id AND cq.id t2.id AND brtt.trans_actor_id N00251.sz) ORDER BY t2.discover_time DESC, t2.id) unpaged_ WHERE rownum 30) WHERE rn_ 20;执行时间 113 秒。打开statistics_levelall后看dbms_xplan.display_cursor的 allstats 输出Starts 列暴露了一切CFMS_REPLYS全表扫描 4368 次、CFMS_QUESTION_MODIFY全表扫描 4368 次、BPMS_RU_TODO_TASK全表扫描 4368 次且 A-Rows 达到 1917 万行。也就是说分页只取 10 行但每个子查询都对着 4368 行基表各跑了一遍。注意分页 SQL 里 SELECT 列表中的标量子查询执行次数等于内层结果集行数而不是最终返回行数。这是最容易被忽略的放大效应。3. TaoToken 前置统一 Key 打通调优工作流调优过程中我需要在 AI 工具里反复问「这个执行计划说明什么」「子查询怎么改写」如果每个工具都单独配 Key、单独记额度切换成本很高。TaoToken 提供统一 Key 和统一 API 通道一个 Key 就能覆盖模型对话、编码 Agent、接入文档查询等场景省掉多平台配置的麻烦。官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基址不加 UTMhttps://taotoken.net/api按用途分流别只记首页用途入口验证模型/问执行计划模型对话 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite长期编码/Agent 调优Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite管理 Key/额度Console https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite生成/查看 API KeyAPI Keys https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档Doc https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteClaude Code 接入ClaudeCodeAnthropic https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite拿到 Key 后先确认通道可用再进配置。这一步别跳后面所有验证都依赖它。4. 可复制配置settings.json 与 config.toml 片段不同 AI 工具读取配置的方式不一样。下面给两份骨架按你用的工具选一份把YOUR_TAOTOKEN_KEY换成 API Keys 页面生成的真实 Key。4.1 settings.json适用于读取 JSON 配置的编辑器类工具{ ai.provider: taotoken, ai.baseUrl: https://taotoken.net/api, ai.apiKey: YOUR_TAOTOKEN_KEY, ai.model: claude-sonnet-4-5, ai.timeoutMs: 60000, ai.maxTokens: 4096, ai.temperature: 0.2 }temperature给 0.2 是有意的调优场景要的是稳定、可复现的改写建议不是发散创意。timeoutMs给 60 秒因为贴执行计划时上下文较长。4.2 config.toml适用于 TOML 配置的 CLI / Agent 工具[provider] name taotoken base_url https://taotoken.net/api api_key YOUR_TAOTOKEN_KEY model claude-sonnet-4-5 [request] timeout_ms 60000 max_tokens 4096 temperature 0.2 [retry] max_attempts 3 backoff_ms 800retry段建议保留。调优时经常连续发多条长上下文请求偶发超时靠重试兜住不用手动重发。4.3 环境变量方式不想写文件时export TAOTOKEN_API_KEYYOUR_TAOTOKEN_KEY export TAOTOKEN_BASE_URLhttps://taotoken.net/api三种方式选一种即可不要同时配否则优先级容易乱。配完先做下一步验证。5. 验证请求与成功结果从执行计划到改写方案5.1 先验证通道curl -s https://taotoken.net/api/v1/models \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ | head -c 400返回模型列表 JSON 就说明 Key 和通道都正常。如果返回 401去 API Keys 页面确认 Key 是否复制完整返回超时检查网络出口。5.2 把执行计划喂给模型通道通了之后把dbms_xplan.display_cursor的 allstats 输出贴进去问一句这是 Oracle 分页 SQL 的 allstats 执行计划Starts 列显示多个子查询被调用 4368 次 BPMS_RU_TODO_TASK 全表扫描 A-Rows 1917 万。请给出子查询改写方案并说明改写后 Starts 预期降到多少。模型会指出核心问题标量子查询在内层结果集上逐行执行。改写方向是把这些子查询从内层 SELECT 列表移到分页外层让它们只对最终返回的 10 行执行。5.3 改写后的 SQL 与实测结果SELECT (SELECT cs.system_name FROM cfms_sys cs WHERE cs.sys_id unpaged.system_id) AS system_name, (SELECT m.name FROM cfms_module m WHERE m.module_id unpaged.module_id) AS module_name, (SELECT count(1) FROM cfms_replys cr WHERE cr.question_id unpaged.id) reply_count, (SELECT v.version_no FROM cfms_versions v WHERE v.version_id unpaged.ps_online_version) AS ps_online_version_no, (SELECT to_char(wmsys.wm_concat(t.tag_id || ; || t.name)) FROM cfms_tag t, cfms_tag_question tq WHERE unpaged.id tq.question_id AND t.tag_id tq.tag_id) AS tags, (SELECT u.name FROM v_user u WHERE u.user_id unpaged.service_id) AS service_name, (SELECT max(m.modify_at) FROM cfms_question_modify m WHERE m.question_id unpaged.id) AS modify_at, decode((SELECT count(1) FROM cfms_questions cq, cfms_question_workflow cqw, bpms_ru_todo_task brtt WHERE cq.id cqw.question_id AND cqw.process_ins_id brtt.cur_process_ins_id AND cq.id unpaged.id AND brtt.trans_actor_id N00251.sz), 0, 0, 1) AS can_handle FROM (SELECT unpaged_.*, rownum rn_ FROM (SELECT t2.* FROM cfms_questions t2 WHERE t2.state -1 AND EXISTS (SELECT 1 FROM cfms_questions cq, cfms_question_workflow cqw, bpms_ru_todo_task brtt WHERE cq.id cqw.question_id AND cqw.process_ins_id brtt.cur_process_ins_id AND cq.id t2.id AND brtt.trans_actor_id N00251.sz) ORDER BY t2.discover_time DESC, t2.id) unpaged_ WHERE rownum 30) unpaged WHERE rn_ 20;关键变化子查询从内层unpaged_的 SELECT 列表移到了最外层作用对象从 4368 行变成 10 行。实测执行时间从 113 秒降到 0.75 秒CFMS_REPLYS的 Starts 从 4368 降到 10BPMS_RU_TODO_TASK的 A-Rows 从 1917 万降到 43880。5.4 用 10046 trace 交叉验证执行计划有时会骗人10046 trace 更直接ALTER SESSION SET tracefile_identifier subq_opt; ALTER SESSION SET events 10046 trace name context forever, level 12; -- 执行改写后的 SQL ALTER SESSION SET events 10046 trace name context off;在 trace 文件里搜BPMS_RU_TODO_TASK改写前该行cr1656230、time32077237 us改写后cr3800、time120000 us量级。两个工具结论一致改写有效。6. 本篇常见错排查错误一只加索引不改写结构。我试过先给CFMS_REPLYS.question_id、CFMS_QUESTION_MODIFY.question_id加索引CFMS_REPLYS从全表扫描变成INDEX RANGE SCAN但 Starts 还是 4368总时间只从 113 秒降到 90 秒左右。索引解决的是单次访问成本解决不了执行次数。子查询被调用 4368 次这个根因不动加再多索引也是治标。错误二把子查询改成 JOIN 但没控制行数。有人第一反应是把标量子查询改成 LEFT JOIN。方向对但如果 JOIN 写在内层JOIN 结果集还是 4368 行聚合类子查询比如count(1)、max(modify_at)还会因为一对多关系产生行膨胀分页结果直接错乱。正确做法是先分页再关联或者用窗口函数在内层一次性算完。错误三忽略rownum与ORDER BY的执行顺序。Oracle 里rownum 30是在排序前还是排序后生效取决于嵌套层级。原 SQL 把ORDER BY放在最内层、rownum放在中间层这个结构本身是对的。改写时如果把ORDER BY挪到外层分页结果会变。改结构前先用小数据集验证结果集一致性。错误四TaoToken 配置里 baseUrl 带了多余路径。有人写成https://taotoken.net/api/v1/chat/completions工具自己还会拼/v1/...结果 404。baseUrl 只写到https://taotoken.net/api具体路径交给工具或 SDK 拼。错误五验证时只看总耗时不看 Starts。总耗时受缓存、并发影响波动大。Starts 是确定性的改写前后对比 Starts 才能确认子查询执行次数真的降下来了。养成看 allstats 里 Starts 列的习惯。排障和接入相关的问题去 API Keys 页面确认 Key 状态再对照接入文档检查配置格式。验证模型对执行计划的理解是否准确用模型对话快速问一轮。如果要把这套「贴计划 → 问改写 → 回库验证」固化成长期编码流程Coding Plan 更适合承载多轮 Agent 调用。7. 把调优思路沉淀成可复用流程这套流程跑通之后我把它固化成了四步第一步statistics_levelall加dbms_xplan.display_cursor(null,null,allstats last)先看 Starts 列找异常放大的算子第二步对可疑子查询跑 10046 trace level 12用cr和time交叉确认第三步把执行计划贴给模型让它给改写方案和预期 Starts第四步改写后回库实测对比 Starts 和总耗时。四步里第三步最容易省但恰恰是它把「凭经验猜」变成了「有依据改」。子查询优化的本质不是背规则是理解执行次数怎么被放大的。分页场景下SELECT 列表里的标量子查询执行次数等于内层行数这个认知一旦建立类似的慢 SQL 你一眼就能看出问题在哪。最后留一个实用技巧改写前后都保存一份 allstats 输出用文本 diff 对比 Starts 列。比只看总耗时可靠得多也方便复盘时回看当时到底改了什么。