重写PostHog SQL解析器提速70倍且改动极小
速览
作者通过重写PostHog的SQL解析器,实现了高达70倍的性能提升。这一优化过程几乎不需要深入阅读或修改原有代码,展示了高效重构的可能性。该成果对提升数据分析工具的处理速度具有重要意义。
AI 深度解读
重写 PostHog SQL 解析器:70倍加速,几乎未看源码
背景
PostHog 是一个开源的产品分析平台,其核心功能之一是允许用户直接使用 SQL 访问数据。为了提供逻辑上独立于数据库物理布局的数据视图,PostHog 会将用户的 SQL 查询转译为原始的 ClickHouse SQL。这种转译机制不仅使得底层数据库结构的变更不会破坏现有查询,还允许平台实施性能优化和访问控制。
PostHog 的大多数工具(如产品分析、会话回放、错误追踪)生成的查询都经过相同的转译流程。然而,在执行转译之前,必须首先使用解析器将 SQL 字符串转换为抽象语法树(AST)。解析器是处理查询的第一个环节,它直接面对不受信任的输入,其生成的 AST 将决定下游所有访问控制和优化逻辑的正确性。
在引入 AI 编码工具之前,PostHog 使用 ANTLR 这一先进的开源解析器生成器来构建 SQL 解析器。虽然 ANTLR 功能强大且灵活,但它通过编译语法为 ATN(带栈的非确定有限自动机)并在运行时通过通用解释器遍历图来工作。这种抽象层和间接性导致其在处理每个令牌时执行大量额外工作,尤其是支持任意动态前瞻(lookahead)时,必须同步模拟所有可能的解释直到确定唯一有效路径。尽管 ANTLR 已高度优化,但其基于图遍历的解释器速度始终无法与手工编写的递归下降解析器(recursive-descent parser)相比。
随着 AI 编码能力的提升,作者决定尝试更具雄心的项目:利用多个长期运行的 Claude Code 会话并行重写 PostHog 的 SQL 解析器,旨在实现性能的大幅提升,同时尽量减少对现有代码的直接干预。
核心内容
作者最终产出了约 16,000 行“手工”编写的解析器代码、5,000 行工具代码以及数千行测试代码,实现了约 70 倍的速度提升。新解析器在所有现实查询中与旧解析器完全等效,仅在极个别由“恶作剧之神”编写的极端边缘情况(如 SELECT SELECT FROM FROM WHERE WHERE AND AND,这在 SQL 语法上是完全合法的)上存在差异。
1. 开发策略:并行探索与“预言机”验证
作者并未简单地命令 AI “用 Rust 重写解析器且不出错”,因为 AI 确实会犯错,且经常怀疑重写的可行性。相反,作者采用了并行测试两种策略的方法:
- 性能优先策略:基于递归下降解析器,结合 Pratt 表达式循环,仅在必要时添加前瞻和回溯。这是理论上最快的解析方式。
- 兼容性优先策略:尽可能模仿 ANTLR 的行为,但将状态转换实现为显式代码而非通用图遍历。
为了验证正确性,作者确立了“预言机”(Oracle)概念,即现有的 C++ 解析器。开发过程本质上是测试驱动开发(TDD):寻找新旧解析器输出不一致的 SQL 查询,修复新解析器以达成一致,并重复此过程。
2. 测试用例生成:从回归测试到属性测试
生成测试用例(即新旧解析器产生分歧的案例)是开发的关键。
- 初始阶段:利用已有的回归测试套件。
- 属性测试(PBT):引入 Hypothesis 库进行属性测试。定义属性为“新解析器与预言机一致”,输入为 SQL 查询。Hypothesis 会自动生成试图破坏该属性的 SQL 查询。
- SQL 生成器:为了让 Hypothesis 生成有意义的 SQL,作者(与 Claude 协作)基于 ANTLR 语法文件(.g4)编写了一个代码生成器,用于生成 SQL 测试用例。有趣的是,重写 SQL 解析器导致需要重写一个用于解析 .g4 文件的解析器。
- 扰动增强:在生成的 SQL 中添加额外排列,如交换令牌或添加括号,以增加覆盖范围。
- 其他来源:
- 从生产查询日志中提取匿名查询。
- 利用后台 AI 代理“深入思考边缘情况”。
- 使用 ShrinkRay 对非 Hypothesis 生成的失败案例进行最小化还原。
- 后期引入代码覆盖率引导的测试生成,使生成的 SQL 分布更均匀,覆盖未执行的语法结构。
3. 解决 AI 编码的痛点:上下文管理与提示工程
在开发过程中,AI 助手(Claude)经常出现“脆弱修复”(brittle fixes),例如为了解决一个案例添加一个令牌的前瞻,随后又发现需要两个令牌的前瞻。这通常是因为上下文窗口耗尽导致 AI “忘记”了原始语法或参考解析器的逻辑。
作者通过以下手段解决了这一问题:
- 提示工程优化:在修复特定分歧之前,强制 AI 立即将语法文件和相关的 C++ 源代码加载到上下文中。
- 自动化工作流:编写工具让 PBT 在后台持续运行,将新的失败测试用例写入文件。AI 在空闲时获取这些用例进行修复。
- 共享回归套件:两个并行开发的解析器会话共享回归测试套件,任何一个会话发现的失败用例都会同步给另一个,加速收敛。
4. 最终迭代循环
最终的迭代流程如下:
- 从属性测试、真实语料库、回归测试以及“深入思考边缘情况”的 AI 代理中生成新的测试失败案例。
- 将失败案例的最小化版本添加到不断扩展的回归测试列表中。
- 深入分析最佳修复方案,优先选择通用解法,并阅读语法文件和 C++ 源代码以了解参考解析器的处理方式。
- 实施修复,并生成一段供人类操作员阅读的一段落摘要。
关键要点
- 性能飞跃:通过从 ANTLR 生成的基于图遍历的解释器转向手工编写的递归下降解析器,实现了 70 倍 的执行速度提升。
- AI 辅助开发的局限性:AI 无法一次性生成完美代码,容易在上下文窗口限制下产生“遗忘”或脆弱修复。需要人工介入设计工作流,如强制加载参考代码到上下文。
- 测试驱动的重要性:利用“预言机”(现有解析器)进行差异测试是确保新解析器正确性的核心。结合属性测试(Hypothesis)和覆盖率引导生成,能够高效挖掘边缘情况。
- 工具链的创新:为了生成高质量的测试数据,甚至需要编写新的解析器来解析 ANTLR 的语法文件(.g4),体现了元编程在解决复杂工程问题中的价值。
- 并行探索策略:同时探索“极致性能”和“高兼容性”两种实现路径,并通过共享测试套件加速收敛,是一种高效的 AI 编码协作模式。
- 现实与理论的平衡:新解析器在 100% 的现实生产查询中与旧解析器行为一致,仅在极罕见的语法边缘情况上有差异,证明了工程上的可行性。
意义与影响
这一案例展示了 AI 编码工具在系统级重构中的巨大潜力,同时也揭示了其当前的局限性。
- 解析器开发的范式转变:传统上,手写高性能解析器需要数月时间且维护成本极高。AI 使得快速迭代和验证复杂语法逻辑成为可能,降低了高性能底层组件的开发门槛。
- 工程流程的重构:成功的 AI 编码不仅仅是提示词工程,更是工作流工程。作者通过构建自动化测试生成、上下文管理和并行会话协调的工具链,克服了 AI 的上下文限制和错误倾向。这种“人机协作+自动化测试”的模式可推广至其他复杂代码库的重构。
- 对 PostHog 及类似平台的影响:70 倍的性能提升意味着用户在进行复杂 SQL 查询时的响应时间将大幅缩短,提升了产品分析平台的用户体验。同时,更高效的解析器也为未来更复杂的查询优化和实时分析能力奠定了基础。
- 对开源社区的启示:该实践证明了利用开源解析器生成器(如 ANTLR)作为“预言机”来验证手写解析器正确性的有效性,为其他需要高性能自定义解析器的开源项目提供了参考路径。
