ClickHouse の RowBinary、RowBinaryWithNames、および RowBinaryWithNamesAndTypes 形式に対応した Node.js 向けリーダー/ライターである @clickhouse/rowbinary をリリースしました。通常のライブラリと同様にインポートして汎用パーサーを呼び出すこともできます。しかし、本パッケージは Agent Skill としても提供されています。コーディングエージェントに同梱の SKILL.md を参照させると、エージェントはライブラリを呼び出す代わりにその内容を読み取り、実行するクエリの正確なカラム型に特化したパーサーを書き出します。生成されたパーサーは、ライブラリの関数をループ内で合成するよりも 1.5〜3.4 倍高速に動作し、生成コストは 1 回あたり約 0.20 ドルで、モデルが記憶を頼りにバイナリデコーダーを書く際に生じるサイレントなデータ破損バグも防げます。
開発の背景
RowBinary は、ClickHouse からデータを取り出す最も効率的な方法の 1 つであると同時に、JavaScript から処理するのが最も厄介な形式の 1 つでもあります。ワイヤフォーマット自体はシンプルです。リトルエンディアンのプリミティブ、可変長のための LEB128 可変長整数、行ごとのオーバーヘッドはありません。苦労するのは読み取り側です。リーフ型ごとに固有の読み取りパターンが存在します。Nullable、Array、Map、Tuple、LowCardinality は任意に入れ子になります。DateTime64 には精度を考慮したスケーリングと任意のタイムゾーンが必要です。Variant、Dynamic、JSON 型は自己記述的かつ再帰的であり、各値が独自の型タグを保持して同じパース処理へディスパッチされます。
そのため、多くのアプリケーションは JSON 形式へと後退してしまいます。しかし、JSON 形式は実質的な CPU 時間を消費する上、さらに悪いことに、低速な「文字列化して再パースする」経路をとらない限り、Number.MAX_SAFE_INTEGER を超える UInt64 の値が暗黙のうちに float64 へと丸められてしまいます。RowBinary を採用するチームであっても、最終的には型ディスパッチを行う汎用パーサーに行き着きます。型ごとに 1 つの関数を用意し、実行時にセルごとにディスパッチする仕組みです。これはメンテナンスしやすい一方で高速化が困難です。すべてのセルでディスパッチのコストが発生し、V8 のインライナーがメガモーフィックな呼び出し箇所の最適化を諦めてしまうためです。高い QPS において本当に必要なのは、クエリに合わせてモノモーフィズム化(単相化)されたパーサーです。つまり、正確なカラム構成に合わせて適切な読み取り処理が適切な順序でインライン化され、ディスパッチが一切発生しないパーサーです。しかし、本番に投入するすべてのクエリに対して、そのようなパーサーを手作業で書く人はいません。
本パッケージはそのギャップを埋めるものです。ライブラリは ClickHouse の全型システムに対して正確でテスト済みの読み取りプリミティブを提供します。スキルはコーディングエージェントに対して、それらのプリミティブを組み合わせ、自分では決して書かないような、クエリごとに手作業でチューニングされたパーサーを構築する方法を教えます。
仕組みと使い方
本パッケージは 2 つのレイヤーで構成されています。
第 1 のレイヤーは、型に特化した読み取りプリミティブのライブラリです。リーフ型ごとに 1 つの小さな関数があり、さらに Nullable、Array、Map、Tuple などの型代数のための合成可能なラッパーが、フルバッファ版とチャンクストリーム版の両方で用意されています。各プリミティブはモノモーフィズム化できるように(小さく、単一目的で、メガモーフィックなディスパッチを含まないように)書かれており、V8 のオプティマイザを阻害することなくクエリ特化のパーサーへとインライン化できます。このレイヤーだけでも完全に実用的なライブラリです。インポートして parseRowBinary(...) を呼び出せば、正確な結果が得られます。また、双方向のストリーミングを行うライターや、@clickhouse/datatype-parser を基盤とした動的な RowBinaryWithNamesAndTypes パイプラインもエクスポートしています。
第 2 のレイヤーが SKILL.md です。API の呼び出し方を説明するのではなく、指定されたクエリのカラム型に合わせてプリミティブを組み上げ、専用パーサーを作成するためのポリシーをコーディングエージェントに教えます。ライブラリのソース内のコメントには、各ブロックがなぜそのような実装になっているのか、どの要素なら安全に変更できるのかが説明されています。バッファの所有権、64 ビット整数における BigInt と number の選択、Date マッパーのフック、Decimal のスケール処理、Array の実体化戦略、固定長カラム向けのファストパスなどが該当します。ライブラリが参照実装、コメントが設計上の根拠、そして SKILL.md がコード生成ポリシーとなります。
インストール方法:
npm i @clickhouse/rowbinaryこのスキルはパッケージの agents.skills フィールドに登録されているため、node_modules からスキルをスキャンするエージェントであれば自動的に検出します。直接追加することも可能です:
npx skills add ClickHouse/clickhouse-js --skill clickhouse-js-node-rowbinaryエージェントが生成するもの
ベンチマークで使用している注文(orders)スキーマを例にとります:
id UInt8
uid UUID
price Decimal64(2)
status Enum8('new' = 1, 'shipped' = 2, 'done' = 3)ライブラリの公開 API を組み合わせるだけでも正確なパーサーを作成でき、通常はそのように書くのが自然です:
export const readOrderRow: Reader<OrderRow> = (s) => ({
id: readUInt8(s),
uid: formatUUID(readUUID(s)),
price: readDecimal64(2)(s), // 行ごとにクロージャを再生成
status: readInt8(s),
});しかし、これではフィールドごと個別に境界チェックが行われ、readDecimal64(2) は行ごとに新しいクロージャを生成し、formatUUID は BigInt を経由します。スキルを読み込ませると、エージェントはすべてのカラムが固定長であることに気づき、代わりに以下のようなコードを出力します:
export const readOrderRowFast: Reader<OrderRow> = (s) => {
const { buf, view } = s;
// すべてのカラムが固定長: 1 + 16 + 8 + 1 = 26 バイト。
// 行全体で境界チェックを 1 回だけ行い、以降は固定オフセットで読み取る。
const o = advance(s, 26);
const id = buf[o]!;
const uid = formatUUIDTable(buf.subarray(o + 1, o + 17)); // BigInt ではなくルックアップテーブルを使用
const price: DecimalValue = [view.getBigInt64(o + 17, true), 2];
const status = view.getInt8(o + 25);
return { id, uid, price, status };
};26 バイトの行全体に対して境界チェックは 1 回のみとなり、固定オフセットで読み取られ、Decimal のスケールは直接埋め込まれ、UUID のフォーマットにはルックアップテーブルが使用されます。出力結果は API 合成版と完全にバイト単位で一致し、速度は 3.41 倍向上します。
パーサーが最初から特化して作られるため、.map() スタイルの変換も追加コストなしで行えます。カラム名を変更したり、派生フィールドを計算したり、使わないフィールドを破棄したりする処理が、ライブラリに追加のオプションを一切増やすことなく、ゼロコストで読み取りループ内に組み込まれます。
導入する前に知っておくべき特徴がいくつかあります:
- 生成されたパーサーは通常のコードです。 人間がコミットし、他のモジュールと同様にレビュー、テスト、ベンチマークが行われます。このスキルが生成するのは、人間が読めるソースコードであり、ブラックボックスのバイナリではありません。ライブラリには包括的なテストスイートが付属しており、アプリケーション側で管理することになったリーダーをカバーするために流用できます。
- スキルは監査可能です。 npm の tarball に含まれる markdown ファイルと、詳細なコメントが付いたテスト済みの TypeScript のみで構成されています。
- 汎用リーダーもそのまま機能します。 エージェントを一切使わずに
parseRowBinary(...)を呼び出す確実な経路も利用できます。スキルは選択肢を広げるものであり、既存の機能を置き換えるものではありません。 - ライブラリ全体を読み取れ、複数ステップの参照手順に従えるモデルで最も効果を発揮します。小型モデルでは精度が落ちますが、焦点を絞ったサブエージェントを用いることで実用性を維持できます。Haiku のパス率は 52% から 86% に跳ね上がりました。
ここに至る経緯
従来のアプローチはコンパイラ
スキーマ駆動のバイナリ形式でモノモーフィズム化されたパーサーを生成する場合、コード生成コンパイラを構築するのが標準的な定石です。これは protoc、flatc、Cap'n Proto がいずれも採用してきた構成です。スキーマを受け取り、コンパイラを実行し、対象言語ごとの専用コードを出力します。これは機能しますが、コード生成コンパイラはスキーマ言語のパーサー、中間表現(IR)、対象言語ごとのバックエンド、オプションの組み合わせマトリクス、リリースサイクル、メンテナーの対応待ちなどを抱えた、本格的なソフトウェアになってしまいます。ユーザーが求めるあらゆるカスタマイズ(異なる decimal ライブラリ、カスタム Date マッパー、Int64 に対する BigInt と number の選択、Array の即時実体化か遅延実体化か、LowCardinality 用の文字列インターンテーブルなど)をフラグとして設計し、名前を付け、ドキュメント化し、バージョン間で互換性を維持し、他のあらゆるフラグとの組み合わせをテストしなければなりません。
JavaScript の RowBinary 向けに同様のコンパイラを作ることも可能でした。しかし私たちは、コンパイラをエージェントが推論時に読み取って再構成できるアーティファクト(詳細なコメント付きのプリミティブと markdown ファイル)へと分解することにしました。これにより、カスタマイズ領域は固定のフラグ一覧ではなく、ライブラリの内容を把握したモデルとの対話になります。Decimal128 をカスタムの高精度 decimal ライブラリにマッピングしたい場合や、DateTime64(9) をナノ秒単位の BigInt として扱いたい場合、あるいは LowCardinality(String) を文字列インターンテーブル経由で実体化したい場合でも、私たちが新機能としてフラグを追加する必要はありません。エージェントが decimal.ts のコメントを理解しているため、わずか数百トークンでそれらの処理を記述できます。
評価(Eval)から得られた知見
私たちは、1 スキーマあたり 5 万行のデータを用いて 3 つのデコード経路をベンチマークしました。正確な JSON 経路(幅の広い整数をサーバー側で文字列化し、クライアント側で BigInt に再パースする)、ライブラリ API を組み合わせた汎用 RowBinary リーダー、そしてエージェントが生成したモノモーフィックなパーサーです。ハードウェアとバージョンは本記事の末尾に記載しています。
大きな整数を含む財務元帳スキーマにおいて、RowBinary は Apple M4 Max 上で JSON を 3.3 倍上回り(ベンチマークのソース)、CI 上の 4 コア AMD EPYC 7763 では 2.5 倍上回りました(実行ログ)。RowBinary の処理がポインタ演算と連続メモリの読み取りであるのに対し、JSON の処理は分岐の多いトークン化と BigInt のメモリ割り当てを伴うため、より新しいハードウェアほどこの差は広がります。IoT スキーマでは、どちらのマシンでも約 2.1 倍とより安定した性能を示しました。なお、これらの数値は正確な JSON 経路と比較したものである点に注意してください。そのままの JSONEachRow は 1.8 倍と差が縮まって見えますが、すべての UInt128、Int128、および Number.MAX_SAFE_INTEGER を超える任意の UInt64 を、警告なく float64 へと丸めてしまいます。デコード自体は成功するものの数値は狂っており、サーバー側と合計値を照合するまで誰も気づきません。
そして、エージェントが生成したパーサーは、API 合成版のリーダーをさらに 1.5〜3.4 倍上回ります:
| スキーマ | 構成 | 合成版リーダーに対する高速化 |
|---|---|---|
| 財務元帳 | 大きな整数(UInt128, Int128) | 1.55 倍 |
| IoT テレメトリ | Float64 / 整数 | 2.46 倍 |
| 注文(Orders) | 固定長、非正規化 | 3.41 倍 |
テストしたすべての数値中心のスキーマで、1.5 倍以上の向上が見られました。ただし、RowBinary があらゆる場面で優れているわけではありません。文字列中心のログスキーマでは、JSONCompactEachRow が最適化された RowBinary リーダーすら上回り、スキルのドキュメント自体もそのようなケースでは RowBinary を使わないよう指示しています。スキルは自らの適用限界を把握しています。
Claude Sonnet 4.6 を使用し、数値中心の 4 つのスキーマで平均した生成コストは、入力が約 23 万トークン(エージェントループ全体でほぼすべてプロンプトキャッシュから処理され、呼び出しあたりのユニークなスキルフットプリントは約 2.8 万トークン)、出力が約 2,100 トークンで、キャッシュ有効時はパーサー 1 つあたり 0.20 ドル、未キャッシュ時の上限は 0.72 ドルでした。デプロイごとに再生成しても十分に予算内に収まります。
評価からは、速度以外の面でもスキルが重要である理由が明らかになりました。RowBinary は UUID を 2 つのリトルエンディアンの UInt64 として格納し、テキスト形式と比べてそれぞれバイト順が反転しています。ドキュメントやツール、テスト&修正のループを与えずに記憶だけでそのデコーダーを書くよう求めたところ、Sonnet 4.6 は 5 回中 3 回バイト順を誤り、そのたびに静かにデータが破損する出力を生成しました。ワイヤ上の 16 バイトをそのままの順序で 16 進化し、一見もっともらしい UUID 文字列としてフォーマットしてしまったのです。一方、Opus 4.8 は 5 回中 5 回成功し、高い信頼性を示しました。スキルを読み込ませておけば、エージェントが読み取っている箇所から関数呼び出し 2 つ分の距離に参照プリミティブがライブラリ内に存在するため、すべての生成が構造的に正しくなります。
私たちが速度の向上以上にこの点を重視している理由は、この障害モードにあります。ClickHouse のユーザーは何十億行ものデータに対してこれらのパーサーを実行します。UInt128 を float64 に丸めてしまう JSON 経路は、何百万レコードもの集計結果にわずかなズレを生じさせ、6 週間後に財務レポートの帳尻が合わないことでようやく誰かが気づくことになります。ワイヤバイトをそのままの順序で 16 進化する UUID デコーダーは、スキーマ検証を通過する文字列を生成し、JOIN 句の条件を静かに壊します。これらは、チームが AI 生成コードをデータ処理パイプラインへ投入することを拒絶する原因となるバグそのものです。スキルは、生成されるパーサーを、私たちがメンテナンスしている公式でテスト済み、コードレビュー済みのプリミティブの再構成とすることで、この問題に対処します。PR で生成されたパーサーをレビューする際、それは私たちが書いたコードから組み立てられたコードをレビューしていることになります。
スキルの作成にかかったコスト
十分に有能なコーディングエージェントなら、スキルなどなくてもこのパーサーを書けるはずだ、という指摘はもっともです。実際に書くことは可能ですし、このスキル自体もそうして作られました。RowBinary の仕様と ClickHouse のソースコードを参照として与えられた Claude Code(Opus)は、正常に動作するリーダーを生成しました。それには数百万トークン、丸 1 日近くに及ぶ継続的なプロンプティング、複数回のテスト作成とベンチマーク調整、そして各イテレーションに対する人間によるレビューが必要でした。
本スキルは、その 1 日の作業から得られた知見を凝縮した成果物です。モデルが毎回ゼロから再発見しなければならない蓄積された教訓(LEB128 の読み取り、Decimal のスケール処理、どの整数幅に BigInt が必要なのか、V8 がインライン化できるケースとできないケース)が組み込まれています。コンパイラも同様にエンジニアリングのコストを分散(償却)する仕組みです。誰かが数か月かけて型システムとコード生成ルールを教え込んだからこそ、下流のすべての利用者が特化された出力を安価に得られます。今回のケースでは、人間と AI の 1 日の協業による投資が markdown とコメント付きコードに固定化され、下流の誰もが 0.20 ドルの推論呼び出し 1 回でその恩恵を享受できるようになりました。
今後の位置づけ
Agent Skills は 2025年10月にフォーマットとして登場し、2025年12月にオープンスタンダードとなりました。これまでの最も一般的なパターンは、エージェントが API を正しく呼び出せるように、npm パッケージと一緒に SKILL.md を提供するというものでした。Vercel Labs の skills CLI、antfu 氏の skills-npm、npm-agentskills などがこれにあたります。これらは非常に有用であり、私たちもすでにその形式のスキルとして、障害発生時にエージェントが参照するランブックである clickhouse-js-node-troubleshooting を提供しています。
@clickhouse/rowbinary は異なるアプローチをとっています。「API としてのライブラリ」ではなく「リファレンスとしてのライブラリ」という形です。エージェントはライブラリを単に呼び出すだけのブラックボックスとして扱うのではなく、クエリごとにフォーク可能な透明性のある参照実装として扱います。このようなスキルを作成する場合、コードの書き方も変わってきます。呼び出されることを前提としたコードには正確な説明が必要です。読まれることを前提としたコードには、詳細なコメント、内部の一貫性、そして再現性のない技巧を避けることが求められます。
私たちはこのパターンが広く一般化していくと考えています。これまでコード生成コンパイラを検討していたような領域(特化型で、スキーマ駆動で、パフォーマンスが重視される出力)には、今や markdown ファイルと 1 回の推論呼び出しだけで済む代替手段が存在します。スキルが不適切な出力を生成する箇所を見つけた場合は、ライブラリ内のコメントを修正することで対処できます。ぜひ Issue を提出 してください。
ベンチマーク環境
ローカル: Apple M4 Max, Node v24.6.0, macOS 26.5.1, ClickHouse 26.1.1.200。CI: AMD EPYC 7763 (4 vCPU), Node v24.17.0, ClickHouse 26.6.1.1193(ベンチマークワークフロー、最新の実行結果)。ソースコードは 8c51d9a に固定。npm run bench によりスキーマあたり 5 万行で計測。



