训练一个 4B 模型,产出比 Postgres 快 81% 的查询计划

查看原文 HN 讨论

文章摘要

Rohan Bansal 在 Recurse Center 休假期间做了一个实验:能不能把一个只有 40 亿参数的开源权重小模型,通过监督微调(SFT)加智能体强化学习(RL)后训练,让它给 Postgres 产出比 Postgres 自己默认计划更快的查询计划?这篇长文是完整的实验记录,从问题背景一路写到成本核算。

问题背景。 Leis 等人 2015 年发表《How Good Are Query Optimizers, Really?》问过这个问题,十年后又问了一遍,结论都是查询优化器仍然远远称不上好。原因之一是连接排序(join ordering)本身就是 NP 难问题。作者用 IMDb 数据集举了个直观例子:同一个三表连接查询,在加上选择性谓词(比如「日本公司」「2000 年代出品」)之后,两种连接顺序的中间结果分别是约 10 万行和约 40 万行——选错顺序就要多做 4 倍的工作。再叠加哈希连接 / 归并连接 / 嵌套循环三种连接算法、内外表的四种朝向、各种扫描方式,搜索空间会组合爆炸。

问题的正确提法。 作者的第一个想法是给模型和 Postgres 优化器完全相同的信息,看能否做出更好的基数估计器。他很快否掉了这条路:那是在跟几十年的基数估计研究硬碰硬,而且光是推理延迟就会吃掉所有收益。第二个想法才是他认为对的提法——瞄准重复运行的分析型工作负载。如果某个查询用次优计划跑了成千上万次,那么哪怕训练过程需要提前执行这个查询几十上百次,摊销下来也极其划算。目标不是在一次性查询上打赢 Postgres 的时间/效率帕累托前沿,而是在反复运行的查询上赢。

技术路线。 落地手段是 pg_hint_plan:模型不改写 SQL,只产出提示(hint)来影响 Postgres 的计划选择。作者基于一个 4B 模型(Qwen 3.8 系列的一个 4B 蒸馏版本)搭了个叫 qo-agent 的轻量智能体框架,给它六个工具:查看表结构与索引、获取列统计、获取计划、评估候选计划(编译成 hint、跑一次并返回计时)、保留默认计划、结束搜索。模型以结构化输出产出 PlanAction JSON。一次典型轨迹是:先问默认计划,然后提交几个候选(比如「先 join t 和 mc,第一步走嵌套循环,mc 走索引扫描」),每次拿到「有效 / 新计划 / 87ms / 1.24× 默认」这样的反馈,候选额度用完后只剩 finish 工具,让模型挑出最好的一个。

基准与度量。 用的是 Join Order Benchmark(JOB,113 条 IMDb 上的连接密集查询)和 Cardinality Estimation Benchmark。度量用两个:几何平均加速比(给每条查询等权重,2× 和 0.5× 会互相抵消为 1×,衡量的是「是不是真学到了东西」)和总工作负载加速比(把整批当一个批次算总时间,衡量的是实用性)。作者特意解释了为什么两个都要——只看后者,一条碰巧撞上的大赢查询就能掩盖其余全部退化。

对照实验。 在训 4B 之前,作者先拿前沿模型验证问题本身可解:在 10 条 JOB 查询的切片上,允许 5 个候选时,GPT-6 Astra 拿到 2.54× 几何平均加速、2.12× 总加速、零退化,Qwen 3.8 2.4T 拿到 2.26× / 1.35×。单候选与 5 候选的差距说明智能体确实在多轮执行中做了上下文内学习,这让作者决定走多轮智能体路线,而不是训练模型一把梭。结论:前沿智能确实能智能体式地做查询优化。

未训练的 4B 基线惨不忍睹:113 条查询里有 81 条压根没产出任何有效候选,16 条选择失败。于是作者先做离策略蒸馏——花约 400 美元调 OpenAI API 生成了五百多条 Astra 轨迹,用 LoRA 做 SFT,把模型先教会「怎么用这个框架」。SFT 后的检查点达到 1.16× 几何平均加速。

强化学习。 作者为这个天然有噪声的环境设计了一个定制的 GRPO 变体来给 rollout 打分,并解决了一个很实际的工程问题:Postgres 测量必须跟训练分开跑。他在租来的 Lambda 2×H100 节点上做校准时发现噪声大得离谱,怀疑非 GPU 部分和别人共享,于是把四个 Postgres 容器留在自己桌上那台双 3090 机器(昵称 FLOPper)上,用 Tailscale 连到 Lambda 节点——rollout 在本地开始并独占一个 Postgres 容器,推理走 H100 上的 vLLM,测量在本地容器跑,结果再回传到第二块 H100 上算优势并更新权重。

结果。 极保守的 120 步更新完全没效果(甚至略微退化)。跑到 600 步:有效候选从 71/113 升到 99/113,几何平均 1.35×,总加速 1.16×,34 胜 0 退化。跑到 1200 步:101/113 有效,1.41× / 1.29×,38 胜 2 退化。最后用最终检查点对每条查询跑三条轨迹(每条最多 15 个候选,取最优),得到 1.81× 的几何平均加速比,总工作负载加速比也恰好是 1.81×,68 胜 0 退化——对应标题里的「快 81%」和摘要里的「总延迟下降 44.7%」。

模型学到了什么。 作者分析了最终评估的 339 次搜索:337 次中有 295 次会先去查表结构、列统计或默认计划(「调查员」行为);339 次里有 235 次把五个候选额度全部用光;在 1347 个动作里,扫描提示用了 1141 次、Leading 连接树 917 次、并行提示 572 次,而行数修正只用了 146 次;模型明显偏爱强制嵌套循环而非哈希连接、偏爱索引扫描,常用 enable_sort=off 和 random_page_cost=1.1。真正带来收益的主要是三种套路:用 Leading 重写连接顺序、不动连接顺序只修一个扫描、以及加并行。有一条 90 倍加速的案例里,模型的推理原文大意是「这个顺序扫描在 575k 行上做有损过滤,用位图应该快得多」,然后它真的强制了位图扫描。

成本。 如果不是作者急着看结果,这个项目本可以接近免费(只算 FLOPper 满载的电费,每天约 9 美元)。实际花费:Lambda 2×H100 节点约 95 小时约 800 美元,OpenAI API 约 400 美元,合计 1200 美元。

作者的结论是:很多公司手里有数据,搭一个 RL 环境、花一小笔钱在细分领域任务上训练开源权重模型,并不是遥不可及的事,而小模型因为训练和推理都更快更便宜,会在这个方向上越来越常见。他也强调这不意味着大模型会消失——整个实验正是靠从 Astra 轨迹蒸馏才起步的。后续计划包括对比 Bao 式的结构化 hint 扫描、尝试在策略蒸馏、以及在专用 EC2 机器上量化「被欺骗的奖励」噪声。

HN 评论精华

689 分、141 条评论,讨论集中在几个很技术的疑虑上。

最高票问题:怎么保证计划真的在执行你写的查询?

fsmv 一上来就问这个。topaz0 给了最清楚的澄清,也是全场最重要的一条纠正:这里存在误解——LLM 不修改查询本身,只影响 Postgres 如何在若干等价执行路径之间做选择。SQL 没变,保证结果正确仍然是 Postgres 的职责;如果提议的计划是无意义的,Postgres 会直接忽略它。作者本人(polyphilz)补充说 pg_hint_plan 有调试日志,可以确认 Postgres 到底采纳了没有,评估时就用了这个。amluto 从设计角度说,任何被接受的 hint 都应当天然是该查询的合法计划,这种性质并不难做到。quotemstr 从复杂度角度总结:正因为 P≠NP,存在一大类「生成昂贵、验证廉价」的问题,LLM 在这类问题上最有用。

优化不是应该确定性的吗?

kingjimmy 的疑问引出了一串懂行的回答。tintor 指出计划选择依赖各种汇总统计,而统计未必是最新的。cogman10 的实战描述很生动:他们的数据形状差异极大——通常一个 Foo 只有一个 Bar,但某些情况下能有一百万个;优化器在一般情况下判断正确,但那个边缘情况会把整个系统搞崩,这就像 n 很小时插入排序比快排快,而优化器对 n 的大小做了错误假设。

基准的代表性受到质疑

refibrillator 的批评最被认可:所谓「快 81%」是在一个 8GB、完全能装进内存的数据集上,shared_buffers 被限制成其中一小部分,查询在测量前预热过,而且全是只读 SELECT。他提醒要警惕过拟合,很难说这些计划在更大规模和更真实的 OLTP 负载下还是否更优——「这就是基于剖面的优化(PGO)的宿命」,数据库负载会随时间和规模漂移。不过他也肯定了作者亲自动手做实验并写出来的价值。

dragontamer 由此发散出一个更尖锐的观点:一个 4B 模型为了跑完这件事,大概要把 8GB 数据在内存里过好几遍;「到了某个程度,我们真该认真讨论一下用 CUDA 去加速 Postgres,而不是这个」——8GB 在今天连 CPU 内存都算不上,服务器 CPU 动辄 2TB,而 8~32GB 恰恰是普通 GPU 的容量,且 GPU 显存带宽是 CPU 内存的十倍以上。bt1a 反问瓶颈不是通常在磁盘吗,tomnipotent 回答说这正是好计划重要的原因——谓词下推、索引消除、连接排序、分区裁剪都是为了尽量避免磁盘 I/O。

成本与运维现实

Someone 注意到那 95 小时并没有算进基准数字里,并指出所有数据库厂商都知道,只要愿意花大量时间推导计划,优化器可以做得好得多;问题是「你能承受每隔一阵就花几小时更新你的 4B 模型,只为确保它还在挑好计划吗」。dvt 认为这可以类比成定期备份的维护窗口,但确定性算法很可能还是比重新微调更划算。hedgehog 提出了一个被多人认同的折中定位:把一个 4B 模型塞进数据库部署里没什么吸引力,但在开发/测试阶段用这套方法找出「Postgres 在这里留了性能在桌上」的查询,收益明显而风险很小。whazor 顺着说,hint 可以提交进 Git 并写测试验证,模型只在写代码时用。cannonpalms 泼冷水:统计或负载一变,这套就失效了,而这些 hint 是提前生成的,花了 95 小时。

setr 提到 Postgres 终于加入 hint 支持后这大概能做成扩展;williamdclt 提醒说 Postgres 会把实际参数值纳入计划生成,同一条查询不同参数本该产生不同计划。Sesse__ 纠正了一处技术细节:Postgres 并不盲目假设均匀分布,它对所有列都保存直方图(含最常见值)。

dvt 还抛了个有意思的思想实验:看看编译器这种被精心打磨了几十年的超优化软件,LLM 能带来什么架构或性能上的贡献吗(他自己的答案是不能)。btown 反驳说 Postgres 查询计划器几十年来面对的是比编译器实时性要求苛刻得多、信息受限得多的环境——它只能从孤立表的汇总统计下结论,看不到表之间的关系,加上过滤条件后更是如此。codebje 举了寄存器分配(图着色,NP 完全)为例,认为机器学习也许能得到更优解,但代价是编译时间、空间和确定性,而手工启发式的优势恰恰在于编译极快、内存占用小、出错时更好调试。FusionGaming 则认为自动向量化是 LLM 有未来的方向。

一段跑题但热闹的插曲:作者在结论里写「蒸馏 Astra 轨迹足以证明大模型不会消失」,devsda 调侃说在当下各家互相指控蒸馏的氛围里,这么坦白会不会惹麻烦。这条引出了一串关于版权和道德高地的争论——make3 说前沿模型训练方几乎把整个互联网的版权内容都拿走了,很难在这件事上占据道德高地,「就像从大英帝国手里偷一个苹果」;p1necone 冷冷回了句「道德高地没有一大笔律师费重要」;globalnode 的总结最精炼:「别偷我偷来的东西。」

最后还有人抱怨文章太长(coolThingsFirst:「看完得五天」),tandr 接了个梗:「他们忘了再跑一个模型来优化文章的阅读速度。」