![]()
作者 | Robbie Coomber
譯者 | 平川
策劃 | Tina
本文最初發布于 PostHog 官方博客。
在 使用代理利用 autoresearch 成功提高查詢性能 后,我想嘗試一些更有野心的事情。
我使用多個長時間運行的并行 Claude Code 會話重寫了 PostHog 的 SQL 解析器。結果是 16K 行“手工”編寫的解析器代碼,5K 行工具代碼,幾千行測試代碼,以及大約 70 倍的速度提升。
在所有針對實際應用場景的查詢中,新解析器均與舊解析器等效,僅在極少數由“惡作劇之神”編寫的極端邊緣案例中存在差異(例如,針對 SELECT SELECT FROM FROM WHERE WHERE AND AND 這類完全合法但極具迷惑性的 SQL 語句的測試用例)。
以下是我的實現過程以及我在這個過程中學到的東西。
為什么 PostHog 會有一個 SQL 解析器?
PostHog 允許你直接使用 SQL 訪問數據。我們將你的 SQL 轉譯成原始的 ClickHouse SQL,因為:
我們希望呈現一個與數據庫物理布局無關的數據邏輯視圖。
這讓我們可以在數據庫層進行變更,而不會破壞現有查詢。
我們還可以添加許多性能優化和訪問控制。
大多數 PostHog 工具(例如產品分析、會話回放、錯誤跟蹤)都使用 SQL 編寫查詢,它們都經過相同的轉譯過程。但在進行這種轉譯之前,我們需要使用解析器將 SQL 轉換成 AST(抽象語法樹),然后再將其轉譯成 ClickHouse SQL。
解析器是第一個接觸查詢的東西,這意味著它操作的是不受信任的輸入。下游的所有東西,如訪問控制和優化,操作的都是它生成的樹。
使用 ANTLR 生成我們的解析器
我們沒有手工編寫這個解析器,因為至少在 AI 編碼出現之前,解析器非常難以維護。沒有 AI 的幫助,編寫一個解析器需要花幾個月的時間,即使它能顯著地提高我們的 p95 響應時間,也可能不值得。
現在,我們使用了 ANTLR,這是一個最先進的開源解析器生成器。你只要以聲明式的方式在.g4 文件中提供你的語法,ANTLR 就會為你生成大部分的解析器代碼。我們使用 C++ 版本,所以它本身已經運行在一種“快速”語言之上。與我們的功能標志重寫不同,加速不僅僅來自于遷移到 Rust 語言。
ANTLR 功能非常強大和靈活,但相應的代價是,它對訪問到的每個 Token 都要進行更多的處理。它會將語法規則編譯成 ATN(本質上是一種帶棧的非確定性有限自動機),并在運行時讓一個通用的解釋器遍歷這個圖。這里沒有手動編寫的 parseExpression() 方法;所有操作都是通過額外的抽象和間接層來完成的。
此外,ANTLR 支持任意動態前瞻(arbitrary dynamic lookahead)。因此,當存在多種可能的備選路徑時,它必須同步模擬每一種解釋,直到僅剩一種解釋有效為止。盡管其優化程度極高,但這種基于圖遍歷的解釋器在速度上永遠無法與手工編寫的遞歸下降解析器相媲美。
編寫一個新解析器,不要犯錯
有了 AI,編寫和維護手工編寫的解析器變得更加可行。遺憾的是,這并不像告訴 Claude“用 Rust 編寫一個新解析器,不要犯錯”那么簡單。實際上,它犯了很多錯誤,讓我一直懷疑這樣的重寫是否可能,并且在完成每一輪編碼后都想就此停下。老實說,我也不知道這是否可能。
我同時測試了兩種方法:
一種側重于性能。我知道,如果它有效,最快的解析器可能會是遞歸下降,帶有 Pratt 表達式循環,只在必要時添加前瞻和回溯。
另一種方法則側重于最有可能成功實現解析器的方案。它盡可能地遵循了 ANTLR 的行為,但將狀態轉換通過顯式代碼實現,而非采用通用的圖遍歷方式。
最終,這兩種方法的效果差不多,但我直到為此忙碌了幾天后才意識到這一點。
我的目標是確保新解析器在所有實際查詢中與 oracle(即現有的 C++ 解析器)完全一致,并在人為構造的查詢中盡可能地與其接近。oracle 對我開發新解析器至關重要,因為我基本上可以通過以下方式進行測試驅動開發:找出在兩個解析器之間存在差異的那些 SQL 語句,修正新解析器直到結果一致,然后重復這一過程。
生成分歧點(多種方式)
起初,生成分歧點或測試用例相當容易,因為我們在開發原來的解析器時已經寫了很多回歸測試。一旦這些測試都通過了,事情便開始變得有趣。
基于屬性的測試
我以前使用的是 PBT(基于屬性的測試)庫 Hypothesis,我們在 SQL 轉譯器中發現過錯誤。你定義代碼的一些屬性以及它接受的輸入,它會嘗試生成不滿足該屬性的輸入。
舉個具體的例子,我這個新解析器的屬性與 oracle 一致。輸入是一個 SQL 查詢。這意味著 Hypothesis 將嘗試找到一個 SQL 查詢,使得我的新解析器與 oracle 的處理結果不一致。
我必須告訴 Hypothesis 如何生成有趣的 SQL 語句,于是我(與 Claude 合作)編寫了一個工具,基于 ANTLR 語法文件自動生成 SQL 生成器。我得承認,當編寫一個新的 SQL 解析器竟然導致我還得為.g4 文件編寫一個新的解析器時,我不禁會心一笑。后來,我還增加了一個步驟,用于在生成的 SQL 中添加額外的排列,比如交換 Token 或添加括號。
針對脆弱修復的提示工程
PBT 能夠可靠地生成新的測試用例,我的開發循環也運行良好,但 Claude 卻總是在做出脆弱的修復。例如,它會通過添加一個 Token 的前瞻來修復某個具體的問題,但隨后又發現其實需要兩個 Token 的前瞻。我經常遇到上下文窗口達到上限而不得不進行壓縮的情況,因此我懷疑它只是“忘記”了實際的語法或參考解析器是什么樣子。
這可以通過一些基本的提示工程來解決。我只是告訴它在編寫任何代碼來修復特定的分歧點之前,立即將語法文件和相關的 C++ 源代碼加載到上下文中。想通這一點,我花的時間比我愿意承認的要長。
全力投入、絞盡腦汁
此時,我希望在編寫解析器時,既能讓 CPU 始終滿負荷運行 PBT,又能讓 Claude 的推理任務保持滿負荷狀態,因此我編寫了一些工具,讓 PBT 能在后臺持續運行,并將新失敗的測試用例寫入文件,而不是僅僅將其顯示出來。這樣,當 Claude 沒有其他任務處理時,就可以調取這些測試用例。
我還有其他幾種生成失敗測試用例的方法,比如從生產環境的查詢日志中提取匿名查詢。有趣的是,最有效的方法之一是告訴 Claude 在后臺代理中“深入思考邊緣情況”。
這兩個平行的解析器方法共享相同的回歸套件,因此,在一個會話中發現的任何失敗的測試用例都會與另一個會話共享。
Hypothesis 還能幫你“精簡”測試用例,將其轉化為最簡單的重現步驟,但我無法將其用于來自其他來源的 SQL 語句。對于這類情況,我改用了 ShrinkRay。
后來,我添加了基于代碼覆蓋率的測試用例生成功能,這使得生成的 SQL 分布更加均衡。借助覆蓋率反饋,生成器能夠識別出尚未覆蓋的語法結構,并有針對性地生成更多的這類用例。雖然這并非在生產數據集上達到 100% 準確率的必要條件,但確實幫助我發現了一些非常微妙的測試用例。
最后的迭代循環
最終,我的迭代循環是這樣的:
從 PBT、真實語料庫、回歸測試和“深入思考邊緣情況”生成新的失敗測試
將這些失敗案例的精簡版本添加到不斷擴充的回歸測試列表中
深入思考最佳修復方案,盡可能采用通用解決方案,并查閱語法規則和 C++ 源代碼以了解參考解析器是如何處理該問題的
實施修復,并生成一段簡要的總結供人工操作員閱讀
運行回歸套件,確保一切測試均通過
自動重新運行循環
由于新解析器的運行速度快很多,所以我可以在生產環境中以“影子模式”運行這個循環,同時繼續使用現有的 C++ 解析器,并報告是否存在任何差異。
與生產環境的查詢日志進行對比時,我之前只測試過約 5 萬個查詢。在影子模式下,我能夠快速測試數百萬次解析,而且未發現任何偏差。我原本計劃讓它運行幾天,但結果如此理想,以至于僅過了幾個小時,我就將生產流量切換到了影子模式(同時啟用了 0.1% 的“反向影子”)。
一個快 454 倍的解析器及未來展望
現在,它生成的輸出(AST+ 源代碼位置)與基于 C++ ANTLR 的解析器完全一致,而性能結果(黃色部分)簡直令人難以置信:
![]()
新解析器的基準測試結果
在生產環境查詢中,其速度平均比之前的解析器快了 454 倍。標題中提到的“70 倍”來自我在筆記本電腦上進行的基準測試,但在生產環境中,我們主要解析的是比較長的未命中解析器緩存的 SQL 語句。
這對我來說是一次進步。用幾天時間就完成一項專業人員可能需要花費數月才能完成的工作,這讓我倍感自信。
雖然我并沒有親手編寫任何代碼,但我絕不會把這稱為“憑感覺編寫的代碼”。我的 PBT 配置基于語法文件生成輸入,并采用覆蓋率來引導生成過程,這在解析器模糊測試領域已經相當接近最先進水平。
這對 ANTLR 這類工具來說意味著什么?這個問題確實很有意思。我猜測,像我這樣使用基于 AI 的方法將成為一種新常態。解析器生成器將提供 oracle,然后大型語言模型(LLM)會利用 PBT/ 模糊測試并通過“手工”調整來構建性能更高的解析器,使其與 oracle 的處理結果相匹配。
我最終得到了什么?從形式上講,我的新解析器是一款“手寫”的、以預測性遞歸下降為主體的解析器。它搭載 Pratt 表達式核心,配備一個 LL(2) 游標(在特定位置通過有界且不消耗資源的前瞻探測進行擴展),并針對少數需要決策的情況,保留了局部有序選擇式試探回溯能力。它完全由 Claude Opus 4.7 生成,使用 Rust 語言編寫,于 2026 年 5 月開發完成。
https://posthog.com/blog/sql-parser
聲明:本文由 InfoQ 翻譯,未經許可禁止轉載。
![]()
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.