Agent 执行 SQL 前该先估成本还是采样
文章比较在数据规模未知时,先检查查询计划成本与先做有界采样两种 SQL 安全门禁。讨论限定于只读分析查询,利用 EXPLAIN 的估算信息或受限执行,降低全表扫描和聚合落盘的风险。
AI 深度解读
AI 生成的 SQL 能执行,不代表它适合在生产库里执行。三个 JOIN、一次聚合,再加上日期过滤,看起来很正常,也可能扫过规模不明的大表,把聚合结果写到磁盘。原文讨论的是:在 Agent 自动执行查询之前,应该先相信执行计划的成本估算,还是先跑一小部分数据看看?
作者的结论是先用计划筛选,再把可疑查询送去受控试跑。值得读的地方是两种检查各自会漏掉什么,以及如何把这些分歧变成可校准的执行规则。
为什么先看计划,仍然挡不住慢查询
原文开头的大表扫描场景是作者构造的复现,并非客户事故,也不是作者记录过的亲身经历。讨论建立在几个前提上:Agent 生成只读分析 SQL,目标是无法完全控制的数据仓库,可以低成本获取执行计划,也有办法限制试跑范围,同时不能让危险查询在生产环境完整执行。
先看成本的一派选择 EXPLAIN (FORMAT JSON),不加 ANALYZE。数据库解析、重写并规划语句,但不执行查询读取数据。即使 Agent 生成二十个候选,也可以先统一检查计划,再决定哪些值得继续。
检查通常有两部分:总成本是否超过预算,计划里是否出现特定危险节点。例如,对事先标记的大表执行 Seq Scan,即顺序扫描,就可以触发进一步检查。
不过,Total Cost 是规划器的估算单位,不能直接当成毫秒。统计信息过期、缺失,或者两个过滤条件存在规划器没识别出的相关性,都可能让估算行数与实际行数相差几个数量级。作者还提醒,连接产生的中间结果可能迅速膨胀,单看输入表规模容易低估风险。
成本检查适合做便宜的第一道筛选,但不能充当运行时间保证。
作者建议在预发布克隆库里比较 EXPLAIN 与 EXPLAIN ANALYZE,观察估算与实际执行的差距。不过,原文没有给出具体工作负载、误差分布或对照数据,这部分属于机制分析与经验判断。
为什么加了 LIMIT,也不等于安全采样
先试跑的一派认为:与其依赖估算,不如让相同查询结构在有界输入上执行,测量耗时和返回行数。原文提出组合使用 LIMIT、statement_timeout 和只读事务,并认为在受限数据切片上不到一秒的执行结果,能提供更多运行证据。
问题在于,限制返回结果和限制计算过程是两回事。
对于先聚合、再按聚合值排序的 SQL,外层 LIMIT 可能要等内部处理大量数据之后才能生效。它限制的是最终返回多少行,未必限制扫描多少行、连接多少次。
LIMIT 也不是随机采样。没有明确排序的扫描返回执行器最先产出的记录,数据头部和尾部的分布可能不同。试跑恰好避开倾斜数据,正式执行仍然可能卡住。
试跑位置同样关键。只读事务能够约束写入,却不保证查询不争抢资源,也不能消除锁竞争。原文明确建议:没有非生产试跑目标,就不要采用这套探测方式。
因此,试跑结果只能说明这次受限执行发生了什么。它能补充估算,却不能替完整执行作担保。
两道检查应该怎样接进 Agent 循环
作者提供的是面向 PostgreSQL 和 psycopg 3 的模板,没有针对读者的数据仓库做过基准测试。
示例设置 COST_BUDGET = 50_000.0,超时为 PROBE_TIMEOUT_MS = 3_000。大表集合 LARGE_RELATIONS 包含 events_raw、sessions_daily、billing_ledger。程序遍历计划中的 Plans,读取根节点的 Total Cost,再结合 Node Type 和 Relation Name 判断是否命中大表顺序扫描。
下面的路由代码决定候选 SQL 是重写、试跑,还是放行:
def route(conn, sql):
total, seq_on_large = plan_signals(plan_doc(conn, sql))
if total > COST_BUDGET:
return "rewrite", total
if seq_on_large or total > COST_BUDGET * 0.4:
return "probe", total
return "allow", total
要看清第一条判断的位置:超过预算直接返回 "rewrite",不会继续试跑。作者的理由是,估算已经超标时,没有必要再承担一次执行风险。成本超过预算的 0.4,或命中大表顺序扫描,则进入 "probe";其他情况返回 "allow"。这些阈值都需要按实际环境校准。
下面是原文用于受控执行的试跑函数:
def probe(conn, sql, row_cap=100_000):
with conn.transaction():
conn.execute("SET TRANSACTION READ ONLY")
conn.execute("SET LOCAL statement_timeout = %s", (PROBE_TIMEOUT_MS,))
wrapped = f"SELECT * FROM ({sql}) AS probe_q LIMIT {int(row_cap)}"
with conn.cursor() as cur:
cur.execute(wrapped)
return len(cur.fetchall())
需要留意 wrapped 那一行:它把原 SQL 包进子查询,再加外层 LIMIT,并没有创建真正的数据样本。默认 row_cap=100_000 也只是结果行数上限。函数返回的是行数,原文主张记录的实际耗时、超时处理及完整决策流程,还需要接入方补齐。
另一个缺口是统计信息新鲜度。作者在文字中要求,无法确认统计信息是否新鲜时也应升级试跑,但上面的 route() 没有这项输入。因此,这段代码只实现了部分路由规则。
最后,循环要记录 SQL 哈希、总成本、命中的关系、路由结果和试跑结果。要求重写时,把失败计划、节点类型、成本与拒绝原因一起交给模型,比只反馈「查询太贵」提供了更明确的修改方向。至于效果提升多少,原文没有量化。
我的判断
我赞成把成本检查放在前面,也赞成用真实执行证据校准规则。它把数据库侧的判断独立于 SQL 生成模型:换模型之后,执行门槛仍然可以保留。
但这套方案更适合被当作风险路由模板。50_000.0、0.4、3_000 都不是经过普遍验证的安全线;一次小规模试跑成功,也不足以证明全量查询安全。原文提出并行运行两道检查一周、记录分歧,这比直接照搬阈值更有价值。
平台前提也不能忽略。方案依赖 PostgreSQL 风格的 JSON 计划和可比较的成本字段;没有稳定成本字段的仓库,需要重新选择结构信号。缺少 statement_timeout 的引擎,则需要连接层取消机制。
如果团队已有强制人工审核,并通过语义层限制可生成的查询,新增检查可能只是增加另一处策略配置。对于亚秒级交互查询,额外一次 EXPLAIN 往返也可能占掉明显预算,作者建议按查询模板规划一次,而非每次执行都规划。
原文还披露了 MonkeyCode 产品推广合作,提到约一千万 tokens 的免费额度和免费服务器选项。这是有时效的商业信息,原文没有提供当前有效性的证据,不能直接拿来做预算依据。免费资源能降低实验成本,但不会提高检查准确性。
能拿来做什么
- 如果你在做自然语言报表或 Agent 查数,把执行计划检查放在生成 SQL 与执行 SQL 之间。先明确成本预算和大表名单,再将具体拒绝原因反馈给模型。
- 如果你负责 Node 服务里的查询执行入口,可以参考原文分离
"rewrite"、"probe"、"allow"三种结果。采用其他数据库驱动时,需要重新确认只读事务、超时和取消机制。 - 在非生产环境并行观察两道检查一周,重点分析成本低却执行慢、成本高却执行快的分歧,再决定是否调整预算,以及哪些关系需要维护统计信息。
- 如果查询模板稳定、响应要求达到亚秒级,优先评估按模板检查计划。若已有人工审核和语义层约束,先确认新检查能补上什么缺口,再增加流程。