我重写了PostHog的SQL解析器,速度快了70倍,几乎没有查看代码
在通过自助研究成功地利用代理提高查询性能后,我想尝试一些更雄心勃勃的项目。我使用多个同时运行的Claude代码会话重写了PostHog的SQL解析器。结果是16000行“手写”的解析器代码,5000行工具代码,再加上几千行测试,速度提高了大约70倍。新的解析器在所有合理查询上与之前的解析器等效,只有在一些由邪恶的恶作剧神写的查询中有所不同(有一个测试针对SELECT SELECT FROM FROM WHERE WHERE AND AND,这完全是有效的SQL)。下面是我如何做到这一点以及我在这个过程中学到了什么。PostHog为什么会有SQL解析器? PostHog让你能直接用SQL访问数据。我们将你的SQL转译成原始的ClickHouse SQL,因为:我们希望提供一个独立于数据库物理结构的逻辑视图。这让我们可以在不破坏现有查询的情况下更改数据库层的内容。我们还可以添加一系列性能优化和访问控制。大多数PostHog工具(例如产品分析、会话重放、错误追踪)都以SQL编写查询,并经历相同的转译过程。但在我们能进行这个转译之前,我们需要使用解析器将SQL转换为AST(抽象语法树),然后再转译成ClickHouse SQL。解析器是接触查询的第一个元素,意味着它在处理不可信的输入。所有下游操作,如访问控制和优化,都是在它生成的树上运行的。我们没有手动编写这个解析器,因为,至少在AI编码之前,解析器是非常难以维护的。没有AI的话,编写一个解析器会花费几个月的时间,并且可能不值得,即使这显著改善了我们的p95响应时间。取而代之的是,我们使用ANTLR,一个最先进的开源解析器生成器。你在.g4文件中声明语法,ANTLR为你生成大部分解析器代码。我们使用C++版本,因此它已经在“快速”语言中。与我们的标志重写不同,速度提升不仅仅来自转向Rust。ANTLR极其强大且灵活,但其权衡是每当它访问一个记号时,需要做更多的工作。它将你的语法编译成ATN(实质上是带栈的NFA),并在运行时让一个通用解释器遍历图形。没有手动编写的parseExpression(); 一切都是通过额外的抽象和间接层完成的。此外,ANTLR支持任意动态前瞻,因此如果有多个可能的替代方案,它必须在锁步中模拟每个解释直到只有一种解释有效。它经过了极好的优化,但图形遍历解释器的速度永远不会像手动编写的递归下降解析器那样快。有了AI,编写和维护手动编写的解析器变得更加可行。可悲的是,这并没有像告诉Claude“在Rust中编写一个新的解析器,不要犯错”那么简单。事实上,它确实犯了很多错误,不断怀疑这样重写是否可能,并且每轮编码后都想要打道回府。老实说,我也不太确定这是否可能。我在并行测试了两种方法:一种专注于性能。我知道,如果成功,最快的解析器将是带有Pratt表达式循环的递归下降,仅在必要时添加前瞻和回溯。另一个专注于最有可能导致成功解析器的方法。它尽可能紧密地遵循ANTLR的行为,但在显式代码中实现过渡,而不是作为通用图遍历。最后,这两种方法的效果差不多,但我直到工作了几天才知道这一点。我的目标是与oracle(即现有的C++解析器)对所有合理查询完全一致,并尽可能接近那些构造的查询。拥有一个oracle对于我如何开发新的解析器至关重要,因为我可以基本上通过找到一些解析器不一致的SQL进行测试驱动开发,修复新的解析器以使其一致,然后重复这个过程。生成不一致或测试用例开始时相当容易,因为在开发原始解析器时,我们已经编写了许多回归测试。一旦那些都通过,事情就开始变得有趣。基于属性的测试我之前使用Hypothesis,一个基于属性测试(PBT)库,在我们的SQL转译器中发现了错误。你定义一些代码属性以及它接受的输入,它会试图生成那些属性不成立的输入。举个具体的例子,我的新解析器的属性是它与oracle一致。输入是一个SQL查询。这意味着Hypothesis将尝试找到一个SQL查询,在这个查询中,我的新解析器与oracle不一致。我必须告诉Hypothesis如何生成
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡