跳转至

控制台输出优化:日志降噪与执行可追踪性

SDD: 审查轮与优化轮解耦

背景

当前 V3 工作流已经引入 review_mode 和纯审查 prompt:正优化候选出现后,下一轮可以进入 checklist 审查,并在审查轮跳过 SQL 执行。但审查轮和优化轮仍共用部分状态、反馈函数和落盘语义。

问题清单

编号 问题 位置 类型
P1 审查轮被 max_rounds 挡住,最后一轮出现正优化时无法进入审查 core/llm_opt_v3/v3_nodes.py::node_stop Bug
P2 报告 compute_statustime_saved 先 round 再判断,和运行时反馈精度不一致 core/ana/export_dba_display_csv.py::compute_status Bug
P3 审查结论依赖 review_round_count,下一轮优化时结论会被清空,无法稳定传入 followup prompt core/llm_opt_v3/feedback.py / prompting.py Bug
P4 build_feedback 同时负责执行反馈和审查结论解析 core/llm_opt_v3/feedback.py 架构
P5 图拓扑固定为 Persist -> ExecuteSQL -> Feedback,审查轮靠 node_execute early return 绕过 SQL 执行 core/llm_opt_v3/v3_langgraph_orchestrator.py 架构
P6 审查轮和优化轮共用 rN.json 轮次落盘,Report 读取语义混乱 node_persist / export_dba_display_csv.py 架构

分阶段方案

Step 1: 先修 Bug,不改图架构

目标:保持现有 LangGraph 拓扑不变,只修复 P1/P2/P3。

  • P1:当 feedback.needs_review_gate == True 且尚无审查结论时,下一轮必须进入审查轮,不受 max_rounds 限制。审查轮不计入优化预算。
  • P2:compute_statustime_saved 使用原始浮点差值,不再先 round(..., 3) 后判断;展示列可以继续保留格式化四舍五入。
  • P3:审查结论从 review_round_count 中解耦,提升为 state 顶层字段:
  • last_review_conclusion
  • last_review_summary 下一轮优化 prompt 从这两个字段读取上一轮审查反馈。

Step 1 验收: - 最后一轮正优化仍会进入一次审查。 - 审查不通过后,下一轮优化 prompt 能看到上一轮审查结论和审查摘要。 - 报告状态判定与运行时 build_execution_feedbacktime_saved 的精度口径一致。

Step 2: 函数级拆分

目标:消除 P4。

  • build_execution_feedback(...):只负责 SQL 执行结果、语义一致性、性能阈值、是否触发 review gate。
  • build_review_feedback(...):只负责从审查输出中提取审查结论和审查摘要。
  • 图中分别使用 node_execution_feedback / node_review_feedback,不再保留统一 node_feedback 兼容入口。

Step 3: 图级路由与落盘隔离

目标:消除 P5/P6。

目标拓扑:

Persist
├─ review_mode=True  -> ReviewFeedback    -> StopPolicy
└─ review_mode=False -> ExecuteSQL -> ExecutionFeedback -> StopPolicy

落盘规则: - 优化轮:继续写 rN.json。 - 审查轮:写 rN_review.json。 - Report 默认跳过 _review.json,只统计优化轮执行结果;审查结果只用于审查备注/人工确认提示。

对话状态规则: - 审查轮使用 reviewer system,只做 checklist 复核。 - 审查不通过后,下一轮优化重新切回 optimizer system,并带上被审查候选、审查结论和摘要。 - 审查反馈只传递给下一轮优化;下一次执行反馈完成后清空,避免后续轮次持续携带旧审查结论。

当前执行状态

Step 1/2/3 已完成:Bug 修复、反馈函数拆分、图级路由、审查轮落盘隔离均已落地。

背景

DBA 使用 sqlopt-csv 批量优化时,控制台被大量冗余日志淹没,关键信息(优化轮次、状态、时间对比)被淹没在连接/查询细节中。同时,执行验证结果(feedback、时间对比、状态判定)没有持久化到可追踪的日志文件。分步命令(init/collect/run/report)各步骤完成后缺少关键结果反馈,DBA 需要手动打开文件才能确认结果。

现状问题清单

P0: 控制台日志冗余

问题 1:连接/查询细节刷屏 - client.py__init__connect()close()execute_query() 均为 logger.info - 一个 3 轮 case 产生 30+ 行 INFO 日志,其中 20+ 行是连接/查询细节 - 涉及文件:core/tdsql_client/client.py(第 66-71、86-92、103、217-226 行)

问题 7:"TDSQL 客户端初始化完成" 完全冗余且误导 - TDSQLClient.__init__ 只是读了 config.ini 配置字典,没有建立任何连接、没有分配任何外部资源 - 但日志写 "客户端初始化完成" 让 DBA 误以为建连成功 - 同一个 case 内被调用 5+ 次(node_load_inputs 2 次 + node_execute 每轮 1 次 + cleanup_case_indexes 1 次) - 涉及文件:core/tdsql_client/client.py 第 66-71 行

处理方案: - "客户端初始化完成"logger.debug - "成功连接到 TDSQL 数据库"logger.debug - "TDSQL 数据库连接已关闭"logger.debug - "查询完成"all=[5次明细]fetch_timefetch_modedriverlogger.debug - "查询完成" 控制台只保留精简版:execute=0.002s 5次=[0.005,0.003,0.002,0.002,0.002] rows=18 - "执行 EXPLAIN 分析"logger.debug - sql_exe.py"执行 SQL""执行 EXPLAIN""✓ SQL 执行完成"logger.debug - run_single_workflow.py 的配置四行合并为一行摘要

P0: 索引 DDL 操作控制台可见

索引创建/回滚是唯一修改生产库结构的操作,DBA 必须实时可见。

时序约束: - 索引创建在 node_execute(轮内,验证 SQL 之前) - 索引回滚在 node_stop → finish() → cleanup_case_indexes(所有轮次结束后) - 回滚不出现在轮次中间

处理方案: - sql_exe.py:apply_create_index_ddlslogger.infoprint,失败时 print + logger.warning 双写 - sql_exe.py:drop_indexes_by_snapshot_diff:已有 print,保留 - 统一前缀:[索引] 创建: ... / [索引] 删除: ... / [索引] 跳过删除(差集为空)

P0: 关键信息缺失

问题 4:看不到优化轮次 - LangGraph 节点函数没有 print 轮次进度,DBA 不知道当前第几轮 - 涉及文件:core/llm_opt_v3/v3_nodes.py

处理方案: - node_call_llmprint(f" [第{r}轮] 调用 LLM ({model})...") - node_execution_feedback / node_review_feedbackprint(f" [第{r}轮] {status} | 基线={baseline:.3f}s 优化={opt:.3f}s 提升={improvement:.1%}")

P0: stop_reason 中文化

  • "reach_max_rounds""达到最大轮次"
  • "effective_positive_optimization""正优化"
  • "manual_confirmation_required""需人工确认"
  • 涉及文件:core/llm_opt_v3/v3_nodes.py 第 1486-1493 行

P1: 分步命令结果反馈

sqlopt-init:缺少基线摘要 - 当前只打印文件路径,DBA 看不到执行时间和行数 - 目标:基准: 0.042s | 18行 | EXPLAIN 已生成

sqlopt-collect:统计维度对 DBA 无意义 - 当前按 extractor 粒度统计(6 个 extractor × N 张表),生产库 innodb 无权限时 fail=2N - DBA 关心的是表级别:核心 4 项(ddl, indexes, partitions, table_stats)齐全 = 表级成功 - innodb 不可用不计入表级失败,单独提示 - 目标:采集完成: 成功=20张, 失败=0张 (innodb 不可用=20张)

sqlopt-run:结果摘要缺轮次概览 - 当前只打印最后一轮状态,没有每轮明细、最优轮次、索引摘要 - 最终收口只分两类✓ 正优化✗ 优化失败,失败必须说明具体原因 - 目标格式:

优化结果: nw_0005 | ✗ 优化失败: 无变化
────────────────────────────────────────────
  基线: 0.042s
  第1轮: 0.041s  无变化
  第2轮: 0.042s  无变化
  第3轮: 0.041s  无变化
  建议: 3轮均无明显提升,SQL 可能已最优或需人工改写
  索引: 创建0个 / 回滚0个
────────────────────────────────────────────

优化结果: nw_0001 | ✓ 正优化
────────────────────────────────────────────
  基线: 2.341s → 0.892s | 提升=61.9% | 3轮
  索引: 创建2个 / 回滚2个
────────────────────────────────────────────
- 失败原因 DBA 文案映射: | 内部状态 | DBA 看到 | 建议文字 | |---------|---------|---------| | 无变化 | 性能无明显变化 | 3轮均无明显提升,SQL 可能已最优或需人工改写 | | 恶化 | 性能变差 | LLM 改写方向错误,建议人工介入 | | 结果不一致 | 语义不一致 | LLM 改变了查询语义,需人工审查 diff | | SQL完全相同 | LLM未做有效改写 | LLM 认为无需优化或无法优化 | | 优化后SQL报错 | SQL执行报错 | LLM 生成了无效 SQL:{错误详情} | | LLM未给出优化SQL | 未输出优化SQL | 输出被截断或格式错误 | | LLM解析失败 | 模型输出无法解析 | 输出格式异常,需检查 LLM 返回

sqlopt-report:只打路径,无摘要 - 目标格式:

报告已生成: display_test_01.csv
────────────────────────────────
  正优化: 3    无变化: 5    恶化: 1    报错: 1
  总提升: 12.3s   平均提升: 34.2%
────────────────────────────────

跨步骤前置检查 - run_single_workflow.py 自动采集 table_ctx 时打印 [自动] 未发现 table_ctx,正在自动采集... - sqlopt-report 找不到数据时给操作建议

P1: 执行追踪日志持久化(开发视角)

受众:开发人员,排查"程序是否正常跑通"。

设计原则:日志只记录程序执行层面的关键事件和异常,不记录优化结果对比(开发去 data/ 下看落盘文件)。一行 = 一个事件。

目标日志格式(事件流):

2026-05-20 09:27:35 | [nw_0005 第1轮] LLM调用开始 | model=deepseek-v4-pro
2026-05-20 09:27:37 | [nw_0005 第1轮] LLM调用成功 | latency=120.3s | tokens=4521
2026-05-20 09:27:37 | [nw_0005 第1轮] LLM解析成功
2026-05-20 09:27:37 | [nw_0005 第1轮] SQL执行完成 | execute=0.003s | rows=18
2026-05-20 09:27:37 | [nw_0005 第1轮] 反馈计算完成: SQL完全相同
2026-05-20 09:27:37 | [nw_0005 第1轮] ✗ 索引创建失败 | DDL="ALTER TABLE..." | error="Duplicate key name 'idx_xxx'"
2026-05-20 09:29:24 | [nw_0005 第2轮] LLM调用成功 | latency=118.7s | tokens=3890
2026-05-20 09:31:01 | [nw_0005 第3轮] 优化结束 | stop_reason=达到最大轮次
2026-05-20 09:31:02 | [nw_0005] ✗ 异常: 索引回滚失败 | error="Connection refused" | traceback=...

记录的事件类型: | 事件 | 说明 | 何时写 | |------|------|--------| | LLM调用开始 | 模型名、轮次 | node_call_llm | | LLM调用成功 | 耗时、token 数 | node_call_llm | | LLM调用失败 | 状态码、错误信息、重试次数 | node_call_llm | | LLM解析成功/失败 | 解析结果 | node_parse_llm | | SQL执行完成 | execute 耗时、返回行数 | node_execute | | SQL执行失败 | 错误信息 | node_execute | | 索引创建成功/失败 | DDL 文本、错误信息 | apply_create_index_ddls | | 索引回滚成功/失败 | 删除数、错误信息 | cleanup_case_indexes | | 反馈计算完成 | 状态 | node_execution_feedback / node_review_feedback | | 优化结束 | stop_reason | node_stop | | 异常 | 任何未捕获异常,含 traceback | 全局 |

不记录的内容(开发去 data/ 查看): - 优化前后时间对比、提升百分比 - 结果是否一致、SQL 是否相同 - status_reason 详细文案 - 索引命中情况

P2: 连接模式(记录,不作为本次修复项)

每次 execute_query 都 connect→execute→close,一个 3 轮 case 建连 10+ 次。根因是 LangGraph 节点间无法共享 client 实例。连接池是未来优化方向,本次仅降噪处理。

修改文件清单

文件 修改内容
core/tdsql_client/client.py 日志降级:__init__/connect/close/EXPLAIN → debug;查询结果精简 INFO + 完整 DEBUG
core/tdsql_client/sql_exe.py 日志降级:执行 SQL/EXPLAIN/完成 → debug;索引操作改 print
core/llm_opt_v3/run_single_workflow.py 配置信息合并为一行;自动采集提示
core/llm_opt_v3/v3_nodes.py 轮次进度 print;stop_reason 中文化;feedback 持久化到 exec_trace 日志
core/llm_opt_v3/utils.py LLM 调用日志保留 INFO
core/dataprocess_v3/table_ctx/service.py CollectSummary 新增表级别统计字段
bin/sqlopt-init init 完成后打印基线摘要
bin/sqlopt-run 结果摘要加轮次概览和索引摘要
bin/sqlopt-collect 打印改为表级别统计
bin/sqlopt-report 报告生成后打印状态分布摘要

控制台输出目标效果

sqlopt-csv(批量)

============================================================
处理数据库: tdsql_northwind_local (northwind) | 共 1 条 SQL
============================================================

[Step 1 - Init] 获取基线数据...
  [1] Init 完成: northwind_0001 | 基准: 0.042s | 18行

[Step 3 - Run] 执行优化...

[1/1] 优化: northwind_0001 | tdsql_northwind_local | 阈值0.1s/20% | 最多3轮
  [第1轮] 调用 LLM (deepseek-v4-pro)...
  [索引] 创建: ALTER TABLE t_xxx ADD INDEX idx_xxx (col1)
  [第1轮] 无变化 | 基线=0.042s 优化=0.041s 提升=2.4%
  [第2轮] 调用 LLM (deepseek-v4-pro)...
  [第2轮] 无变化 | 基线=0.042s 优化=0.042s 提升=0.0%
  [第3轮] 调用 LLM (deepseek-v4-pro)...
  [第3轮] 无变化 | 基线=0.042s 优化=0.041s 提升=2.4%
  [索引] 回滚: 删除新增索引 1 个 ✓ t_xxx.idx_xxx
[1/1] ✗ 优化失败: northwind_0001 | 无变化 | 3轮均无明显提升,建议人工改写

[Step 4 - Report] 生成统一报告...
  报告已生成: display_test_01.csv
  ────────────────────────────────
    正优化: 0    优化失败: 1    (无变化:1)
  ────────────────────────────────

============================================================
执行汇总:
  总数: 1  ✓ 正优化: 0  ✗ 优化失败: 1  (无变化:1)
============================================================

sqlopt-init(分步)

已创建: /Users/.../nw_0005.sql
基准: 0.042s | 18行 | EXPLAIN 已生成

sqlopt-collect(分步)

发现 20 张表,开始采集...
采集完成: 成功=20张, 失败=0张 (innodb 不可用=20张)
输出目录: /Users/.../table_ctx

sqlopt-run(分步,单条)

优化: nw_0005 | tdsql_northwind_local (northwind) | 阈值0.1s/20% | 最多3轮
  [第1轮] 调用 LLM (deepseek-v4-pro)...
  [第1轮] ✗ 无变化 | 基线=0.042s 优化=0.041s 提升=2.4%
  [第2轮] 调用 LLM (deepseek-v4-pro)...
  [第2轮] ✗ 无变化 | 基线=0.042s 优化=0.042s 提升=0.0%
  [第3轮] 调用 LLM (deepseek-v4-pro)...
  [第3轮] ✗ 无变化 | 基线=0.042s 优化=0.041s 提升=2.4%
  [索引] 回滚: 跳过(差集为空)

优化结果: nw_0005 | ✗ 优化失败: 无变化
────────────────────────────────────────────
  基线: 0.042s
  第1轮: 0.041s  无变化
  第2轮: 0.042s  无变化
  第3轮: 0.041s  无变化
  建议: 3轮均无明显提升,SQL 可能已最优或需人工改写
  索引: 创建0个 / 回滚0个
────────────────────────────────────────────

sqlopt-report(分步)

报告已生成: display_test_01.csv
────────────────────────────────
  正优化: 3    无变化: 5    恶化: 1    报错: 1
  总提升: 12.3s   平均提升: 34.2%
────────────────────────────────

P1: sqlopt-report 审查备注(review gate CSV 展示)

Review Gate 完整流程

触发条件:某轮出现 effective_positive_optimization = Trueenable_positive_review_gate = Truereview_round_count == 0(只有正优化才进入,且只触发一次)。

流程: 1. 触发轮(第N轮):node_stop 不停止,继续下一轮,第N轮 JSON 写 stop_reason = "" 2. 审查轮(第N+1轮):LLM 用 checklist 复核,risk_assessment 第一行审查结论:正优化/部分合理/无明显优化/功能风险高,需人工确认 3. 审查轮 node_review_feedback 读取 _extract_model_review_conclusion: - 正优化review_allows_positive = Truenode_stop 调用 finish("effective_positive_optimization") - 功能风险高,需人工确认finish("manual_confirmation_required") - 其余 → 带着审查结论和摘要回到下一轮优化 4. stop_reason 只写入最后一轮的 JSON,其余轮 stop_reason = ""

当前问题

compute_status 只看执行结果,不看 stop_reason。审查轮的 SQL 执行结果可能仍是 正优化,导致 CSV 里显示 r{n}结果 = 正优化,而实际 stop_reason = manual_confirmation_required(旧版英文原始值)或 需人工确认(新版中文)。DBA 看到 正优化 直接采纳,实际该 case 需要人工确认。

CSV 新增列设计

只新增一列"审查备注"(行级,紧跟 sql编号 之后),不改动每轮列结构。

数据来源:遍历该 case 所有轮次 JSON,找到 stop_reason 非空的那一轮(即最终轮)。

触发条件
⚠️ 需人工确认 - {审查结论原文} 最终轮 stop_reason == "需人工确认""manual_confirmation_required"
(空) 无审查,或审查结论为正优化
  • {审查结论原文}:从最终轮 risk_assessment_extract_model_review_conclusion 提取,填入人类可读的结论标签(如"功能风险高,需人工确认")
  • 不做多级标记(去掉"高风险改写"/"部分合理"中间层),只关注最终需不需要人工介入

汇总统计新增

sqlopt-report 终端摘要新增一行(仅在有 case 需人工确认时显示):

⚠️ 需人工确认: 2

修改文件

文件 修改内容
core/ana/export_dba_display_csv.py build_headers 新增"审查备注"列(header1/header2 各加一格);每轮末列 r{n}优化风险(来自 _review.json);build_row 读最终轮 stop_reason 填充行级"审查备注";不再输出"输出说明"表头行
bin/sqlopt-reportexport_dba_display_csv.py 终端摘要新增"⚠️ 需人工确认: N"

目标 CSV 结构(三行表头)

行0: 输出参考 | (空列补齐)
行1: (空) | sql编号 | 审查备注 | 基准(合并4列) | 第1轮优化(合并9列) | 第2轮优化 | ...
行2: 输出项 | sqlopt_review_display | 审查备注 | 原sql | 原执行计划 | 原sql时间 | 瓶颈分析 | r1优化后sql | ... | r1优化率 | r1优化风险 | r2...
  • 不再输出"输出说明"行;列含义由行2 的 r{n}* 列名表达。
  • 每轮 9 列:优化后sql / 新增索引 / 优化后执行计划 / 思考步骤 / 优化策略 / 结果 / 时间 / 优化率 / 优化风险(header2 中带 r{n} 前缀)。
  • 行首多一列行级"审查备注"(仅 stop_reason 为需人工确认时填充)。

P0: 批次优化汇总报告(LLM 综合摘要)

背景

display_{batch}.csv 按轮次平铺展示每个 case 的优化过程,适合逐字段标注。但缺少一份综合性的可读文本:将 N 轮优化数据交给 LLM 做一次总结,输出统一的最终推荐 SQL、整体策略、索引建议和风险评估。

产物

summary_{batch}.txt,与 display_{batch}.csv 同目录。

输出格式

每个 case 一段,段间用分隔线隔开:

================================================================================
Case: nw_0001
================================================================================

【最终推荐SQL】
SELECT ... ;

【综合思考过程】
(模型综合各轮思考步骤后的统一叙述)

【需要应用的索引】
CREATE INDEX idx_xxx ON table_a(col1);
CREATE INDEX idx_yyy ON table_b(col2);

【整体优化策略】
(模型综合各轮策略后的统一描述)

【优化风险评估】
(模型综合各轮风险 + 审查风险的统一评估)

【性能对比】
  基线: 2.341s → 最优轮: 0.892s (第1轮) | 提升=61.9%
  最终状态: 正优化

================================================================================
Case: nw_0002
================================================================================
...

数据来源

复用 export_dba_display_csv.py 已有的数据读取基础设施: - discover_families() / choose_family() 定位版本族 - read_json() / read_text() 读取各轮 JSON 和文件 - 不新增任何数据读取逻辑

每个 case 收集的结构化数据: - 原始 SQL、基线执行时间(raw_data/{folder}/) - 各轮 opt_res_v3 JSON 中的 thinking_stepsoptimization_strategycreate_index_ddlrisk_assessment - 各轮 mannul_data 中的执行时间、优化后 SQL - 各轮 compute_status 结果 - 审查轮的 risk_assessment(来自 _review.json

LLM 调用

  • 函数:复用 core.llm_opt_v3.utils.call_llm_api_with_messages()
  • 调用时机:export_batch_report() 完成后,在同一个 Step 4 中调用
  • 调用频率:每个 case 一次(非每轮)
  • prompt 模板:core/llm_opt_v3/prompts/summary_v1.txt
  • 模型:使用同一 model_section="llm"

接入位置

文件 改动
core/ana/export_dba_display_csv.py 新增 _collect_case_rounds()_build_summary_prompt()export_batch_summary() 三个函数
core/llm_opt_v3/prompts/summary_v1.txt 新增汇总 prompt 模板
core/csv_batch.py Step 4 末尾加一行 export_batch_summary() 调用

不改动的模块

  • v3_nodes.pyv3_state.pyv3_langgraph_orchestrator.pyrun_single_workflow.py — 状态机和节点逻辑不变
  • cli_output.py — 控制台输出不变
  • build_row() / export_batch_report() / build_headers() — CSV 生成逻辑不变

SDD: SQLite 持久化落地(两阶段方案)

创建日期:2026-06-24 关联文档:docs/sqlite_storage_proposal.md(表结构详设计)、core/sqlite_storage/schema.sql(DDL)

背景

当前所有生产数据以文件目录承载(raw_data/opt_res_v3/mannul_data/table_ctx/),存在三类痛点: 1. 跨 case 聚合查询慢ana_multi_version.py 反复扫文件系统、解析 JSON 2. 指纹/分类无法复用:每次都需要重算 dataprocess_v3.cluster_sql / classify_sql 3. 规则资产无沉淀载体:review checklist 只在 prompt 文件里

SQLite 库(core/sqlite_storage/sql_opt_results.db)承担"结构化数据单一源",文件目录在过渡期作为双写备份,最终下线为归档。

阶段划分

阶段 目标 范围 风险
阶段一(P0) 建库 + 灌存量数据,作为黄金校验集 仅新增 core/sqlite_storage/ 模块与 ETL 脚本,生产代码 0 改动
阶段二(P1) 生产代码双写 + 读路径迁移到 DB 修改 persistence.py / execution.py / sql_exe.py / ana_*.py
阶段三(P2) 文件 I/O 下线 移除文件读写分支,data/ 转归档

架构决策

决策 选项 决议 理由
本次执行范围 仅阶段一 / 一+二 / 先 docs 仅阶段一 先验证数据正确性再改生产路径,降低风险
阶段二读迁移范围 报表优先 / +table_context / 全量 全量迁移(含 dataprocess_v3 文件 I/O) 一次性收尾,避免长期双轨
阶段二双写失败策略 warn 不阻断 / fail-fast / +错日志 warn 不阻断 文件仍是 source of truth,过渡期保 workflow
表设计 宽表 vs 关联表 sql_case 保持 37 列宽表(仅删 cluster_size SQLite 分析型场景宽表性能优于 JOIN
枚举字段 全 TEXT / 加 CHECK 16 个固定枚举字段加 CHECK,开放字段保持 TEXT 利用 SQLite 静态约束,避免脏数据

Repository API 设计

core/sqlite_storage/ 模块结构:

core/sqlite_storage/
├── schema.sql                     # 已存在(v2.0,6 表 + 1 视图)
├── connection.py                  # connect() 上下文 + PRAGMA(WAL/FK/encoding)
├── fingerprints.py                # 包装 dataprocess_v3 指纹/分类计算
└── repositories/
    ├── sql_case_repo.py           # upsert / get_by_name / get_by_hash / update_baseline / update_classification
    ├── opt_result_repo.py         # insert_round / list_by_case / latest_by_case
    ├── opt_exec_repo.py           # insert_exec / latest_by_case / list_by_case
    ├── table_ctx_repo.py          # upsert / get_by_table / list_by_db
    ├── audit_rule_repo.py         # seed / list_active / hit
    └── rewrite_rule_repo.py       # seed / list_active / record_feedback

API 契约: - 所有写方法支持 upsert 语义(PK 冲突时 UPDATE) - 所有方法接受 conn 参数(外部管理事务),不内置 commit - 失败抛 sqlite3.IntegrityError / sqlite3.OperationalError,由调用方决定 warn 还是 fail - 返回值:upsert 返回 bool(是否新增),list 返回 list[dict],get 返回 dict | None

ETL 流程(阶段一)

入口scripts/migrate_legacy_to_sqlite.py --data-root data/northwind --db core/sqlite_storage/sql_opt_results.db

按 FK 依赖顺序

输入路径 关键计算
1 table_ctx table_ctx/{db.table}/ 合并 ddl.sql / indexes.json / table_stats.json / partitions.json
2 sql_case raw_data/{folder}/ 三件套 cluster_sql.normalize_sql_regexsql_digest*;调 classify_sql.classify_sql_by_syntaxprimary_category*;调 result_compare 算 baseline 3 层 hash
3 opt_result opt_res_v3/{folder}/*.json 解析 meta.batch_suffix / meta.prompt_version / round_num / review_mode,JSON 字段(thinking_steps/strategy/index_trace 等)保留为字符串
4 opt_exec mannul_data/{folder}/ 三件套 计算 3 层 hash,与 sql_case.baseline_* 对比填 comparison_status
5 audit_rule prompts/review_checklist.txt + 手工 抽 review checklist 中明示的规则种子(约 10 条)
6 rewrite_rule 手工初版 几条样例(谓词下推 / CTE 去重 / EXISTS→JOIN)

校验scripts/verify_sqlite_import.py): - 行数对账:SELECT COUNT(*) 与文件数一致 - 抽样比对:随机 5 个 case,逐字段 diff 文件 vs DB - 一致性:opt_exec.comparison_status 分布合理(多数 equal

双写策略(阶段二预案)

写路径追加点(不替换原逻辑,仅追加 DB 写):

生产代码 追加 DB 调用 时机
persistence.py:14 persist_round_result opt_result_repo.insert_round() JSON 落盘成功后
execution.py:11 _write_optimized_sql opt_exec_repo.insert_exec() 三件套落盘后
sql_exe.py:213 run_baseline sql_case_repo.update_baseline() baseline 跑完后
csv_batch.py:44 init_raw_dir sql_case_repo.upsert_sql_case() case 初始化时
table_ctx/stores.py:27 write_json table_ctx_repo.upsert_table_ctx() 文件写入后

失败处理:所有 DB 写入用 try/except 包裹,logger.warning 记录,不抛异常。文件路径仍是 source of truth。

读路径迁移(阶段二预案)

生产代码 改造方式
ana_multi_version.py:46,47 文件发现 SELECT DISTINCT sql_case_name FROM opt_result
ana_multi_version.py:74,101 三件套加载 repo 查询替代
export_dba_display_csv.py:337,413,472,530 聚合 SQL 聚合 + Python 后处理
table_context.py:21 build_table_info 全量迁移(用户决议)
dataprocess_v3/sql_process/engine/*.py 文件 I/O 全量迁移(用户决议)

CLI 开关bin/sqlopt-run --storage={file,db,both}bin/sqlopt-report --source={file,db}

验收标准

阶段一: - ✅ core/sqlite_storage/sql_opt_results.db 中 6 表数据齐全(行数对账通过) - ✅ 生产代码 0 改动(git diff core/ bin/ 为空) - ✅ 抽样 5 个 case 文件 vs DB 字段一致

阶段二: - ✅ --source=db 模式下 display.xlsx 与 --source=file byte-level 一致 - ✅ 一轮优化跑完后 sql_case / opt_result / opt_exec 都有正确数据 - ✅ 文件模式行为不变

阶段三: - ✅ 移除文件 I/O 后所有功能正常 - ✅ data/ 转为归档(按月 zip)