はじめに
Snowflake で「この問い合わせは解約の相談か」「この投稿は自社製品の話か」といった Yes/No 判定を SQL の中で済ませたいとき、まず候補になるのは Cortex の AI 関数 AI_FILTER です。ただ、日本語で除外条件まで細かく書いた質問では、判定の質やコストは十分でしょうか。
本記事では、Yes/No 判定に特化した新しいモデル Jev(TypeSafe 社)を Snowflake から呼び出し、私の X(旧 Twitter)の投稿 1,774 件で AI_FILTER と比べました。
判定させたのは、次の質問です(要約。全文は「題材と質問」を参照)。
質問: この投稿の主題は、DBMS(Oracle Database、PostgreSQL、Snowflake、BigQuery などのデータベース製品・サービス)そのものですか。
除外条件: BI ツール・ETL ツール・データカタログなどの周辺ツールの話や、特定の製品に紐づかない SQL 論・データモデリング論だけの投稿は「いいえ」とする。
私の投稿はデータベースやデータ基盤の話題が多く、DBMS の話と周辺の話が入り混じっているので、除外条件をどこまで守れるかが判定の難しいところです。
わかったことは次の 3 点です。
- コスト: Jev は AI_FILTER の約 1/34(0.055 ドル対 1.87 ドル)
-
判定の質:
- 判定は大筋では一致している(AUC 0.97)
- ただし「いいえ」の判定では、Jev のほうが精度が良い
- Jev が「いいえ」(スコア 0.2 未満)、AI_FILTER が「はい」と分かれた 70 件を読むと、Jev が正しかったのは 54 件、AI_FILTER が正しかったのは 5 件だった
- 残りの 11 件は判断が分かれる
- 処理時間: Jev は 23 秒で AI_FILTER の約 3 倍。ただしこれは、API のレート制限による頭打ち
想定読者: Snowflake 上で、LLM を使ったテキストの分類や判定を検討している方。
結果だけ見たい方は「結果のまとめ」へ、Snowflake から Jev を呼び出す実装手順は「Appendix C」へどうぞ。
当初は Snowflake Decision(
AI_COMPLETEでsnowflake-decisionモデルを使う方法)も比較対象に入れる予定でした。しかしプライベートプレビューのため、私のアカウントでは使えませんでした。今回は比較から外しています。
結果のまとめ
| 観点 | Jev | AI_FILTER |
|---|---|---|
| 判定の質 | AI_FILTER と大筋で一致(AUC 0.97)。「いいえ」の判定の精度が良く、除外条件をよく反映した。短い投稿ではスコアが低めに出る | DB・SQL 関連の言葉に引っ張られて「はい」に傾きがち。TRUE とした 324 件のうち、少なくとも 54 件は「いいえ」が妥当 |
| コスト(1,774 件) | 約 0.055 ドル(API 0.049 ドル + ウェアハウス 0.006 ドル) | 約 1.87 ドル |
| 処理時間(1,774 件) | 23.0 秒(レート制限 80 リクエスト/秒による頭打ち。応答は中央値 53 ms) | 7.1 秒 |
- Jev が「いいえ」(スコア 0.2 未満)、AI_FILTER が「はい」と判定が分かれた 70 件を読むと、Jev が正しかったのは 54 件、AI_FILTER が正しかったのは 5 件でした(残り 11 件は判断が分かれる)。
- 「DBMS ベンダーの周辺ツールは DBMS 製品に入るか」のように、質問の定義があいまいで判定が割れた投稿もありました。判定基準を前もって細かく決めておくことが大切です。
検証の概要
評価した手法と比較対象
| 手法 | 位置づけ | 呼び出し方 | 戻り値 |
|---|---|---|---|
| Jev(Noul) | 評価の対象 | 外部アクセス統合を使う Python UDF から TypeSafe API を呼ぶ | 0〜1 のスコア(1 に近いほど「はい」) |
| AI_FILTER | 比較の基準 | SQL から Cortex AI 関数を直接呼ぶ | TRUE / FALSE |
題材と質問
データ: X のアーカイブ(28,642 件)から、2025 年に投稿したリポスト以外の 1,774 件を取り出しました。私の投稿はデータベースやデータ基盤の話題が多いものの、日常の話もかなり混ざっています。
質問: 次の文面を、2 つの手法に同じように渡しました。
この投稿の主題は、データを格納・管理・問い合わせする「データベース管理システム(DBMS)」の製品・エンジン・サービスそのもの(例: Oracle Database, PostgreSQL, MySQL, SQL Server, DB2, MongoDB, Redis, SQLite, DuckDB, Snowflake, BigQuery, Redshift, Amazon RDS/Aurora, TiDB, Spanner などのRDBMS/NoSQL/クラウドDWH/データプラットフォーム)ですか。
BIツール(Tableau,Power BI,Lightdash,Looker)、ETL/データ統合ツール(Informatica,Fivetran,dbt,Stitch)、データカタログ(OpenMetadata)、セマンティックレイヤー(Cube.js)、あるいは特定製品に紐づかない一般的なSQL論・データモデリング論だけの投稿は「いいえ」とする。
-
Jev: 前半の主質問を
instructionsに入れました。criteria.trueには「DBMS の製品・エンジン・サービスそのものが主題の投稿」、criteria.falseには後半の除外条件を入れています。 -
AI_FILTER: 上の全文を
PROMPT()で投稿本文と組み合わせて渡しました。
評価の方針
- 正解ラベルは付けていません。 そのため、Jev の精度(正解率)そのものは測っていません。代わりに AI_FILTER の判定を基準にして、Jev がどこまで一致し、どこで食い違うかを見ました。食い違った投稿は私が実際に読んで、どちらの判定が妥当かを確かめています。
- Jev のスコアは二値化していません。 閾値を決めずに評価できるよう、AI_FILTER の判定を基準にした AUC を使いました。
- 実行環境: ウェアハウスは X-Small(Gen2)です。各手法とも、全件を 1 回ずつ実行しました。
結果 1:判定の質
Jev のスコアは AI_FILTER の判定とどこまで揃うか
- Jev: スコアの中央値は 0.02 で、全体の 77% が 0.1 未満でした。
- AI_FILTER: TRUE が 324 件(18.3%)、FALSE が 1,450 件でした。
AI_FILTER の判定ごとに、Jev のスコアを見てみます。
| AI_FILTER の判定 | 件数 | Jev 平均 | Jev 中央値 | 第 1 四分位 | 第 3 四分位 |
|---|---|---|---|---|---|
| TRUE | 324 | 0.543 | 0.59 | 0.238 | 0.85 |
| FALSE | 1,450 | 0.039 | 0.01 | 0.01 | 0.03 |
AI_FILTER の TRUE を陽性として Jev のスコアの AUC を計算すると、0.9685 でした。AI_FILTER が TRUE とした投稿と FALSE とした投稿を 1 件ずつ組にすると、96.5% の組で、Jev は TRUE 側に高いスコアを付けていたことになります。Jev の判定は、AI_FILTER と大筋でよく一致しています。
スコアの分布
Jev のスコア帯(0.1 刻み)ごとに、AI_FILTER の判定がどう分かれているかを見たのが次の 2 つの図です。
| Jev のスコア | 0.0〜 | 0.1〜 | 0.2〜 | 0.3〜 | 0.4〜 | 0.5〜 | 0.6〜 | 0.7〜 | 0.8〜 | 0.9〜1.0 |
|---|---|---|---|---|---|---|---|---|---|---|
| AI_FILTER=TRUE | 37 | 33 | 21 | 31 | 19 | 24 | 26 | 28 | 47 | 58 |
| AI_FILTER=FALSE | 1,336 | 68 | 16 | 13 | 3 | 4 | 4 | 4 | 2 | 0 |
- Jev が高スコアを付けた投稿: 0.9 以上の 58 件は、すべて AI_FILTER も TRUE でした。
- AI_FILTER が FALSE の投稿: Jev もほぼすべてを低スコアにしています。0.5 以上だったのは 14 件(1.0%)だけでした。
- AI_FILTER が TRUE の投稿: Jev のスコアは低い側から高い側まで広く散らばっています。**0.2 未満が 70 件(21.6%)**ありました。
つまり、食い違いの大半は「Jev は低スコア、AI_FILTER は TRUE」という向きです。どちらの判定が妥当なのかを、投稿を読んで確かめました。
判定が食い違った投稿を読んでみる
食い違いが大きかった次の 3 つのグループについて、全件を読んで振り分けました。振り分けは私の判断で、正解ラベルではありません。
- Jev が 0.1 未満、AI_FILTER が TRUE(37 件)
- Jev が 0.1 以上 0.2 未満、AI_FILTER が TRUE(33 件)
- Jev が 0.5 以上、AI_FILTER が FALSE(14 件)
Jev < 0.1 なのに AI_FILTER=TRUE(37 件):Jev の判定がほぼすべて妥当
| 私の判断 | 件数 | 例 |
|---|---|---|
| いいえ(データモデリング・SQL の一般論) | 16 | CRUD 図、テーブル名は単数形か複数形か、論理削除、UUID の主キー、インデックスの数、データモデリングの本 |
| いいえ(dbt・BI・ETL・データエンジニアリング) | 10 | Lightdash の設定、dbt の incremental、#dbt_tokyo の感想、Apache NiFi |
| いいえ(DBMS とは関係のない話) | 4 | 生活アプリのシステムエラー、Tavily の文字化け、「Claude Code が動いてるとこ初めて見た」 |
| いいえ(ベンダーの名前は出るが、主題は別) | 4 | Oracle Cloud アカウントの MFA、Oracle AI World への名称変更 |
| 判断が分かれる | 3 | GoldenGate と Qlik Replicate の比較、Snowflake の ai_agg 関数 |
| はい(Jev の取りこぼし) | 0 | — |
実際の投稿をいくつか挙げます。
- 「QOLismというアプリをインストールしユーザー登録まではいけたけど、その後はシステムエラー頻発でまともに使えず。」(Jev 0.01、AI_FILTER TRUE)
- 「よくBIで定義する論理層ではファクトテーブルの外部キーは表示しないように設定するけど、Lightdashだとymlファイルに書いたうえでhiddenにしないといけないのね。」(Jev 0.03、AI_FILTER TRUE)
- 「たまにDBのテーブル名を単数形/複数形のどちらにするかが議論になり、私は複数形。」(Jev 0.04、AI_FILTER TRUE)
37 件のうち 34 件は「いいえ」が妥当で、Jev が取りこぼした「はい」の投稿は 1 件もありませんでした。Jev は、質問文ではっきり「いいえ」とした BI ツールやデータモデリング論に、きちんと低いスコアを付けています。AI_FILTER のほうは、これらを「はい」と判定してしまっていました。
0.1 ≦ Jev < 0.2 なのに AI_FILTER=TRUE(33 件):過半数は Jev の判定が妥当
| 私の判断 | 件数 | 例 |
|---|---|---|
| いいえ | 20 | TRUNCATE は DML か DDL か、外部キーとインデックス、Text-to-SQL の一般論、Unity Catalog、Snowflake Openflow、dbt のメトリクス |
| はい寄り(Jev の取りこぼし) | 5 | Snowflake の外部ボリューム、Oracle の json_id、account_usage ビュー、Exadata、create table ... like 構文 |
| 判断が分かれる | 8 | GoldenGate、SQL Developer、SQLcl、ADB Data Studio、to_date、Tableau から Snowflake を使う話 |
- 全体の傾向: 0.1 未満のグループに比べると DBMS 寄りの投稿が増えますが、それでも過半数は「いいえ」で、Jev の判定が妥当でした。
- Jev の取りこぼし(5 件): どれも製品名や構文が一言触れられるだけの短い投稿でした。Jev は、短い投稿からは主題が DBMS 製品だと判断しにくいようです。たとえば外部ボリュームの投稿は、本文に Snowflake という単語がなく、前後の投稿とつなげて読まないと Snowflake の話だとわかりません。
Jev ≧ 0.5 なのに AI_FILTER=FALSE(14 件):Jev が拾えたものと取りすぎが混在
| 私の判断 | 件数 | 例 |
|---|---|---|
| はい寄り(Jev が拾い、AI_FILTER が取りこぼした) | 5 | 「昔の『Oracleあるある』」、23ai の SQL ドメイン、MS Access、「世界一周Data Guard」 |
| 判断が分かれる(DBMS の分類や業界の話) | 4 | NewSQL の講義や動向、「DBMSの機能改善に資する研究」 |
| いいえ寄り(Jev の取りすぎ) | 3 | Snowflake Intelligence の話、「DBはMySQL/PostgreSQLどっちが主流?」 |
| 文脈がなく判断できない | 2 | 「幻の20c ww」 |
- Jev が拾えた投稿: Oracle や Data Guard のように、製品名と機能がはっきり書かれている投稿に Jev は高いスコアを付けました。AI_FILTER はこれらを取りこぼしています。
- Jev の取りすぎ: 製品名が出てくるだけで主題は別の投稿に、Jev が高いスコアを付けた例も 3 件ありました。
- 件数の規模: このグループは 14 件しかなく、上の 2 つのグループ(合わせて 70 件)に比べれば小さな問題です。
まとめると
Jev が 0.2 未満、AI_FILTER が TRUE だった 70 件を、私の判断で分けると次のようになります。
結果 2:コスト
ドルへの換算には、次の単価を使いました。
- Jev: API 料金は、入力トークンが 0.042 ドル/百万トークン、出力トークンは無料です(TypeSafe 公式ブログの「Frontiers, Old and New」の表)。
- Snowflake のウェアハウス: 今回のアカウントは AWS US West(Oregon)の Standard エディションで、1 クレジットは 2.00 ドル です。
- AI_FILTER: 料金は 1.62 AI クレジット/百万トークン(入力・出力の両方が課金対象)で、1 AI クレジットは 2.00 ドル です(Snowflake Service Consumption Table)。100 万トークンあたり 3.24 ドルになります。
| 内訳 | Jev | AI_FILTER |
|---|---|---|
| 外部の API 料金 | 入力 1,158,233 トークン → 0.0486 ドル | — |
| AI クレジット | — | 0.932514 AI クレジット → 1.865 ドル |
| ウェアハウスのクレジット | 0.003074 クレジット → 0.0061 ドル | 0.000181 クレジット → 0.0004 ドル |
| 合計 | 0.055 ドル | 1.865 ドル |
| 1 件あたり | 約 0.000031 ドル | 約 0.00105 ドル |
-
Jev のコストは AI_FILTER の約 1/34: 同じ 1,774 件の判定にかかったコストは、Jev が 0.055 ドル、AI_FILTER が 1.865 ドルでした。
-
トークン単価の差はもっと大きい:
- Jev は入力だけに課金され、100 万トークンあたり 0.042 ドルです。AI_FILTER は入力と出力の両方に課金され、100 万トークンあたり 3.24 ドル(1.62 AI クレジット)なので、Jev はトークン単価で約 1/77 です。
- ただし Jev は、課金対象の入力トークンが 1 件あたり約 653 トークンと、AI_FILTER の約 2 倍あります。
criteriaを含む質問の構造を、毎回リクエストに入れて送っているためです。その分だけ差が縮んで、件数あたりでは約 1/34 になっています。 - 実測の AI_FILTER のトークン単価(0.932514 AI クレジット ÷ 575,626 トークン)は、公表単価の 1.62 AI クレジット/百万トークンと一致しました。
-
Jev のコストの内訳:
- API 料金は 1 件あたり約 0.000027 ドルです。
- ウェアハウスの分は 0.0061 ドルと小さいものの、合計の約 1 割を占めます。UDF が API の応答を待っている間もウェアハウスが動いているためです。
- AI_FILTER の場合、ウェアハウスの分は 0.0004 ドルで、合計のほぼ 100% が AI クレジットです。
-
件数を増やしたときの目安: 今回の 1 件あたりの値を、そのまま 100 万件に当てはめました。
手法 100 万件あたり Jev API 約 27 ドル + ウェアハウス約 1.7 クレジット(約 3.5 ドル)→ 約 31 ドル AI_FILTER 約 526 AI クレジット → 約 1,050 ドル ただし Jev は、レート制限のため 100 万件の処理に最短でも約 3.5 時間かかります(結果 3 を参照)。
料金はいずれも執筆時点の公開価格です。契約によって単価が異なる場合は、読み替えてください。外部アクセスで発生するデータ転送の料金は、リクエストが小さいため計算に含めていません。
結果 3:処理時間
| 項目 | Jev | AI_FILTER |
|---|---|---|
| 処理時間(1,774 件) | 23.0 秒 | 7.1 秒 |
| 1 リクエストあたりの応答時間 | 平均 62.5 ms(中央値 53 ms、最大 560 ms) | — |
| トークン数 | 入力 1,158,233 / 出力 39,028 | 575,626 |
| モデル | jev-1.13.0 | 非公開 |
| エラー | 0 件(レート制限によるリトライもなし) | 0 件 |
処理時間だけを見ると、Jev は AI_FILTER の約 3 倍かかっています。ただし、これは Jev のモデルが遅いからではありません。次に見るように、API のレート制限でほぼ決まった時間です。
レート制限から見た処理時間
TypeSafe の API には、次のレート制限があります(TypeSafe コンソールのレート制限のページに記載。閲覧にはログインが必要です。値は執筆時点で私のアカウントに表示されていたものです)。ドキュメントによると、レート制限は組織ごとに設定されていて、上限を超えたリクエストには 429 Too Many Requests が返ります。custom プランや enterprise プランでは、より高い上限も使えます。
- 80 リクエスト/秒
- 入力 100,000 トークン/秒
今回の量をこの制限に当てはめると、処理にかかる最短時間は次のようになります。
| 制限 | 今回の量 | 制限から決まる最短時間 | 実測の処理速度 |
|---|---|---|---|
| 80 リクエスト/秒 | 1,774 リクエスト | 22.2 秒 | 約 80 リクエスト/秒(上限ちょうど) |
| 入力 100,000 トークン/秒 | 入力 1,158,233 トークン | 11.6 秒 | 約 52,000 トークン/秒(上限の約半分) |
- 実測はリクエスト数の上限そのもの: Jev のクエリの実行時間は 22.16 秒でした。1,774 件をこの時間で割ると 約 80 リクエスト/秒 で、レート制限から決まる最短時間で処理できていたことになります。429(リクエスト過多)のエラーや再試行は 1 件もありませんでした。
- トークン数の上限には余裕があった: 1 リクエストあたりの入力は平均 653 トークンでした。トークン数の上限にかかるのは、平均が 1,250 トークン(100,000 ÷ 80)を超えたときです。今回は投稿が短いので(平均 92 文字)、リクエスト数の上限のほうが先に効きました。
Snowflake 側の並列度の計算と、処理速度が上限にぴったり張り付いた理由の検討は、「Appendix F」にまとめました。
応答時間:公式値との比較
TypeSafe の公式ブログでは、応答時間は「End-to-end response time is 70ms-500ms」(Introducing System One Models and Jev)とされています。今回 UDF の中で計った、1 リクエストあたりの応答時間と比べました。
| 指標 | 実測(1,774 リクエスト) |
|---|---|
| 中央値 | 53 ms |
| 99 パーセンタイル | 183 ms |
| 最大 | 560 ms |
| 70 ms 未満(公式値より速い) | 1,333 件(75.1%) |
| 500 ms 超(公式値より遅い) | 1 件(0.1%) |
4 分の 3 のリクエストが、公式値の下限 70 ms より速く返ってきました。ただし今回の値は Snowflake(AWS us-west-2)の UDF から計ったもので、公式値とは測り方が違う可能性があります。目安として見てください。分布の詳細と、速かった理由の推測も Appendix F に載せています。
考察
1. 判定は大筋で一致するが、「いいえ」の判定では Jev のほうが精度が良い
AUC は 0.97 で、Jev のスコアは AI_FILTER の判定とよく揃っていました。DBMS 製品がはっきり主題になっている投稿(Jev が 0.9 以上の 58 件は、すべて AI_FILTER も TRUE)と、明らかに関係のない投稿については、両者の判断はほぼ同じです。
食い違いの大半は「Jev は低スコア、AI_FILTER は TRUE」という向きでした。判定が分かれた投稿を読んだ結果を、どちらの判定が正しかったかで数えると次のようになります。
| 判定の分かれ方 | 件数 | Jev が正しい | AI_FILTER が正しい | 判断が分かれる・判断できない |
|---|---|---|---|---|
| Jev が「いいえ」(< 0.2)、AI_FILTER が「はい」 | 70 | 54 | 5 | 11 |
| Jev が「はい」(≧ 0.5)、AI_FILTER が「いいえ」 | 14 | 5 | 3 | 6 |
- 「いいえ」の判定: Jev が「いいえ」とした投稿では、Jev が正しかったケース(54 件)が AI_FILTER が正しかったケース(5 件)を大きく上回りました。「いいえ」の判定では Jev のほうが精度が良いと言えます。裏返すと、AI_FILTER が TRUE とした 324 件のうち、少なくとも 54 件(約 17%)は「いいえ」が妥当でした。読んだのは Jev が 0.2 未満の投稿だけなので、これは下限の値です。
- 「はい」の判定: Jev が「はい」とした側の食い違いは 14 件と少なく、正誤も 5 件対 3 件で、はっきりした差は見えませんでした。
AI_FILTER の判定を基準に Jev を評価したにもかかわらず、食い違いを読むと、基準側の AI_FILTER の誤りが目立つ結果でした。
2. Jev は除外条件をよく反映する
Jev は criteria.false に書いた除外条件に沿って、BI・ETL・モデリング論の投稿にきちんと低いスコアを付けていました。質問とは別に「はい」「いいえ」の基準を渡せる API の作りが、うまく働いているようです。
一方の AI_FILTER は、DB・SQL・データといった言葉に引っ張られて「はい」に傾き、除外条件を十分に反映できていないように見えました。ただし AI_FILTER には、不利な条件もありました。
- モデルが公開されておらず、ドキュメントでは英語での入力が推奨されています。
- 今回のプロンプトは、日本語で除外条件を細かく書いた長いものでした。
英語のプロンプトにする、除外条件を分けて別々に判定する、といった工夫をすれば、AI_FILTER の結果は変わる可能性があります。
3. Jev の弱点は短い投稿と、実行ごとのぶれ
- 短い投稿に弱い: 製品名や構文が一言出てくるだけの「はい」寄りの投稿に、Jev は低いスコアを付けがちでした(0.1〜0.2 の帯の 5 件)。逆に、製品名があるだけで主題は別の投稿に高いスコアが付いた例も、わずかにありました(3 件)。文脈を補える形で入力を渡す(スレッドの前後をまとめるなど)と、改善するかもしれません。
- 実行ごとにスコアがぶれる: 同じ 20 件を 2 回判定すると、スコアは最大 0.07 ずれました。閾値ぎりぎりの投稿は、実行するたびに判定が変わる可能性があります。
4. 判定が割れた原因の一部は、質問の定義にある
「判断が分かれる」に振り分けた投稿には、次の 2 種類が多くありました。
- DBMS ベンダーの周辺ツール(GoldenGate、SQL Developer、SQLcl、Data Studio)
- DBMS の分類や業界の話(NewSQL など)
今回の質問文は「DBMS の製品・エンジン・サービスそのもの」とだけ書いていて、これらを含めるかどうかを決めていません。こうした投稿で判定が割れたのは、どちらかの手法の誤りというより、質問の定義があいまいだったからです。Jev では criteria に境界の例を書き足せるので、判定基準を詰めるほど結果が安定しそうです。手法を比べる前に、境界にあたる例を集めて判定基準を詰めておくことが大事だと感じました。
5. Jev のコストは AI_FILTER の 1 桁以上下
Jev のコストは AI_FILTER の約 1/34 でした。1,774 件ではどちらも数ドル以下ですが、100 万件規模になると Jev が約 31 ドル、AI_FILTER が約 1,050 ドルと大きな差になります。日次で大量のレコードを判定するような使い方では、このコストの差が Jev を選ぶ決め手になりえます。
6. Jev の処理時間はレート制限で決まる
Jev(23.0 秒)と AI_FILTER(7.1 秒)の差は、モデルの速さではなく、ほぼ Jev のレート制限による差です。1 リクエストあたりの応答時間は中央値 53 ms と、それ自体は十分に速いものでした。Snowflake から使うときは、次の点に注意が必要です。
- 大量に処理するなら、まずレート制限の引き上げを検討する: TypeSafe のドキュメントには「Higher limits are available on custom and enterprise plans. Contact sales@typesafe.ai.」とあり、custom プランや enterprise プランでは、より高い上限を使えます。処理時間の上限そのものを引き上げられるので、本来はこちらを検討すべきです。
- ウェアハウスを大きくしても速くならない: 上限が今のままだと、ウェアハウスを大きくしても処理時間は短くなりません。並列度が上がって 429 が出始めると、今回の UDF は 2・4・8・16 秒と待ってから再試行するので、かえって遅くなるおそれがあります。上限を引き上げられない場合は、ウェアハウスを小さいままにするか、送るリクエスト数を UDF 側で調整するのが次善の策です。
- 件数を増やしたときの最短時間: 1 件 1 リクエストなら、1 万件で約 2 分、10 万件で約 21 分、100 万件で約 3.5 時間です。
- 質問を増やすときは 1 リクエストにまとめる: Jev は 1 回のリクエストで複数の質問を評価できるので、リクエスト数の上限に有利です。ただし入力が平均 1,250 トークンを超えると、今度はトークン数の上限(10 万トークン/秒)が効いてきます。長い文書を判定する場合も同じです。
7. Jev と AI_FILTER の使い分け
| 観点 | Jev | AI_FILTER |
|---|---|---|
| 判定の質(今回の質問) | 大筋で AI_FILTER と一致。「いいえ」の判定の精度が良い(食い違った 70 件で 54 件対 5 件)。短い投稿ではスコアが低めに出る | 取りすぎが目立った(TRUE の 324 件のうち、少なくとも 54 件は「いいえ」が妥当) |
| 結果の形 | 0〜1 のスコア。閾値で精度と網羅性を調整できる | TRUE / FALSE のみ |
| コスト(1,774 件) | 0.055 ドル | 1.865 ドル |
| 処理時間(1,774 件) | 23.0 秒(レート制限が上限。custom / enterprise プランで引き上げ可能) | 7.1 秒 |
| 導入の手間 | 外部アクセス統合・SECRET・UDF の準備が必要 | SQL から直接呼べる |
- Jev が向いている場面: 除外条件まで含めて判定させたいとき、スコアで閾値を調整したいとき、大量の件数を安く判定したいとき。
- AI_FILTER が向いている場面: 準備なしに SQL だけで済ませたいとき、件数が少なくて処理の速さを優先したいとき。
注意事項
- 本記事の比較は一致度であり、精度ではありません。判定が食い違った投稿の振り分けは、私の判断によるものです。
- 各手法とも 1 回だけ実行した結果です。LLM の出力は実行ごとに変わることがあります。
- 投稿は 1 件ずつ単独で判定しています。スレッドの前後や引用元の文脈は渡していません。
- 投稿本文の HTML エスケープ(
&など、16 件)は、そのまま両手法に渡しました。 - ドルでのコスト比較は、Jev の公式ブログに記載された料金(入力 0.042 ドル/百万トークン、出力は無料)と、Snowflake の公開価格(AWS US West・Standard エディションで 1 クレジット 2.00 ドル、AI_FILTER は 1.62 AI クレジット/百万トークン・1 AI クレジット 2.00 ドル)で計算しました。契約によって単価が異なる場合は、読み替えてください。
おわりに
Jev は、Snowflake からでも外部アクセス統合と UDF を用意するだけで、問題なく使えました。今回の日本語の質問では、以前からある AI_FILTER と大筋で一致する判定を返しました。さらに「いいえ」の判定では、Jev のほうが精度が良い結果でした(判定が分かれた 70 件で、Jev が正しかったのは 54 件、AI_FILTER は 5 件)。コストは AI_FILTER の約 1/34 でした。処理時間はレート制限で頭打ちになるものの、1 リクエストあたりの応答は数十 ms と速く、件数が数万件程度までなら実用上の問題はなさそうです。それ以上の規模なら、custom / enterprise プランでレート制限を引き上げることを検討するとよいでしょう。
一方で、短い投稿や文脈に頼る投稿ではスコアが低めに出る、実行ごとにスコアが少しぶれる、といった弱点も見えました。そして、どちらの手法を使うにしても、判定基準をきちんと詰めることと、少数件で結果を目で確かめることが大切だと感じた検証でした。
Appendix A. 環境
| 項目 | 内容 |
|---|---|
| リージョン | AWS us-west-2(US West / Oregon) |
| エディション | Standard(1 クレジット 2.00 ドル、1 AI クレジット 2.00 ドル) |
| ロール |
DECISION_ROLE(SNOWFLAKE.CORTEX_USER を付与済み) |
| ウェアハウス |
DECISION_WH(X-Small、Gen2) |
| データベース・スキーマ | DECISION_DB.PUBLIC |
| 実行日 | 2026-10-20 |
管理者(ACCOUNTADMIN)に、事前に次の権限を付与してもらいました。
-- Jev 呼び出し用の外部アクセス統合を作成できるようにする
GRANT CREATE INTEGRATION ON ACCOUNT TO ROLE decision_role;
-- 処理時間やコストを集計するため、ACCOUNT_USAGE を参照できるようにする
GRANT IMPORTED PRIVILEGES ON DATABASE snowflake TO ROLE decision_role;
Appendix B. データの準備
X のアーカイブ(tweets.json)は、Snowsight の画面からステージにアップロードしました。
CREATE FILE FORMAT IF NOT EXISTS decision_db.public.json_format
TYPE = JSON
STRIP_OUTER_ARRAY = TRUE;
CREATE STAGE IF NOT EXISTS decision_db.public.tweets_stage
FILE_FORMAT = decision_db.public.json_format
ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE')
DIRECTORY = (ENABLE = TRUE);
-- tweets.json は Snowsight からアップロードする
CREATE OR REPLACE TABLE decision_db.public.all_posts (
raw_data VARIANT
);
COPY INTO decision_db.public.all_posts
FROM @decision_db.public.tweets_stage/tweets.json
FILE_FORMAT = (FORMAT_NAME = decision_db.public.json_format)
ON_ERROR = ABORT_STATEMENT;
-- 28,642 件
-- 2025 年(UTC)の、リポスト以外の投稿を取り出す
CREATE OR REPLACE TABLE decision_db.public.my_posts_2025 (
id_str STRING,
full_text STRING
) AS
SELECT
raw_data:tweet.id_str::STRING AS id_str,
raw_data:tweet.full_text::STRING AS full_text
FROM decision_db.public.all_posts
WHERE RIGHT(raw_data:tweet.created_at::STRING, 4) = '2025'
AND NOT STARTSWITH(raw_data:tweet.full_text::STRING, 'RT @');
-- 1,774 件(本文の長さは 2〜204 文字、平均 92.1 文字)
created_at には "Sun Jul 19 01:24:06 +0000 2026" のような形式で日時が入っているので、末尾の 4 文字で年を判定しています。
Appendix C. Jev を呼び出す UDF
呼び出しの流れ
準備(API キー、ネットワークルール、外部アクセス統合)
-- API キーを登録する(値は各自で設定)
CREATE SECRET IF NOT EXISTS decision_db.public.typesafe_api_key
TYPE = GENERIC_STRING
SECRET_STRING = '<API キー>';
-- api.typesafe.ai への外向き通信を許可する
CREATE OR REPLACE NETWORK RULE decision_db.public.typesafe_api_rule
MODE = EGRESS
TYPE = HOST_PORT
VALUE_LIST = ('api.typesafe.ai:443');
CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION typesafe_api_integration
ALLOWED_NETWORK_RULES = (decision_db.public.typesafe_api_rule)
ALLOWED_AUTHENTICATION_SECRETS = (decision_db.public.typesafe_api_key)
ENABLED = TRUE;
UDF 本体
CREATE OR REPLACE FUNCTION decision_db.public.ask_jev(post_text STRING)
RETURNS VARIANT
LANGUAGE PYTHON
RUNTIME_VERSION = '3.11'
PACKAGES = ('requests')
HANDLER = 'ask_jev'
EXTERNAL_ACCESS_INTEGRATIONS = (typesafe_api_integration)
SECRETS = ('api_key' = decision_db.public.typesafe_api_key)
AS
$$
import time
import _snowflake
import requests
ENDPOINT = "https://api.typesafe.ai/v1/systemone"
MODEL = "jev-latest"
TIMEOUT_SEC = 60
MAX_ATTEMPTS = 5
INSTRUCTIONS = (
"この投稿の主題は、データを格納・管理・問い合わせする「データベース管理システム(DBMS)」の"
"製品・エンジン・サービスそのもの(例: Oracle Database, PostgreSQL, MySQL, SQL Server, "
"DB2, MongoDB, Redis, SQLite, DuckDB, Snowflake, BigQuery, Redshift, "
"Amazon RDS/Aurora, TiDB, Spanner などのRDBMS/NoSQL/クラウドDWH/データプラットフォーム)ですか。"
)
CRITERIA_TRUE = "DBMSの製品・エンジン・サービスそのものが主題の投稿"
CRITERIA_FALSE = (
"BIツール(Tableau,Power BI,Lightdash,Looker)、ETL/データ統合ツール(Informatica,Fivetran,"
"dbt,Stitch)、データカタログ(OpenMetadata)、セマンティックレイヤー(Cube.js)、"
"あるいは特定製品に紐づかない一般的なSQL論・データモデリング論だけの投稿は「いいえ」とする。"
)
session = requests.Session()
def build_body(post):
return {
"state": post,
"model": MODEL,
"questions": {
"is_dbms": {
"type": "noul",
"instructions": INSTRUCTIONS,
"criteria": {"true": CRITERIA_TRUE, "false": CRITERIA_FALSE},
}
},
}
def parse(res):
try:
return res.json()
except ValueError:
return {"raw": res.text}
def ask_jev(post_text):
api_key = _snowflake.get_generic_secret_string("api_key")
body = build_body(post_text)
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
}
started = time.time()
result = None
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
res = session.post(ENDPOINT, json=body, headers=headers, timeout=TIMEOUT_SEC)
if res.ok:
result = parse(res)
if not isinstance(result, dict):
result = {"raw": result}
result["_status"] = res.status_code
break
result = {"error": parse(res), "_status": res.status_code}
if res.status_code != 429 and res.status_code < 500:
break
except Exception as e:
result = {"error": f"{type(e).__name__}: {e}", "_status": None}
if attempt < MAX_ATTEMPTS:
time.sleep(2 ** attempt)
result["_elapsed_ms"] = int((time.time() - started) * 1000)
result["_attempts"] = attempt
return result
$$;
実装時の注意点
-
urllibでは SSL エラーになる: 最初は標準ライブラリのurllibで書きましたが、Snowflake の Python 実行環境ではSSL: CERTIFICATE_VERIFY_FAILED (unable to get local issuer certificate)で失敗しました。PACKAGES = ('requests')を指定してrequestsに切り替えたところ動きました。 -
1 件の失敗でクエリ全体を止めない: 例外は捕捉して
errorに入れて返すようにしています。失敗した行だけを後からMERGEで再実行できます。今回はエラーが 0 件だったので、再実行は不要でした。 -
モデルの指定方法: 公式ドキュメントの例ではリクエストに
selectedModelsを使っていますが、"model": "jev-latest"でも受け付けられました。レスポンスのmodelはjev-1.13.0でした。 -
レート制限への備え: API には 80 リクエスト/秒、入力 100,000 トークン/秒のレート制限があります。UDF では、429 や 5xx のときに待ち時間を延ばしながら最大 5 回まで再試行するようにしました。今回は 429 が 1 件も出なかったので、再試行は動いていません。大量に処理する場合は、まず TypeSafe にレート制限の引き上げを相談し、そのうえでウェアハウスのサイズやリクエスト数の調整を検討してください(考察 6 を参照)。なお、TypeSafe の公式 SDK は、429 の再試行時に
retry-afterヘッダーがあればそれに従います。今回の UDF はretry-afterを見ていないので、本格的に使うならこの対応も入れるとよいでしょう。 -
計測用の情報を返す: レスポンスの JSON に、HTTP ステータス(
_status)、再試行込みの応答時間(_elapsed_ms)、試行回数(_attempts)を追加して返しています。レート制限の考察に使った応答時間の分布は、この_elapsed_msから集計しました。
実行
CREATE OR REPLACE TABLE decision_db.public.result_jev AS
SELECT
id_str,
full_text,
jev_response,
jev_response:answers.is_dbms.noul::FLOAT AS score
FROM (
SELECT
id_str,
full_text,
decision_db.public.ask_jev(full_text) AS jev_response
FROM decision_db.public.my_posts_2025
);
Appendix D. AI_FILTER の実行
AI_FILTER の第 2 引数(return_error_details)に TRUE を渡すと、戻り値が OBJECT(value BOOLEAN, error VARCHAR) になり、失敗した行のエラー理由も取れます。
CREATE OR REPLACE TABLE decision_db.public.result_ai_filter AS
SELECT
id_str,
full_text,
raw_result:value::BOOLEAN AS answer,
raw_result:error::STRING AS error
FROM (
SELECT
id_str,
full_text,
AI_FILTER(
PROMPT(
'この投稿の主題は、データを格納・管理・問い合わせする「データベース管理システム(DBMS)」の製品・エンジン・サービスそのもの(例: Oracle Database, PostgreSQL, MySQL, SQL Server, DB2, MongoDB, Redis, SQLite, DuckDB, Snowflake, BigQuery, Redshift, Amazon RDS/Aurora, TiDB, Spanner などのRDBMS/NoSQL/クラウドDWH/データプラットフォーム)ですか。\nBIツール(Tableau,Power BI,Lightdash,Looker)、ETL/データ統合ツール(Informatica,Fivetran,dbt,Stitch)、データカタログ(OpenMetadata)、セマンティックレイヤー(Cube.js)、あるいは特定製品に紐づかない一般的なSQL論・データモデリング論だけの投稿は「いいえ」とする。\n\n投稿: {0}',
full_text
),
TRUE
) AS raw_result
FROM decision_db.public.my_posts_2025
);
トークン数と AI クレジットは、CORTEX_AISQL_USAGE_HISTORY から取得します(token_credits 列)。AI_FILTER の使用量は、CORTEX_FUNCTIONS_QUERY_USAGE_HISTORY には出てきませんでした。
SELECT query_id, tokens, token_credits
FROM snowflake.account_usage.cortex_aisql_usage_history
WHERE query_id = '<AI_FILTER を実行したクエリの query_id>';
なお、AI_COUNT_TOKENS('ai_filter', ...) は NULL を返したので、トークン数の事前見積もりには使えませんでした。今回は、20 件で試した結果から全件分を見積もりました。
Appendix E. 比較の SQL
2 つの結果を統合する
CREATE OR REPLACE TABLE decision_db.public.comparison AS
SELECT
p.id_str,
p.full_text,
a.answer AS ai_filter_answer,
j.score AS jev_score
FROM decision_db.public.my_posts_2025 p
LEFT JOIN decision_db.public.result_ai_filter a ON a.id_str = p.id_str
LEFT JOIN decision_db.public.result_jev j ON j.id_str = p.id_str;
AUC を求める
TRUE と FALSE の投稿を 1 件ずつ組にした全ペアについて、「TRUE 側のスコアのほうが高い」割合を数えます。同点は 0.5 として数えます(Mann-Whitney の U 検定と同じ考え方)。今回は 324 × 1,450 = 約 47 万ペアなので、単純な CROSS JOIN でもすぐに計算が終わります。
WITH pos AS (SELECT jev_score AS s FROM decision_db.public.comparison WHERE ai_filter_answer),
neg AS (SELECT jev_score AS s FROM decision_db.public.comparison WHERE NOT ai_filter_answer)
SELECT
COUNT(*) AS n_pairs,
COUNT_IF(pos.s > neg.s) AS pos_higher,
COUNT_IF(pos.s = neg.s) AS ties,
ROUND((COUNT_IF(pos.s > neg.s) + 0.5 * COUNT_IF(pos.s = neg.s)) / COUNT(*), 4) AS auc
FROM pos CROSS JOIN neg;
-- n_pairs 469,800 / pos_higher 453,426 / ties 3,134 → AUC 0.9685
スコアの分布
SELECT
LEAST(FLOOR(jev_score * 10), 9) / 10 AS bin_start,
COUNT_IF(ai_filter_answer) AS ai_filter_true,
COUNT_IF(NOT ai_filter_answer) AS ai_filter_false
FROM decision_db.public.comparison
GROUP BY 1
ORDER BY 1;
Appendix F. 処理時間とコストの取得
| 指標 | 取得元 | 反映までの時間 |
|---|---|---|
| 処理時間 |
INFORMATION_SCHEMA.QUERY_HISTORY の TOTAL_ELAPSED_TIME
|
遅延なし |
| AI_FILTER のトークン数・AI クレジット | ACCOUNT_USAGE.CORTEX_AISQL_USAGE_HISTORY |
今回は約 10 分 |
| ウェアハウスのクレジット(クエリ単位) | ACCOUNT_USAGE.QUERY_ATTRIBUTION_HISTORY |
実行当日には未反映。4 日後に確認したときには反映されていた |
| Jev のトークン数 | API レスポンスの usage.input_tokens と usage.output_tokens を合計 |
— |
SELECT query_id, credits_attributed_compute
FROM snowflake.account_usage.query_attribution_history
WHERE query_id IN ('<AI_FILTER の query_id>', '<Jev の query_id>');
Jev の応答時間の分布と、同時に処理していたリクエスト数の目安は、UDF が返した _elapsed_ms から集計しました。
SELECT
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY jev_response:_elapsed_ms::INT) AS p50_ms, -- 53
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY jev_response:_elapsed_ms::INT) AS p75_ms, -- 69
PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY jev_response:_elapsed_ms::INT) AS p90_ms, -- 96
PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY jev_response:_elapsed_ms::INT) AS p99_ms, -- 183
MAX(jev_response:_elapsed_ms::INT) AS max_ms, -- 560
SUM(jev_response:_elapsed_ms::INT) AS sum_ms, -- 110,848
ROUND(AVG(jev_response:usage.input_tokens::INT), 1) AS avg_input_tokens, -- 652.9
COUNT_IF(jev_response:_elapsed_ms::INT < 70) AS lt_70ms, -- 1,333
COUNT_IF(jev_response:_elapsed_ms::INT > 500) AS gt_500ms -- 1
FROM decision_db.public.result_jev;
-- 同時に処理していたリクエスト数の目安 = sum_ms ÷ クエリの実行時間(22,157 ms)≒ 5.0
QUERY_ATTRIBUTION_HISTORY のクレジットには、ウェアハウスのアイドル時間は含まれません。また、同じ時間帯に動いていたほかのクエリとの按分の影響も受けます。実際、20 件の試行のほうが全件実行より多いクレジットが記録されていました。値が小さいので、あくまで目安と考えてください。
Jev の処理時間の詳細:レート制限との関係
本編の「レート制限から見た処理時間」の詳細です。
-
Snowflake 側は平均 5 件ほどを同時に処理していた:
- 各リクエストの応答時間を合計すると 110.8 秒でした。これを実行時間 22.16 秒で割ると約 5.0 で、同時に平均 5 件ほどのリクエストが飛んでいた計算です。
- 平均の応答時間は 62.5 ms なので、5 件 ÷ 0.0625 秒 = 80 リクエスト/秒となり、実測の処理速度と一致します。
- 429(リクエスト過多)のエラーや再試行は 1 件もありませんでした。
処理速度が上限にぴったり張り付いた理由として、2 つの可能性が考えられます。今回のデータでは、どちらかを決められませんでした。ただし TypeSafe のドキュメントには「上限を超えたリクエストには 429 を返す」と書かれているので、ドキュメントどおりなら 1 つ目の可能性が高いと考えられます。
- 並列度がたまたま上限と釣り合った: X-Small のウェアハウスで UDF が同時に約 5 本動き、その結果がたまたま 80 リクエスト/秒になった。
-
API 側がエラーを返さずに応答を遅らせて調整している: 上限を超えそうなときに、429 を返す代わりに応答を少し遅らせていた。
- 20 件の試行では、応答時間は平均 50 ms でした。全件実行では平均 62.5 ms で、遅い側に伸びた応答もありました(99 パーセンタイルは 183 ms)。これは 2 つ目の可能性と矛盾しません。
- 確かめるには、送信と受信の時刻を記録する、ウェアハウスのサイズを変えて処理速度と 429 の有無を比べる、といった追加の検証が必要です。
Jev の応答時間の詳細:公式値との比較
TypeSafe の公式ブログでは、応答時間は「End-to-end response time is 70ms-500ms」(Introducing System One Models and Jev)とされています。今回 UDF の中で計った、再試行込みの 1 リクエストあたりの応答時間(_elapsed_ms)と比べてみます。
| 指標 | 実測(1,774 リクエスト) |
|---|---|
| 最小 | 36 ms |
| 中央値 | 53 ms |
| 75 パーセンタイル | 69 ms |
| 90 パーセンタイル | 96 ms |
| 99 パーセンタイル | 183 ms |
| 最大 | 560 ms |
| 平均 | 62.5 ms |
| 公式値(70〜500 ms)との関係 | 件数 | 割合 |
|---|---|---|
| 70 ms 未満(公式値より速い) | 1,333 | 75.1% |
| 70〜500 ms(公式値の範囲内) | 440 | 24.8% |
| 500 ms 超(公式値より遅い) | 1 | 0.1% |
- 公式値より速い応答が多い: 4 分の 3 のリクエストが 70 ms 未満で返ってきました。500 ms を超えたのは 1 件(560 ms)だけで、ほぼすべてが公式値の上限より速く返っています。
-
速かった理由(推測): 確かめてはいませんが、次の 2 点が効いたと考えられます。
- 今回の入力が短い投稿だった(平均 92 文字、入力 653 トークン)。
- UDF で
requests.Sessionを使い、接続を使い回していた。新しく TLS 接続を張る時間が、2 回目以降のリクエストでは省けた可能性があります。
- レート制限の影響を含んだ値: 本編で見たように、今回の処理速度はレート制限の上限に張り付いていました。もし API 側が応答を遅らせて調整していたなら、制限にかからない条件での応答時間は、今回の値よりさらに短いはずです。
- 公式値とは測り方が違う: 今回の値は、Snowflake(AWS us-west-2)の UDF から計った値です。公式値がどこから、どんな入力で計ったものかは書かれていません。単純には比べられないので、目安として見てください。