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