はじめに
こんにちは。Gunosy R&D チームの森田です。
こちらの記事は Gunosy Tech Blog Festa の 3日目の記事です。
去年の今頃には「今は難しいけどそのうちできるようになるか」と思っていた SQL 生成 (Text-to-SQL) がいつの間にやら実用にかなり近いところまで来ています。ざっと必要なテーブル情報など与えれば SQL 生成できるようになってきているので、さっと GPT や Gemini にクエリを書いてもらっている方も多いのではないでしょうか。
今回は Gunosy で今取り組んでいる、ある程度雑な指示から分析クエリを生成する時の知見、特に評価の難しさについてお話しようと思います。

1. クエリ生成のスコープ
今回のクエリ生成では、ターゲットユーザとして、 SQL を自分で書くのは大変だが読むことはできる SQL 初学者や社内のテーブルに詳しく無いエンジニアを想定しています。今回、クエリを生成して分析したい内容は単純な例だと以下のようなものです。
- 「12/15 にヘビーユーザーによく読まれている記事の上位 10 件を知りたい」
- 「ユーザの機種ごとに Push 通知の開封率を調べたい」
Text-to-SQL でこのくらいの自然文をそのまま SQL に変換できるようにするのが目標です。
指示は最小限の粗いものにしたい: 記事を読んだログがどのテーブルのどのカラムにあるか、ユーザは把握していない想定。
定義を毎回教えたくない: 「ヘビーユーザーとは直近 1ヶ月で N 回以上アクセスしたユーザーで…」と毎回指示に書かずに、過去の分析で使われていた定義を再利用したい。
特定のモデルに依存したくない: LLMの進化は速いため、特定のモデルへの Fine-tuning は避け、モデルをスイッチしやすい構成にしたい。
単なるクエリ生成にとどまらず、「よく読まれる記事の条件が知りたい」「Push 通知の開封率を上げる条件を知りたい」といった実際の分析そのもの、クエリ生成〜結果分析のループをLLMが自律して回す、といった所は今回はスコープ外です。(分析のステップを丁寧に分解して再現していけばできそうな気配はありますが、またの機会に)
2. SQL クエリをLLMに書かせるには
ここは世の中にいくらでも似た事例があると思うので本題に入る前にざっとどんなことをしているか説明するにとどめましょう。
現在のLLM(GPT-5 や Gemini 3.0)は適切なコンテキストを与えれば、With 句(CTE: Common Table Expression、仮想的な中間テーブル)を多用した500行程度の複雑なクエリでも十分に生成可能です。とすると、重要なのは、LLMに渡すコンテキストをどのように用意するかです。
クエリの構築に必要なドメイン知識と、LLM がやりがちなミスを補正する指示を揃えると、ほぼ問題なく実行でき、多少の修正で目的の分析が行えるクエリが生成できるようになります。
Agentic に動的な過去クエリ収集やサンプルデータの収集でクエリを生成する方向性ももちろんありますが、クエリ生成に必要な知識のうち、「収集しやすい知識」と「収集しにくい知識」を分類・分析し、必要に応じて人手で修正できるようにするため、今回は知識収集と生成のステップを分けました。
2.1 テーブル定義と実サンプル
単にスキーマ定義を与えるだけでも中身を推測してうまくクエリを書いてくれますが、あくまで推測なのでエラーになるクエリを生成してしまうこともあります。推測に頼らず生成されるクエリのエラーを減らすにはより詳細な情報を与える必要があります。
各テーブルのデータサンプルと、そのテーブルを過去に分析に用いたクエリのサンプルから、テーブルの扱い方を収集しましょう。
サンプルデータとフォーマット: カラム名だけでなく、実際に入っている値の例や詳細なフォーマット
テーブルに仕掛けられた罠
- カラム名は
dateだが、中身は Unix Timestamp abc_idとあるが、実際の値はfloatになっている
- カラム名は
2.2 既存クエリから分析に使う「ドメイン知識」を注入する
「ヘビーユーザー」のような分析上で使う定義は、過去に実行された分析クエリを参考に収集します。過去クエリからテーブルに紐づくドメイン知識、テーブルに紐づかない全般的なドメイン知識をそれぞれ収集しておきます。
- 収集される知識の例
- 「ヘビーユーザ」はどこのテーブルを参照して、どのように決めるか
- テーブル A とテーブル B を結合する際、どのようにIDを変換するか
- 分析から除くべき異常値を含むログはどのようなものか
- 似ているが異なるデータを保持しているカラムの使い分け
- 分析につかうべきでないカラム
2.3 構文エラー回避の指示セット
SQL 生成を試していると、なぜか LLM が頻繁に繰り返しがちなミスがあります。環境に応じて LLM のミスを事前に防ぐための指示を整備するかどうかで、クエリのエラー率は大きく変わります。今回実験と評価を行った環境は Athena ですが、環境ごとに間違えるポイントは変わってくると思われるので、生成されたクエリのエラーみつつ揃えていくのが良いでしょう。
特に、タイムゾーン変換(JST/UTC)をどう扱うかは、一般的にLLMで苦手とされていてエラーの頻度も特に高い要注意ポイントです。世の中で公開されているクエリで JST/UTC の変換をするようなクエリは少なく、変換方法には環境ごとに方言があり、おおよその構文は同じであるがゆえに、ローカルな文脈ではどう変換すべきかが推測しづらく、LLM 泣かせなポイントだと思います。
他にもありがちな誤りとしては次のようなものがあります。
- from_iso8601_timestamp を対応していないフォーマットのタイムスタンプでも多用しがち
- format_datetime と date_format を混同しがち
- 文末のセミコロン ; の後にコメントを書いてしまう (Redash ではエラーになってしまう)
3. 評価をどうする?
今の LLM は賢いので、ある程度良さそう、使えそうなクエリはすぐに作れるようになります。しかし、実運用に乗せる前に 「本当に正しいクエリが生成できているのか?」 を評価しようとすると、急に道が険しくなってきます。
今回は、過去にデータ分析に用いられた 500 行程度の SQL クエリをターゲットとしています。
アカデミックな評価と実務の乖離
学術的な Text-to-SQL の評価(Spider 2.0 や BIRD-SQL など)も急速に進化しており、かなり実務に近いデータセットも用意されるようになってきました。
- Spider 2.0:
- テーブル数は 400 件程度と多いが、クエリ自体は 30 行〜最大 180 行程度で、今回ターゲットとするクエリと比べて短め。
- BIRD (BIg Bench for LaRge-scale Database Grounded Text-to-SQL Evaluation):
- テーブル数 94 件で実数値が文字列で保存されているなど素直でないデータも含む
- 1,000 行近いクエリもありかなり巨大
ベンチマークでの評価は「クエリの一致」や「結果の完全一致」がベースになっています。しかし、ベンチマーク用に統制されたデータセットとは異なり、ゴールが必ずしも明確でない長大な分析クエリの生成においては、結果を完全一致させること自体が困難な場合があります。
なぜ「クエリの一致」で評価できないのか?
人間が書いたクエリ同士であっても、同じ目的で全く異なる SQL クエリになることが多々あります。
- タイムゾーン変換:
+ interval 9 hoursと書くか、with_timezone(..., 'JST')と書くか。 - フィルタリング順序:先に絞るか、JOIN してから絞るか。
- データの所在:アクティブユーザー数を出すのに「日別サマリ」を使うか「生ログ」を集計するか。
- 分析テンプレートの利用: 必要な指標だけ計算するか、巨大なテンプレートクエリをベースに使って必要な列だけ SELECT するか。
- 中間テーブルをどう切るか、サブクエリで済ませるか、JOIN の組み合わせで表現するかなど
また、人が書いたクエリでも必ずしも完全な正解ではない、というのも実データでの評価の難しい点です。分析結果に影響のない細かなミスが残っていたり、不要なカラムがあったり、実は使われていない壊れたクエリであったり等など、生成したクエリと結果が合わずに細かく分析していくと実は人が書いたクエリが間違っている……ということもあります。
ではなぜ「結果の一致」も難しいのか?
「結果が合っていれば OK」としたいところですが、これも一筋縄ではいきません。
- 厳密な正解が定義しにくく、間違いとは言えない範囲が広い処理
- 大量のログを扱った分析をする場合などでは厳密な値を取得するコストに対して分析上の意義が薄く、
APPROX_PERCENTILEなどの近似関数やTABLESAMPLEを使うことがあり、これらの関数は実行のたびに結果が微妙に変わる可能性があります。 - 集計方法も分析の目的によってはどちらかがより正しいといえる場合もありますが、どちらが間違いとも言えない処理で結果が微妙に変わってしまうケースがあります
- 「クリック数上位 1% ユーザの除外」を集計のどのタイミングで行うか(日付ごとのユーザ全体1%か、抽出条件を満たすユーザの1%か)で集計から外されるユーザが変わってしまう
- 上位 10 位の記事を取り出す時に
RANK()かROW_NUMBER()かでタイ(同順位)の扱いが変わってしまう
- 類似した情報を表すどのデータを参照するか
- 例えば記事を読み始めた時間をつかって集計するか、読み終わった時間とするか、ログが作成された時間とするか、等
- 大量のログを扱った分析をする場合などでは厳密な値を取得するコストに対して分析上の意義が薄く、
- 分析結果に対する許容度の違い
- 例えば、売上が 1% 上がった要因を分析する時、 Impression 数が1%ずれていても分析上の影響は軽微ですが、売上金額の 1% のズレは当然許容できません。「一律 ±1% ならOK」というような判定は難しく、分析の目的やデータの性質を考慮して評価する必要があります。
- 絶対値としての値のズレよりも、グラフで表した時の傾向が変わってしまう方が、分析結果に与える影響は大きく、これも単純な割合では表しにくい理由の一つです。
4. じゃあどうやって評価・分析するのか?
手法間を比較したいのであれば BIRD など一定の統制がある問題設定で比較するのがベストです。一方で、今回のようにあるクエリ生成の方法があなたのデータで使えるかどうか、改善する余地がどこにあるかを判定するにはかなり泥臭いアプローチが必要です。
実験設定としては、元となった分析用のクエリだけがある状況です。分析用のクエリを元に LLM でクエリ生成の指示(クエリ作成課題)を作成し、クエリ作成課題とドメイン知識を入力として再度生成したクエリを元のクエリと比較します。
結果の比較
最初のステップは結果の比較です。(運よく)完全に一致していることももちろんありますが、問題となるのは結果にズレがある場合です。分析結果に差が生じるズレかどうか、生成したクエリのどこが誤っていて、誤りの原因はどのような知識の不足、LLM の能力不足、あるいは元となったクエリが間違っているのか、を分析していく必要があります。
クエリ比較
生成したクエリのどこが間違っているかを調べるには、生成クエリと元クエリの処理内容を比較する必要があります。 しかし多くの場合CTEの分け方や処理の順番が異なるため、単純な diff で比較することはできません。デバッグするように、ロジックとしてどこが等価かを調べながら処理を追う必要があります。
これが人手ではなかなか難しく、サイゼリヤの間違い探しを超える高難度間違い探しの様相を呈します。通常のデバッグであれば中間テーブルが想定と一致しているかを見ていきますが、処理の方針が異なる 2つのクエリでは比較するポイントを探すのも一苦労。SQL 力が不足気味の私の場合、1時間にらめっこしても何の成果も得られないこともある、過酷な作業になります。一生懸命読み込んだ生成クエリに誤りがなく、元クエリの誤りということもあるため生成クエリだけではなく元クエリも詳しく読んでいく必要があります。
ここは、人力の比較は最終手段と割り切り、LLM の力を借りましょう。しかし、 LLM を用いた評価(LLM-as-a-Judge)であればすんなり解決するか?というとそれほど話は単純ではありません。もちろん、LLM に 2つのクエリの比較を行わせ、処理の差異を説明させることはできますが、結局データベースでそのクエリがどう振る舞うかは LLM には分からないため、結果のズレにつながる差異を一発で見つけるようなことはまだ難しいようです。あまり重要でない差異にこだわりすぎてしまったり、LLM が生成したクエリを過剰に信用する傾向があります。
結果、対話的に LLM と協力して差異を潰し、クエリを修正しながら実行を繰り返して、結果に大きな影響を及ぼす差異とその原因となる不足知識を追うことになります。基本的にはこの方針で、泥臭くエラーの分析や不足知識の調査を進めていきます。
まとめ
クエリ生成はできる: 適切なコンテキスト(既存クエリのドメイン知識)を与えれば、LLM は500行以上の複雑な分析クエリでも十分に書くことができます。
評価は難しい: 実務の分析 SQL クエリにおいて、クエリの完全一致や単純な結果の一致による自動評価は困難で、LLM にとっても出力の違いを生む原因を特定することは困難な作業のようです。「生成された複雑なクエリの正当性を実環境でどう効率的に検証するか」は、Text-to-SQL を実用化する際の大きな課題になりそうです。
SQL 生成の評価手法について、「こんな論文があるよ」「うちはこうやってるよ」という知見があれば、ぜひ教えてください!
Appendix
クエリを修正し、出力を見ながらデバッグを進めていく作業は人にとっても泥臭く困難が伴う作業ですが、 LLM にとってもなかなか辛い作業のようです。 時には、修正しても出力が元クエリと一致せず、袋小路に迷い込んでしまうこともあります。そんな時に LLM (Gemini) が残した諦めの言葉が面白かったので Appendix として残しておきます。
最終的な結論と推奨
ここまで、考えられる全ての論理的な差異を一つずつ潰すという、非常に詳細なデバッグ作業を行ってきました。しかし、それでもなお、実行のたびに不可解な結果が出てくる状況です。
これは、もはや SQL の記述を修正することで解決できる問題の範囲を超えていることを示しています。クエリの実行環境、テーブルのメタデータ、あるいは Athena のクエリプランナーの挙動など、我々が直接触れることのできない部分に、根本的な原因が潜んでいると結論付けるのが最も合理的です。
これ以上、テキストベースでクエリの修正を続けることは、かえって混乱を招く可能性が高いと思われます。つきましては、大変恐縮ですが、私からの SQL 修正の提案は、これを最終とさせていただければと存じます。
(ちなみに原因となっている差異はタイムスタンプの扱い方でした)