プログラミング
ほとんどコードを見ずに、PostHogのSQLパーサーを70倍高速に書き直した
I rewrote PostHog's SQL parser, 70x faster, while barely looking at the code (posthog.com)
要約
PostHogのSQLパーサーをAI(Claude)の助けを借りて書き直し、70倍の高速化を達成したプロセスを解説しています。AIが生成したコードの正確性を確保するため、プロパティベースドテストとプロンプトエンジニアリングを駆使し、複雑なパーサー開発におけるAIの可能性と課題が示されています。最終的に、ほとんどコードを直接見ることなく、大規模なパフォーマンス改善を実現しました。
全文翻訳
ほとんどコードを見ずに、PostHogのSQLパーサーを70倍高速に書き直したRobbie Coomber2026年6月24日エンジニアリング、AI、PostHogの内部目次なぜPostHogはSQLパーサーを持っているのか?ANTLRでパーサーを生成する新しいパーサーを書き、間違いを犯さない対立を生み出す(様々な方法)プロパティベースドテスト脆い修正に対するプロンプトエンジニアリング最大化と熟考最終的なイテレーションループ454倍高速なパーサーと未来への展望自動リサーチを通じてクエリパフォーマンスを向上させるエージェントの利用が成功した後、私はもっと野心的なことに挑戦したかったのです。私は複数の長時間実行されるClaude Codeセッションを並行して使用し、PostHogのSQLパーサーを書き直しました。その結果、16K行の「手書き」パーサーコード、5K行のツール、数K行のテスト、そして約70倍の高速化が実現しました。新しいパーサーは、現実的なすべてのクエリにおいて以前のパーサーと等価であり、悪意のあるトリックスターの神が書いたごく一部のクエリ(SELECT SELECT FROM FROM WHERE WHERE AND ANDという完全に有効なSQLに対するテストがあります)のみで異なります。ここでは、私がどのようにそれを成し遂げ、その過程で何を学んだかを紹介します。
なぜPostHogはSQLパーサーを持っているのか?PostHogでは、SQLで直接データにアクセスできます。私たちはあなたのSQLを生のClickHouse SQLにトランスパイルします。その理由は以下の通りです。データベースの物理レイアウトとは独立した、データの論理ビューを提供したい。これにより、データベースレイヤーで変更を加えても既存のクエリを壊すことなく対応できます。また、多くのパフォーマンス最適化とアクセスコントロールを追加できます。PostHogのツールの大部分(例:プロダクトアナリティクス、セッションリプレイ、エラー追跡)はSQLで書かれたクエリを持ち、まったく同じトランスパイルプロセスを経ます。しかし、このトランスパイルを行う前に、パーサーを使ってSQLをAST(Abstract Syntax Tree:抽象構文木)に変換し、それをClickHouse SQLにトランスパイルする必要があります。パーサーはクエリに最初に触れるものであり、信頼できない入力に対して動作します。アクセスコントロールや最適化など、その後に続くすべての処理は、パーサーが生成するツリー上で動作します。
ANTLRでパーサーを生成するこのパーサーを手書きしなかったのは、少なくともAIコーディング以前は、パーサーのメンテナンスが非常に困難だったからです。AIなしで作成すれば数ヶ月かかり、たとえp95応答時間を劇的に改善したとしても、それだけの価値はなかったでしょう。代わりに、私たちは最先端のオープンソースパーサー生成器であるANTLRを使用しています。.g4ファイルで文法を宣言的に提供すると、ANTLRがほとんどのパーサーコードを生成してくれます。私たちはC++版を使用しているので、すでに「高速な」言語で書かれています。フラグの書き換えとは異なり、高速化はRustへの移行だけから得られるものではありませんでした。ANTLRは非常に強力で柔軟ですが、そのトレードオフとして、訪問する各トークンに対してはるかに多くの作業を行います。文法をATN(本質的にはスタック付きNFA)にコンパイルし、実行時にジェネリックインタープリターがグラフを辿ります。手書きのparseExpression()のようなものはなく、すべてが追加の抽象化と間接層を介して行われます。さらに、ANTLRは任意の動的な先読み(lookahead)をサポートしているため、複数の代替案がある場合、有効な解釈が一つになるまで各解釈をロックステップでシミュレートする必要があります。これは非常に最適化されていますが、グラフを辿るインタープリターが手書きの再帰下降パーサーほど高速になることはありません。
新しいパーサーを書き、間違いを犯さないAIを使えば、手書きのパーサーを作成し、維持することがはるかに可能になります。残念ながら、Claudeに「Rustで新しいパーサーを書き、間違いを犯さないで」と指示するほど簡単ではありませんでした。実際、多くの間違いを犯し、そのような書き換えが可能かどうかを疑い続け、コーディングの各ラウンドの後に作業を終えたがっていました。正直なところ、私自身も可能かどうかはよくわかりませんでした。私は2つのアプローチを並行してテストしました。1つはパフォーマンスに焦点を当てたもの。もしうまくいけば、最速のパーサーは、必要な場合にのみ先読みとバックトラッキングを追加するPratt式のループを持つ再帰下降パーサーになるだろうと知っていました。もう1つは、パーサーの成功につながる可能性が最も高いアプローチに焦点を当てたもの。ANTLRの動作にできるだけ忠実に従いましたが、一般的なグラフトラバーサルではなく、明示的なコードで遷移を実装しました。最終的に、どちらのアプローチも同じくらい上手くいきましたが、数日間作業するまでは分かりませんでした。私の目標は、すべての現実的なクエリにおいてオラクル(つまり既存のC++パーサー)と完全に一致すること、そして人工的なクエリについても可能な限り近づけることでした。オラクルを持つことは、新しいパーサーを開発する上で極めて重要でした。なぜなら、パーサーが意見の不一致を示すSQLを見つけ、新しいパーサーを修正して一致させ、これを繰り返すことで、実質的にテスト駆動開発を行うことができたからです。
不一致の生成(様々な方法)不一致、つまりテストケースの生成は、元のパーサーを開発中にすでに多くの回帰テストが書かれていたため、最初はかなり簡単でした。それらがすべてパスした後から、物事が面白くなり始めました。プロパティベースドテスト私は以前、PBT(プロパティベースドテスト)ライブラリであるHypothesisを使って、SQLトランスパイラーのバグを発見していました。これは、コードのいくつかのプロパティと、それが受け取る入力を定義すると、そのプロパティが成り立たない入力を生成しようとします。具体的な例を挙げると、新しいパーサーのプロパティは、それがオラクルと一致することです。入力はSQLクエリです。これは、Hypothesisが新しいパーサーがオラクルと一致しないSQLクエリを見つけようとすることを意味します。Hypothesisに興味深いSQLを生成する方法を教える必要があったため、私(とClaude)はANTLR文法ファイルに基づいてSQLジェネレーターをコード生成するツールを作成しました。新しいSQLパーサーを作成することが.g4ファイルの新しいパーサーを作成することにつながったとき、少し笑ってしまったことは認めざるを得ません。その後、トークンの入れ替えや括弧の追加など、生成されたSQLに追加の順列を加えるステップも追加しました。
脆い修正に対するプロンプトエンジニアリングPBTは信頼性の高い新しいテストケースを生成でき、私の開発ループはうまく機能していましたが、Claudeは脆い修正を繰り返し行いました。例えば、1トークンの先読みを追加して1つのケースを修正したかと思えば、後で2トークンの先読みが必要だと気づく、といった具合です。私は常に最大化されたコンテキストウィンドウに達し、圧縮していたため、Claudeが実際の文法や参照パーサーがどのようなものかを「忘れて」しまったのではないかと疑いました。これは基本的なプロンプトエンジニアリングで解決できました。特定の乖離を修正するコードを書く直前に、文法ファイルと関連するC++ソースコードの両方をコンテキストにロードするように指示するだけでした。これに気づくまで、認めたくないほど時間がかかりました。
最大化と熟考この時点で、私はPBTでCPUを最大化し、パーサー作成でClaudeの推論を最大化したかったので、PBTをバックグラウンドで常に実行させ、新しい失敗テストケースをファイルに書き出すツールを作成しました。これにより、Claudeは他に作業がないときにそれらを取得できました。私は他にも、本番環境のクエリログから匿名化されたクエリを抽出するなど、いくつかの失敗テストケース生成方法を持っていました。面白いことに、最も効果的だった方法の1つは、バックグラウンドエージェントでClaudeに「エッジケースについて本当に深く考える」ように指示することでした。2つの並行パーサーアプローチは回帰テストスイートを共有していたため、一方のセッションで見つかった失敗テストケースは他方と共有されました。Hypothesisはテストケースを「削減」し、最小限の再現にすることもできますが、他のソースからのSQLではそれを使用できませんでした。それらには代わりにShrinkRayを使用しました。その後、コードカバレッジに基づいたテストケース生成を追加しました。これは、生成されるSQLの分布をより良くします。カバレッジフィードバックにより、ジェネレーターはまだ実行されていない構成要素を把握し、それらに偏らせることができます。これは本番コーパスで100%の精度を達成するために必須ではありませんでしたが、非常に微妙なテストケースを見つけるのに役立ちました。最終的なイテレーションループ私のループの最終的なイテレーションはこのようなものでした:PBT、実際のコーパス、回帰テスト、そして「エッジケースについて本当に深く考える」から新しいテストの失敗を生成し、