0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ClickHouse の AI 関数(ベータ)で LLM の利用量を管理できるか試してみた

0
Posted at

この記事は、私が Claude(Claude Code)と一緒に検証した内容です。ClickHouse の system.query_log と Gemini API の使用状況画面から読み取った実測に加えて、そこからの推定(「〜と考えられます」と書いた箇所)を含みます。AI 関数はベータの機能で(今回使った 26.8 では実験的機能の扱い)、ここに書いた挙動やデフォルト値は今後変わる可能性があります。

1. はじめに

前回の記事1では、Oracle の Select AI で LLM を「誰が」「どれだけ」使ったかを、DB 側と OCI 側でどこまで追えるかを確かめました。

前回の記事を公開する前日(2026 年 9 月 11 日)に、ClickHouse の公式ブログで AI 関数がまとめて紹介されました2aiClassifyaiGenerate など、SQL の中から行ごとに LLM を呼ぶ 8種類の関数です。ブログの注意点の節には、次の一文があります。

Cost and latency scale with row count, so always test with LIMIT first

費用と待ち時間は行数に比例するので、まず LIMIT を付けて試すように、という注意です。この一文の続きには、クォータの設定を使うようにとも書かれています。ブログで紹介されているのは、1クエリあたりの入力トークン数・出力トークン数・HTTP リクエスト数の上限です。

では、複数の利用者が同じ ClickHouse から AI 関数を使うとき、DBA は「誰がどれだけ使ったか」を答えられるのでしょうか。1クエリ単位の上限はあっても、1 時間や 1 日あたりの上限はかけられるのでしょうか。Select AI と同じく DB の中から LLM を呼ぶ機能なので、前回と同じ観点で、ClickHouse の記録と Gemini 側の記録を突き合わせて確かめました。

1.1. 結論(先出し)

  • 課金の単位になるトークン数は、利用者ごとに分けられる。system.query_log に、クエリごとの実行ユーザー・quota_keylog_comment と、AI 関数の入力・出力トークン数と呼び出し回数が記録される。DBA は全員分を 1本の SQL で集計でき、一般ユーザーは query_log を読めない
  • Gemini の使用状況画面と 1 時間単位で突き合わせると、6つの時間帯の合計で、出力トークン数(2,683)と成功した呼び出し回数(156)が手元の記録と一致した。156回には、切り分けのために ClickHouse を通さず直接呼んだ 33回も含む。入力トークン数だけは Gemini 側が 1回あたり約 5.5トークン多く、この差の説明は、トークンの数え方の公式ページと英語での検索では見つけられなかった
  • DB 側の回数は 2種類あり、429(回数超過)が出た時間帯の照合で、どちらも Gemini 側の値と一致した。AIAPICalls は 429 の失敗とリトライを含む HTTP リクエスト数で、API キー別のリクエスト数と一致する。AIRowsProcessed は結果を受け取った行数で、モデル別のリクエスト数(成功だけ)と一致する
  • クエリ単位の上限は、設定プロファイルでユーザーに強制でき、ユーザーは自分で解除できない。期間単位の上限は、CREATE QUOTA で制限できる項目に AI の項目が無い(2026 年 9 月時点のドキュメントの一覧とソースの定義で確認)ので直接はかけられず、クエリ数のクォータと組み合わせて間接的に絞ることになる

1.2. 検証ゴール

# 確かめること 確認できれば OK の条件
1 課金量(トークン数)を利用者ごとに分けられるか DB ユーザー別・エンドユーザー識別子別に、DBA が query_log からトークン数を集計できる。その合計を Gemini 側の値と突き合わせて、一致するか、差の理由を説明できる
2 DB 側で数えた回数が、実際の呼び出し回数と一致するか 正常・429・リトライ・上限での打ち切り・エラーを無視して続行、の場面ごとに、DB 側の回数と Gemini 側のリクエスト数の関係を表にできる
3 使える量の上限をユーザーに強制できるか クエリ単位の上限をユーザーが自分で解除できない状態にできるかと、1 時間・1 日あたりの上限を作れるかが、手段付きで分かる

2. 検証環境

項目
ClickHouse clickhouse/clickhouse-server:26.8.2.7(Docker コンテナ 1つ。2026 年 9 月時点で 26.8 系の最新の LTS)
LLM Gemini API の無料枠、gemini-3.5-flash-lite。OpenAI 互換の API(/chat/completions)で呼ぶ
無料枠の上限(このモデル) 毎分 15リクエスト・毎分 25万トークン(入力)・1 日 500リクエスト(Google AI Studio のレート制限画面で確認。AI Studio は Gemini API の Web の管理画面)
データ Hacker News の公開データ(ClickHouse のドキュメントのサンプル)から、投稿の種類が story(記事の投稿)の行を 500行取り出した表 hn_sample(列は idtitletext
DB ユーザー alicebob(個別の DB ユーザーを持つ構成の役)、app_svc(アプリのサービスアカウント役)、default(DBA 役)。上限の検証用に p3_bobp3_appp3_alicep3 は検証の段階の番号)

ClickHouse Cloud は使っていません。関数リファレンスに、AI 関数は現時点では ClickHouse Cloud のサービスで使えないと書かれているためです3。手元の Docker で 26.8.2.7 を使いました。26.8 には公式ブログの手順と違う設定があるので、4.1 章で書きます。

LLM に Gemini API の無料枠を選んだのは、費用をかけずに試せて、OpenAI 互換の接続先があるためです4。AI 関数が対応している LLM のプロバイダー(provider パラメータに指定する提供元)は openaianthropic3、OpenAI 互換の接続先なら openai として呼べます。一方で、無料枠で送ったプロンプトと応答は Google の製品改善に使われることがあると、課金のページに書かれています5。そのため、送るデータは公開データの Hacker News のタイトルだけにしました。

Gemini API のレート制限はプロジェクト単位で6、使用状況画面もプロジェクトごとに表示されます(API キーで絞り込めるのはリクエスト数のグラフだけ)。今回のプロジェクトは他の用途と共用なので、他で使っていない gemini-3.5-flash-lite を選び、Gemini 側の値はモデル別の値で照合しました。


3. 構成

3.1. AI 関数が LLM を呼ぶ経路

AI 関数が Gemini を呼ぶ経路と、DB 側・Gemini 側の記録の場所

AI 関数は、ClickHouse のサーバーのプロセスから、行ごとに LLM の API を HTTPS で呼びます。接続先・モデル・API キーは named collection(接続情報に名前を付けてまとめておく仕組み)7に書いておき、関数の引数でその名前を指定します。

SELECT id,
       aiClassify(title, ['question','show','launch','news','other'],
                  map('credentials','gemini_text')) AS label
FROM hn_sample ORDER BY id LIMIT 5
SETTINGS log_comment = 'p1-a';

aiClassify は、テキストを指定したラベルのどれかに分類する関数です。ソースコードでは、行の値そのものをユーザーのメッセージにして、「次のカテゴリのどれか 1つに分類し、ラベルだけを答えよ」という英語のシステムプロンプトと、ラベルを列挙した JSON スキーマ(response_format)を付けて送っています8。温度(temperature)のデフォルトは 0 です。答えは 1語でも応答は {"category": "question"} のような JSON になるので、出力トークン数は 1回あたり 6 か 11 になりました(5.1 章)。

トークン数は、LLM の応答に含まれる usageprompt_tokenscompletion_tokens)を ClickHouse がそのまま読み、クエリの ProfileEvents(クエリごとのカウンタ)に足しています9。ただし 26.8.2.7 では、応答の形式を検査してから usage を読むので、課金されたのに形式の検査で失敗した応答は、トークン数が記録されません。検査の前に usage を読む修正は 2026 年 8 月に master に取り込まれていますが、26.8.2.7 には入っていません10。今回の検証では出力トークン数が Gemini 側と一致していて(5.2 章)、この修正の対象になる応答は無かったと考えられます。

3.2. 記録される場所

AI 関数の ProfileEvents には、次のものがあります311

ProfileEvents 何を数えるか
AIAPICalls LLM のプロバイダーに送った HTTP リクエスト数。リトライも 1回ずつ数える12
AIInputTokens 応答の usage にある入力トークン数の合計
AIOutputTokens 応答の usage にある出力トークン数の合計
AIRowsProcessed 結果を受け取った行数
AIRowsSkipped 上限の超過やエラーで、デフォルト値(文字列なら空文字)になった行数

記録される場所は、DB 側と Gemini 側に 1か所ずつです。

場所 何の記録か 全員分を確認できる人
system.query_log クエリごとの実行ユーザー(user)・quota_keylog_comment と、上の ProfileEvents DBA(今回は default)。一般ユーザーは読めない
AI Studio の使用状況画面 API キー別のリクエスト数、モデル別のリクエスト数と入力・出力トークン数、API エラー数 Google 側のプロジェクトにアクセスできる人(DB の権限とは別)

quota_key はクォータの単位を分けるためにクライアントが渡す値で13log_comment はクエリに付けられる任意の文字列です。どちらも query_log の列にあります。


4. 手順

4.1. 26.8 で AI 関数を使えるようにするまで

公式ブログの手順を 26.8.2.7 でそのまま実行すると、最初の呼び出しが、有効化の設定が無いというエラーで失敗しました。

Code: 344. DB::Exception: Received from localhost:9000. DB::Exception: AI functions are experimental. Set `allow_experimental_ai_functions` setting to enable it. (SUPPORT_IS_DISABLED)

この設定の名前は、ブログにも関数リファレンスにも出てきません(2 ページの本文を文字列で検索して 0件)。ClickHouse の master のソースの設定変更履歴では、次の 26.9 の変更として、この設定を廃止し、AI 関数をベータとしてデフォルトで使えるようにすると記録されています14(26.9 は 2026 年 9 月 13 日時点で未リリース)。26.8 では allow_experimental_ai_functions = 1 が必要です。同じ履歴には、リトライ回数 ai_function_max_retries のデフォルトを 26.9 で 0 から 1 にする変更もあります。ブログと設定のドキュメントに書かれている 1 はこの master の値で215、26.8.2.7 の system.settings では 0 でした。設定の説明どおりなら、リトライが 0 で ai_function_throw_on_error がデフォルトの 1 のときは、429 が 1回返った時点でクエリ全体が例外で終わります。

26.8 の AI 関数は、有効化の設定が必要な実験的機能です。ClickHouse のドキュメントでは、ベータの機能は公式サポートの対象、実験的機能は公式サポートの対象外とされています16。本記事の 26.8 での手順は検証のためのもので、本番での利用を考えるなら、AI 関数がベータになる予定の 26.9 以降(2026 年 9 月 13 日時点では未リリース)を待つことになります。なお、公式の Docker イメージでの起動は、ドキュメントに載っているインストール方法の 1つです17

次に、公式イメージの環境変数 CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1default ユーザーに SQL でユーザーや権限を管理させる設定)で起動したところ、default 自身が、設定ファイル(config.d)に書いた named collection を使えませんでした。

Code: 497. DB::Exception: Received from localhost:9000. DB::Exception: default: Not enough privileges. To execute this query, it's necessary to have the grant NAMED COLLECTION ON gemini_text: while executing function aiClassify on arguments ... (ACCESS_DENIED)

default から自分に GRANT NAMED COLLECTION を付けようとしても、WITH GRANT OPTION が無いので同じエラーになります。ユーザーの権限には、access_management とは別に、named collection を管理する named_collection_control があります7。users.d のファイルで access_managementnamed_collection_controlshow_named_collections(named collection の一覧を表示する権限)の 3項目を 1 にすると、SHOW GRANTS FOR defaultNAMED COLLECTION ON * TO default WITH GRANT OPTION が出て、SQL で作ったユーザーに named collection の権限を付けられるようになりました。

users.d/default-access.xml
<clickhouse>
  <profiles>
    <default>
      <allow_experimental_ai_functions>1</allow_experimental_ai_functions>
    </default>
  </profiles>
  <users>
    <default>
      <access_management>1</access_management>
      <named_collection_control>1</named_collection_control>
      <show_named_collections>1</show_named_collections>
    </default>
  </users>
</clickhouse>

default プロファイルに allow_experimental_ai_functions を置いたので、SQL で作ったユーザーのクエリにもこの設定が適用されます。

API キーは、設定ファイルの named collection に api_key の項目として置き、値は from_env 属性で環境変数から読むようにしました18。named collection は SQL の CREATE NAMED COLLECTION でも作れますが、その場合はキーをクエリ文に書くことになります。

config.d/ai.xml
<clickhouse>
  <named_collections>
    <gemini_text>
      <provider>openai</provider>
      <endpoint>https://generativelanguage.googleapis.com/v1beta/openai/chat/completions</endpoint>
      <model>gemini-3.5-flash-lite</model>
      <api_key from_env="CH_AI_GEMINI_KEY"/>
      <max_tokens>256</max_tokens>
    </gemini_text>
  </named_collections>
</clickhouse>
docker run -d --name ch-ai \
  -e CH_AI_GEMINI_KEY \
  -v ch-ai-v3:/var/lib/clickhouse \
  -v ./config.d/ai.xml:/etc/clickhouse-server/config.d/ai.xml:ro \
  -v ./users.d/default-access.xml:/etc/clickhouse-server/users.d/default-access.xml:ro \
  --ulimit nofile=262144:262144 \
  clickhouse/clickhouse-server:26.8.2.7

-e CH_AI_GEMINI_KEY は、値を書かずにホストの環境変数をそのままコンテナに渡す指定です。起動後、キーの書式に合わせた正規表現(AIza に続く 35文字)で探し、system.named_collections の値・query_log・サーバーログ・エラーログ・展開後の設定ファイルのどれにもキーが出ていないことを確かめました。はじめは AIza の 4文字で探したところサーバーログに 3件見つかりましたが、3件とも確認のために実行した自分のクエリでした。ClickHouse は実行したクエリ文をサーバーログにも書くので、検索語そのものが記録されます。

4.2. 呼び出し元の分け方

ユーザーは SQL で作り、hn_sample の読み取りと named collection の利用を許可しました(パスワードなし・ローカル接続だけのユーザーは検証用です)。

CREATE USER alice IDENTIFIED WITH no_password HOST LOCAL;
CREATE USER bob IDENTIFIED WITH no_password HOST LOCAL;
CREATE USER app_svc IDENTIFIED WITH no_password HOST LOCAL;
GRANT SELECT ON default.hn_sample TO alice, bob, app_svc;
GRANT NAMED COLLECTION ON gemini_text TO alice, bob, app_svc;

app_svc は、エンドユーザーの識別子を quota_key で渡します。quota_key は、clickhouse-client なら --quota_key オプションで渡せます。

docker exec ch-ai clickhouse-client --user app_svc --quota_key u01 --query "
SELECT id, aiClassify(title, ['question','show','launch','news','other'], map('credentials','gemini_text')) AS label
FROM hn_sample ORDER BY id LIMIT 5
SETTINGS log_comment = 'p1-c-u01'"

次の 5本を実行しました。log_comment は、集計で実行を見分けるための目印です(p1 は検証の段階の番号)。

log_comment DB ユーザー quota_key 分類した行数
p1-a alice なし 5
p1-b bob なし 10
p1-c-u01 app_svc u01 5
p1-d-u02 app_svc u02 10
p1-e app_svc なし 5

各実行の間は 60〜70 秒空け、5本とも日本時間の 20時台に収めました(最初の開始は 20:01、最後の終了は 20:09)。Gemini 側の値を 1 時間単位で照合するためです(4.3 章)。

4.3. Gemini 側の値の読み方

Gemini 側の値は、AI Studio の使用状況画面(Gemini API の使用状況)を期間「1 日」で表示し、1 時間ごとに読みました。次の図は、API キーを検証用の CHtestKey、モデルを Gemini 3.5 Flash Lite に絞り込んだ状態です。

AI Studio の使用状況画面(期間 1 日、API キーは CHtestKey、モデルは Gemini 3.5 Flash Lite)。上段が API リクエストの合計数と API エラーの合計数、下段がモデルごとの入力トークン数・出力トークン数・リクエスト数

図のとおり、API エラーの合計数のグラフには API キーを選ぶ欄がありません。429(7件)は、5.4 章で失敗した呼び出しの分です。503(Service Unavailable、3件)は日本時間の 20時台のもので、この時間帯の検証の呼び出し(5.1 章の 35回)はすべて成功していたので、同じプロジェクトの別の用途の分と考えられます。

グラフの値はマウスを乗せるとツールチップで読めます。今回はブラウザからページ内で JavaScript を実行し、グラフに付いている読み上げ用のデータ表を表示して数値を読みました(データ表は列が間引かれていて、列見出しを押すと省略された時間帯の列が表示されます)。

注意した点 内容
表示の間隔 期間「直近 1 時間」では 1 分ごとの点になるが、点の値が個々の呼び出しと合わなかった(ある 1 分の点がリクエスト 1・入力 26・出力 10 で、近い時刻の呼び出しは入力 46・出力 6)。照合は 1 時間単位にした
時間軸 UTC-8 で表示される。日本時間の 20時台は、画面では 3:00 の時間帯になる
列の時刻 API キー別の「API リクエストの合計数」(棒)は時間帯の終わり、モデル別のグラフ(折れ線)は時間帯の始まりの時刻が列名になる。日本時間 20時台の呼び出しは、棒では 4:00、折れ線では 3:00 の列に出る
集計の遅れ 呼び出しの終了から 4〜8 分後に読むと、12回中 7〜9回分しか出ていなかった。30 分ほど空けてから読んだ
時間の境目 境目の直後に始めた呼び出しは、トークン数の一部が前の時間帯の値に含まれることがあると考えられる(5.2 章)
生成以外の呼び出し モデル一覧を取得する GET も、API キー別のリクエストに 1回と数えられる(トークンのグラフには出ない)

5. 結果

5.1. DB 側では利用者ごとにトークン数を集計できる

DBA 役の default で、4.2 章の 5本の記録を集計しました。例外で終わったクエリの分も含めるため、type = 'QueryFinish' ではなく、開始時の行(QueryStart)だけを除いています。

SELECT event_time, user, quota_key, log_comment, type,
       ProfileEvents['AIAPICalls']      AS calls,
       ProfileEvents['AIInputTokens']   AS in_tok,
       ProfileEvents['AIOutputTokens']  AS out_tok,
       ProfileEvents['AIRowsProcessed'] AS ok_rows,
       ProfileEvents['AIRowsSkipped']   AS skipped
FROM system.query_log
WHERE event_time >= '2026-09-12 11:01:00'
  AND log_comment LIKE 'p1-%'
  AND type != 'QueryStart'
ORDER BY event_time;

結果は次のとおりです(時刻の列は UTC で、日本時間では 20:02〜20:09)。

   ┌──────────event_time─┬─user────┬─quota_key─┬─log_comment─┬─type────────┬─calls─┬─in_tok─┬─out_tok─┬─ok_rows─┬─skipped─┐
1. │ 2026-09-12 11:02:23 │ alice   │           │ p1-a        │ QueryFinish │     5 │    241 │      50 │       5 │       0 │
2. │ 2026-09-12 11:04:34 │ bob     │           │ p1-b        │ QueryFinish │    10 │    486 │     105 │      10 │       0 │
3. │ 2026-09-12 11:06:04 │ app_svc │ u01       │ p1-c-u01    │ QueryFinish │     5 │    241 │      40 │       5 │       0 │
4. │ 2026-09-12 11:07:31 │ app_svc │ u02       │ p1-d-u02    │ QueryFinish │    10 │    486 │     105 │      10 │       0 │
5. │ 2026-09-12 11:09:01 │ app_svc │           │ p1-e        │ QueryFinish │     5 │    241 │      45 │       5 │       0 │
   └─────────────────────┴─────────┴───────────┴─────────────┴─────────────┴───────┴────────┴─────────┴─────────┴─────────┘

userquota_keyGROUP BY すれば、利用者ごとの合計になります。サービスアカウントの app_svc 1つを共有していても、quota_key の値でエンドユーザーごとに分けられます。識別子を渡さなかった p1-e は、quota_key が空の行になりました。この 1 時間の合計は、呼び出し 35回・入力 1,695・出力 345 で、同じ時間帯に AI 関数を使ったほかのクエリは query_log にありませんでした。

一般ユーザーの alicequery_log を読もうとすると、次のエラーになります。

Code: 497. DB::Exception: alice: Not enough privileges. To execute this query, it's necessary to have the grant SELECT for at least one column on system.query_log. (ACCESS_DENIED)

SQL で作ったユーザーに system.query_log の権限を付けていない状態では、DBA だけが全員分を確認でき、一般ユーザーは自分の分も参照できません。

同じ 5行を分類した p1-ap1-c-u01p1-e は、入力トークン数はどれも 241 ですが、出力は 50・40・45 と違いました。分類結果も、id 344071 の行は p1-a では newsp1-c-u01p1-e では other でした。温度 0 でも、同じ 5行の分類を 3回実行すると、答えも出力トークン数も一致しませんでした。

出力トークン数が 1回あたり 6 か 11 に分かれるのは、JSON の空白の違いです。ClickHouse を通さずに直接呼んだとき(5.3 章)の応答は、{"category": "question"} なら 6、改行とインデント付きの同じ JSON なら 11 で、同じタイトルでも呼ぶたびに入れ替わりました。

5.2. Gemini 側との照合

Gemini 側の値を、1 時間ごとに手元の記録と突き合わせました。手元の記録は、ClickHouse 経由の呼び出しなら query_log、ClickHouse を通さずに直接呼んだ分(5.3 章の切り分け)なら応答の usage の合計です。Gemini 側のモデル別のリクエスト数は成功した呼び出しだけを数えるので(5.4 章)、手元の回数も成功した回数にしています。

時間帯(日本時間) 呼び出し リクエスト(Gemini 側) リクエスト(手元) 出力(Gemini 側) 出力(手元) 入力(Gemini 側) 入力(手元)
18時台 ClickHouse 2回・直接 1回(接続確認) 3 3 23 23 130 115
19時台 ClickHouse 9回(5.5 章) 9 9 89 89 471 425
20時台 ClickHouse 35回(5.1 章) 35 35 413 345 1,891 1,695
21時台 直接 12回(5.3 章) 12 12 1,194 1,259 388 351
22時台 直接 20回(5.3 章) 20 20 207 210 1,090 962
23時台 ClickHouse 77回(5.4 章) 77 77 757 757 4,113 3,681
合計 156 156 2,683 2,683 8,083 7,229

出力トークン数と成功した呼び出し回数は、6つの時間帯の合計で一致しました。時間帯ごとに見ると、Gemini 側の出力が、20時台は 68 多く、21時台は 65、22時台は 3 少なくなっています。21時台と 22時台の呼び出しは、どちらも時間の境目の直後(21:00:18 と 22:00:26)に始めていて、トークン数の一部が前の時間帯の値に含まれたと考えられます。リクエスト数は時間帯ごとにも一致しているので、ずれたのはトークン数だけです。

入力トークン数は、どの時間帯でも Gemini 側が多く、合計の差は 854 でした。成功 156回で割ると 1回あたり 5.47トークンで、時間帯ごとには 1回あたり 5.0〜6.4トークンです。20時台と 21時台は、1回あたりの差が 5.6 と 3.1 に分かれていて境目のずれを含むと考えられるので、まとめて計算しました。22時台は、22時台の呼び出しの前後で読んだ 21時台の入力がどちらも 388 で、入力はずれていなかったので単独で計算しています。

照合の相手は使用状況画面の値で、請求額ではありません(無料枠なので請求は発生していません)。また、思考トークン(モデルが回答の前の推論に使うトークン)が、OpenAI 互換の接続先の出力トークンに合算されていないかも確認しました。Gemini 本来の API(generateContent)で呼んだ 16回の応答には、思考トークン数の項目(thoughtsTokenCount)が 1件もありませんでした。このモデルとこの文面では、思考トークンは出ていません。

5.3. 入力トークンの差はどこで生じているか

入力トークンの差の原因を、ClickHouse を通さずに Gemini を直接呼んで切り分けました(5.2 章の表の 21時台と 22時台)。

疑った原因 確かめ方 結果
ClickHouse がトークン数を足し間違えている 20時台と同じ 10件のタイトルを、ClickHouse と同じ送信本文(システムプロンプトと JSON スキーマ付き)で直接呼び、応答の usage を合計する 入力 486・出力 105 で、ClickHouse の記録(p1-bp1-d-u02)と一致した
OpenAI 互換の接続先が少なく返している 同じ本文を Gemini 本来の API でも呼び、promptTokenCount と比べる システムプロンプトなしでは同じ値(3件のタイトルの入力が 7・14・9トークン)、ありでは本来の API のほうが 1 少なかった
システムプロンプトや JSON スキーマの数え方 送信本文の条件を変えて、使用状況画面の値と比べる 使用状況画面の 1 分ごとの点は個々の呼び出しと合わず(4.3 章)、1 時間単位では条件ごとに分けて比べられなかった

ClickHouse の記録は、応答の usage を 1件ずつ足した値と一致しました。使用状況画面の入力トークン数は、応答の usage の合計より 1回あたり約 5.5トークン多くなっています。その理由は、トークンの数え方の公式ページ19(システム指示が入力トークンに含まれることは書かれている)では見つけられませんでした。使用状況画面の値と、Gemini 本来の API で usage にあたる usageMetadata の違いを英語で検索しても、公式の説明は見当たりませんでした。

5.4. 回数の突き合わせ

日本時間の 23:05〜23:20 に、場面を変えて 8本を実行しました。429 を起こす場面は、無料枠の毎分 15リクエストを超える 23行にしています。

場面 設定 行数 クエリの結果 AIAPICalls AIRowsProcessed AIRowsSkipped 入力 出力
正常 デフォルト 5 成功 5 5 0 241 40
毎分の上限を超える・リトライなし ai_function_max_retries = 0 23 成功(429 は返らなかった) 23 23 0 1,107 233
毎分の上限を超える・リトライ 2 ai_function_max_retries = 2 23 429 で例外 17 14 0 665 139
エラーを無視して続行 ai_function_throw_on_error = 0、リトライなし 23 成功(4行が空文字) 23 19 4 909 184
回数の上限で打ち切り ai_function_max_api_calls_per_query = 3ai_function_throw_on_quota_exceeded = 0 5 成功(2行が空文字) 3 3 2 147 33
同じタイトルの行を 5行 デフォルト 5 成功 5 5 0 230 45
入力トークンの上限 150(1 スレッド) ai_function_max_input_tokens_per_query = 150max_threads = 1 20 成功(16行が空文字) 4 4 16 191 44
入力トークンの上限 150(スレッド数はデフォルト) ai_function_max_input_tokens_per_query = 150 20 成功(16行が空文字) 4 4 16 191 39
合計 84 77 38 3,681 757

この 1 時間の Gemini 側の値と並べると、次のようになりました。

項目(23時台) Gemini 側 DB 側
API リクエストの合計数(API キー別) 84 AIAPICalls の合計 84
API エラーの合計数(429) 7 リトライ 2 の失敗 3 + エラー無視の失敗 4
リクエスト数(モデル別) 77 AIRowsProcessed の合計 77
出力トークン数(モデル別) 757 AIOutputTokens の合計 757
入力トークン数(モデル別) 4,113 AIInputTokens の合計 3,681

AIAPICalls は 429 で失敗した試行も含む HTTP リクエスト数で、Gemini 側の API キー別のリクエスト数と一致します。AIRowsProcessed は成功した回数で、モデル別のリクエスト数と一致します。トークン数は、成功した応答の usage だけが足されています。

リトライ 2 の実行は、呼び出し 17回に対して結果を受け取った行が 14行でした。失敗した 3回の試行も AIAPICalls に 1回ずつ数えられていて、ソースのコメント(リトライを含めて数える)12のとおりです。1つの行で 429 が 3回続いてリトライを使い切り、クエリは次の例外で終わりました。

Code: 86. DB::Exception: Received from localhost:9000. DB::AIProviderHTTPException. DB::AIProviderHTTPException: AI provider error: HTTP 429: [{   "error": {     "code": 429,     "message": "You exceeded your current quota, please check your plan and billing details. ...

例外で終わったクエリの行(typeExceptionWhileProcessing)にも、それまでの呼び出し回数とトークン数が記録されていました。利用者には結果が 1行も返っていませんが、ClickHouse は 14行分の応答を受け取っていて、その分のトークンは使われています。

リトライなしで 23行を 21 秒で実行した場面では、毎分 15リクエストを超えていても 429 は返りませんでした。上限は厳密に毎分 15回で打ち切られるわけではないと考えられます。

エラーを無視して続行する設定では、23行すべてが返り、クエリは成功扱い(QueryFinish)でした。そのうち 4行(id 344173・344174・344198・344205)は、分類結果が空文字です。結果の表からは「分類できなかった行」と「空文字」を区別できず、query_logAIRowsSkipped(4)でしか分かりません。

同じタイトルを 5行並べた場面は、5回呼んで入力 230(1回あたり 46)でした。同じ文でも結果を再利用せず、行数と同じ回数だけ呼んでいます。

5.5. 利用量を制限する手段

クエリ単位の上限は、設定プロファイルで固定できます。CONST は変更不可、MAX は上限値の指定です20

CREATE SETTINGS PROFILE ai_limited
  SETTINGS ai_function_max_api_calls_per_query = 5 CONST,
           ai_function_max_input_tokens_per_query = 2000 MAX 2000
  TO p3_bob;

上限の設定は 0 にすると無効になる(上限なし)ので、0 への変更を拒否できるかを p3_bob で確かめました。

p3_bob の操作 結果
SET ai_function_max_api_calls_per_query = 0 Code: 452 ... Setting ai_function_max_api_calls_per_query should not be changed.
クエリ末尾の SETTINGS ai_function_max_api_calls_per_query = 0 同じ Code 452
SET ai_function_max_input_tokens_per_query = 1500 成功(上限 2000 以下)
SET ai_function_max_input_tokens_per_query = 3000 Code: 452 ... Setting ai_function_max_input_tokens_per_query shouldn't be greater than 2000.
10行を分類する 5回呼んだところで Code: 290 ... AI API call limit reached: 5 calls made, maximum: 5.

打ち切られたクエリの query_log の行には、呼び出し 5回・入力 241・出力 55 が記録されていました。

トークン数の上限は、超えた時点で止まるわけではありません。入力トークンの上限を 150 にして 20行を分類すると、4回呼んで 191 を使い、残りの 16行は空文字になりました(5.4 章の表の最後の 2行)。呼んだ 4回の入力は 1回あたり 44〜53 で、超過の 41 は 1回分より少ない値です。設定のドキュメントには、応答が返るまでトークン数は分からないので、処理中の呼び出し 1回分まで超えることがあると書かれています15。スレッド数を 1 にしてもデフォルトにしても、結果は同じ 191 でした。

期間単位の上限に使うクォータ(CREATE QUOTA)には、AI の項目がありません(ドキュメントの項目の一覧とソースの定義で確認。下の表)。AI の項目を指定すると、構文エラーになります。

CREATE QUOTA q_bad FOR INTERVAL 1 hour MAX ai_input_tokens = 1000 TO p3_bob;
Code: 62. DB::Exception: Syntax error: failed at position 60 (=): = 1000 TO p3_bob;. Expected end of query. (SYNTAX_ERROR)

期間単位の上限をかける手段を、次の場所で探しました。

探した先 結果
クォータで制限できる項目(ドキュメント13とソースの定義21 querieserrorsresult_rowsread_rowsexecution_time など 12項目で、AI 関数のトークン数や呼び出し回数は無い(2026 年 9 月、26.8.2.7 と master ブランチの両方のソースで確認)
ai_function_* の設定15 上限の設定はトークン数と呼び出し回数のどちらも 1クエリあたり
設定の制約(CONSTMINMAX20 1クエリで使う設定値を固定・制限する仕組みで、期間の累計は持たない
Gemini 側のレート制限6 プロジェクト単位(API キー単位ではない)の毎分・毎日の上限

そこで、クエリ数のクォータで間接的に絞れるかを試しました。quota_key ごとに、1 時間 3本までにしています。

CREATE QUOTA q_ai KEYED BY client_key FOR INTERVAL 1 hour MAX queries = 3 TO p3_app;

p3_app から quota_keyu01 にして 1行の分類を 60 秒おきに実行すると、3本目までは成功し、4本目で止まりました(エラー文の時刻は UTC で、日本時間では 20:00)。

Code: 201. DB::Exception: Quota for user `p3_app` for 3600s has been exceeded: queries = 4/3. Interval will end at 2026-09-12 11:00:00. Name of quota template: `q_ai`. (QUOTA_EXCEEDED)

止まったクエリの query_log の行は ExceptionBeforeStart で、AIAPICalls は 0 でした。LLM を呼ぶ前に止まっています。続けて quota_keyu02 にすると、同じ 1 時間の中でも成功しました。

「使える/使えない」の切り替えは、named collection の権限でできます。REVOKE NAMED COLLECTION ON gemini_text FROM p3_alice; のあとに p3_aliceaiClassify を実行すると、Code 497 で失敗し、この行も ExceptionBeforeStartAIAPICalls 0 でした。


6. 考察

6.1. 利用者ごとの内訳は query_log から集計する

ClickHouse の AI 関数は、プロバイダーの応答にある usage を、クエリの実行ユーザーと同じ query_log の行に記録します(3.1 章・5.1 章)。一方、Gemini の使用状況画面には API キー別のリクエスト数はありますが、トークン数はモデル別の表示だけで(3.2 章)、どの DB ユーザーの呼び出しかは画面から分かりません。1つの ClickHouse から 1つの API キーで呼ぶ構成では、利用者ごとの内訳は query_log から集計することになります。

6.2. トークン数を利用者ごとの費用の配分に使うときの注意

query_log のトークン数は、応答の usage を 1件ずつ足した値と一致しています(5.3 章)。ただし、使用状況画面の入力トークン数より 1回あたり約 5.5トークン少なく、今回の短い分類(1回あたり入力 48 前後)では合計の約 1割にあたります。上乗せが 1回あたりほぼ一定なら、利用者の間の比率で配分する用途への影響は小さく、プロバイダーの集計に合わせるなら、成功した回数(AIRowsProcessed)に 5.5 前後を掛けて足すことになると考えられます。

集計の範囲にも注意が必要です。例外で終わったクエリにも、それまでの呼び出し回数とトークン数が記録されます(5.4 章・5.5 章)。WHERE type = 'QueryFinish' で絞ると、結果が返らなかったのに課金されうる分が集計から抜けます。

また、同じ 5行の分類を 3回実行すると、トークン数も分類結果も一致しませんでした(5.1 章)。query_log の値は「その実行の記録」で、同じ処理の費用の見積もりにはなりません。ブログにも、同じ行でも答えが変わるので、温度を 0 にして結果を保存するよう書かれています2

エラーを無視する設定にした場合は、空文字の行が成功扱いで結果に並びます(5.4 章)。結果を保存する運用では、空文字の行を確認する手順が必要です。

6.3. 期間単位の上限の作り方(クエリ数と 1クエリの上限の組み合わせ)

期間単位で AI 関数の量を直接制限する設定は、クォータにも ai_function_* にもありませんでした(クォータは項目の一覧、ai_function_* は 12個の設定をすべて確認。5.5 章)。今回試した手段を組み合わせるなら、1クエリの呼び出し回数を設定プロファイルの CONST で固定し、クエリ数をクォータで制限します。このとき、1 時間あたりの呼び出し回数の上限は「クエリ数 × 1クエリの上限」になります。たとえば 1 時間 3本・1クエリ 5回なら、1 時間 15回までです。2つの設定はそれぞれ試しましたが、同じユーザーに両方をかけた組み合わせは試していません。

ただし、クエリ数のクォータは AI 関数を使わないクエリも数えるので、通常の SELECT も同じ本数の中に入ります。AI 関数を使う処理だけを別の DB ユーザーに分けると、クォータで数えるクエリを AI 関数の処理だけにできると考えられます。

なお、ai_function_* の上限は、分散クエリではシャードごとにかかると設定のドキュメントに書かれています15。今回はサーバー 1台なので、1クエリの上限がそのまま全体の上限でした。

6.4. quota_key と log_comment はクライアントが決める値

quota_keylog_comment も、クライアントが接続やクエリに付ける値です。利用者ごとの集計が正しいかどうかは、アプリがエンドユーザーごとに正しい値を渡しているかで決まります。

クォータを quota_key 単位(KEYED BY client_key)にした場合は、キーを変えれば上限を超えて実行できます。5.5 章でも、u01 が止まったあと u02 では同じ 1 時間の中で成功しました。利用者が直接 ClickHouse に接続する構成で上限を確実にかけるなら、利用者ごとに DB ユーザーを分けて、クォータをユーザー単位にする必要があります。コネクションプールでキーを付け替える場合に、クォータがどう数えられるかは確かめていません。


7. まとめ

ClickHouse の AI 関数では、プロバイダーの応答にある usage がクエリごとに query_log に記録されるので、DBA は DB ユーザー別・quota_key 別のトークン数と回数を 1本の SQL で集計できました。Gemini の使用状況画面と照合すると、出力トークン数と成功した回数は一致し、入力トークン数は Gemini 側が 1回あたり約 5.5トークン多い結果でした。回数は、失敗を含む AIAPICalls と成功だけの AIRowsProcessed が、Gemini 側の API キー別とモデル別のリクエスト数にそれぞれ一致しました。

上限は、1クエリ単位なら設定プロファイルでユーザーに強制できます。1 時間や 1 日あたりの上限は、2026 年 9 月時点のクォータに AI の項目が無いため、クエリ数のクォータと組み合わせて間接的に作ることになります。26.8 で試す場合は、有効化の設定 allow_experimental_ai_functions が必要なことと、リトライのデフォルトが 0 であること(ブログとドキュメントに書かれている 1 は、26.9 に向けた master の値)に注意が必要です。

参考


ClickHouse、ClickHouse のロゴ、および関連するマークは、ClickHouse, Inc. またはその関連会社の商標または登録商標です(https://clickhouse.com)。

  1. Select AI の LLM 利用は誰が・どれだけまで追えるか(前回の記事)

  2. AI Functions in ClickHouse(ClickHouse 公式ブログ、2026-09-11。8種類の関数と追加されたバージョン、クォータの設定、利用前の注意点) 2 3

  3. AI functions(関数リファレンス。ベータであること、現時点では ClickHouse Cloud で使えないこと、対応するプロバイダー、ProfileEvents) 2 3

  4. OpenAI compatibility(Gemini API の OpenAI 互換の接続先。ベータとの記載)

  5. Billing(Gemini API の課金。無料枠のプロンプトと応答は製品改善に使われることがある)

  6. Rate limits(Gemini API のレート制限。API キー単位ではなくプロジェクト単位) 2

  7. Named collections(named collection の定義方法と、管理に named_collection_control が必要なこと) 2

  8. aiClassify.cpp(ClickHouse のソース。システムプロンプトと JSON スキーマの組み立て、温度のデフォルト 0)

  9. OpenAIProvider.cpp(ClickHouse の master のソース。応答の usage からトークン数を読む処理)

  10. Fix AI functions under-reporting provider usage(ClickHouse の PR #114905。応答の検査で失敗した場合も usage を記録する修正。2026-08-25 に master へ取り込まれ、26.8.2.7 には入っていない)

  11. ProfileEvents.cpp(ClickHouse のソース。AIAPICalls などのカウンタの定義)

  12. AIQuotaTracker.h(ClickHouse のソース。呼び出し回数はリトライを含めて数えるとのコメント) 2

  13. Quotas(クォータで制限できる項目の一覧と、quota_key によるキー別のクォータ) 2

  14. SettingsChangesHistory.cpp(ClickHouse の master のソース。26.9 の変更として、ai_function_max_retries のデフォルトを 0 から 1 にすることと、allow_experimental_ai_functions の廃止が記録されている。26.9 は 2026 年 9 月 13 日時点で未リリース)

  15. ai_function_* session settings(12個の設定とデフォルト値。トークン数の上限は処理中の呼び出し 1回分まで超えることがある、分散クエリではシャードごと。デフォルト値は 26.9 に向けた master の値) 2 3 4

  16. Beta and experimental features(ベータの機能は公式サポートの対象、実験的機能は対象外。ai_function の設定はベータの一覧に載っている)

  17. Install ClickHouse using Docker(公式の Docker イメージでのインストール手順)

  18. Configuration files(設定ファイルの値を環境変数から読む from_env 属性)

  19. Understand and count tokens(Gemini API のトークンの数え方。システム指示は入力トークンに含まれる)

  20. Constraints on settings(設定の最小値・最大値・変更不可の制約。違反すると Code 452) 2

  21. QuotaDefs.cpp(ClickHouse のソース。クォータの項目の定義)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?