LLMによる自然言語を用いたDB問い合わせ機能の実装例
はじめに
こんにちは、Zennチームの五十嵐です。
Zennでは、日々の運用業務(お問い合わせの調査やデータ分析など)において、DBに対してSQLでデータを参照する事があります。非定型作業については毎回SQLを手書きしていましたが、人間がSQLを書くのではなくAIに任せられないかと考え、独自の管理画面からデータベースに対して自然言語で問い合わせをできる機能を実装しました。(将来的にはタスクをAI Agentが自律的に解決できるようになるのがベストですが、今回はその第一歩です。)
当初このアイディアは、データベースに対応したMCPサーバーを用いることで実現できることが検証できました。しかし、現時点ではMCPサーバーのリスク診断を行うエコシステムが成熟していないため、もしMCPに悪意のあるコードが混入されたら?と考えると、本番環境で使うにはリスクが大きいと考えました。そこで、MCPを使わずに自然言語で問い合わせを実現する方法を検討しました。
検討していたところ、Claude CodeはMCPを使わずとも要求に応じたSQLを生成できることが分かりました。これは、プロジェクトがRuby on Railsであり、DBスキーマが schema.rb
というファイルに定義されているためです。Claude Codeは主にこのファイルを読み込むことで、要求に応じたSQLを生成できることが分かりました。つまり、LLMにDBスキーマを読み込ませることで、SQLの生成ができるということを示しています。
実装
実装にはもともとプロジェクトで使用していたGemini SDKを使用しました。モデルはGemini 2.5 Flash(Thinkingあり)とGemini 2.5 Proを比較しましたが、どちらも同等の精度・速度であり、利用頻度が低いことからコストは考えずにGemini 2.5 Proを選択しました。
Gemini SDKの設定。結果をJSONで取りたいのでresponse_schemaを設定します。
VertexAiApi::Gemini.new(
model: "gemini-2.5-pro",
temperature: 0,
thinking_budget: -1, # 動的思考をオンにする
max_output_tokens: 2048,
fallback_models: ["gemini-2.5-flash"],
json: true,
response_schema: response_schema
)
response_schemaは管理画面で使いやすいように以下のように定義しました。
def response_schema
{
type: "object",
properties: {
sql_query: {
type: "string",
description: "生成されたSQL SELECTクエリ"
},
explanation: {
type: "string",
description: "クエリが何をするかの人間が読める説明"
},
confidence: {
type: "integer",
minimum: 0,
maximum: 100,
description: "生成されたSQLの信頼度 (0-100)"
},
warnings: {
type: "array",
items: { type: "string" },
description: "クエリに関する警告や制限事項"
}
},
required: ["sql_query", "explanation", "confidence"],
additionalProperties: false
}
end
プロンプトは以下のように定義ました。
あなたはPostgreSQLのエキスパートで、自然言語から安全で正確なSELECTクエリを生成します。
重要な安全ルール:
- SELECT文のみを生成する
- INSERT、UPDATE、DELETE、DROP、CREATE、ALTER、TRUNCATEは絶対に使用しない
- システムテーブル (pg_*、information_schema) にはアクセスしない
- 結果は最大1000行に制限する
- テーブルを結合する際は適切なJOIN構文を使用する
- 可能な場合は常にパラメータ化クエリを使用する
データベーススキーマ:
#{format_schema_context(schema_context)}
ユーザークエリ: "#{query}"
この質問に答えるPostgreSQL SELECTクエリを生成してください。以下を含めてください:
1. SQLクエリ (SELECTのみ)
2. クエリが何をするかの明確な説明
3. 信頼度 (0-100)
4. 警告や制限事項
指定されたJSON形式で応答してください。
プロンプトで安全ルール(SELECT以外を実行しない)を設けていますが、SQLを実行する際はDBに読み取り専用で接続することで安全性を高めています。
「データベーススキーマ」にはDBのスキーマ情報が、「ユーザークエリ」にはユーザーからの自然言語での要求が入ります。
データベーススキーマは、ActiveRecordから以下のように取得します。
class SchemaContextGenerator
# ActiveRecordのイントロスペクションを使用してデータベーススキーマコンテキストを動的に生成
# これは、自然言語からSQLへの変換に使用するために、
# すべてのテーブルとその関係の完全な概要を提供します
def self.generate_context
{
tables: generate_tables_info,
relationships: generate_relationships,
}
end
def self.generate_tables_info
ActiveRecord::Base.connection.tables.filter_map do |table_name|
# Rails内部テーブルとシステムテーブルをスキップ
next if skip_table?(table_name)
columns = ActiveRecord::Base.connection.columns(table_name)
{
name: table_name,
columns: columns.filter_map { |col| format_column(col) },
}
end
end
def self.generate_relationships
relationships = {}
ActiveRecord::Base.connection.tables.each do |table_name|
next if skip_table?(table_name)
foreign_keys = ActiveRecord::Base.connection.foreign_keys(table_name)
relationships[table_name] = foreign_keys.map do |fk|
{
column: fk.column,
references_table: fk.to_table,
references_column: fk.primary_key,
name: fk.name
}
end
end
relationships
end
def self.skip_table?(table_name)
# Rails内部テーブルとシステムテーブルをスキップ
internal_tables = %w[
schema_migrations
ar_internal_metadata
]
internal_tables.include?(table_name)
end
def self.format_column(column)
# クエリで公開すべきでない機密列をスキップ
sensitive_columns = %w[
encrypted_password
]
return nil if sensitive_columns.any? { |sensitive| column.name.include?(sensitive) }
{
name: column.name,
type: column.type.to_s,
null: column.null,
default: column.default,
comment: column.comment
}
end
end
データベーススキーマは、Hashから以下の形式に変換してLLMに渡されます。
Table: articles
Columns:
id: integer
user_id: integer
slug: string
title: string (nullable)
...
Table: ...
Columns:
...
RELATIONSHIPS:
articles.user_id -> users.id
...
管理画面
実際の動作をデモします。
まず最初にDBから参照したい情報を自然言語で入力します。
「SQLを生成」を実行すると、下に生成されたSQLが表示されます。SQLは編集することも可能です。生成されたSQLでは、投稿期間の絞り込みや、ポリモーフィック関連のテーブルJOINが適切に表現できています。
「SQLを実行」する前に、必ず「EXPLAIN実行」を必須にしています。意図せず高負荷なクエリを実行してしまうことを防ぐため、いまは人間の目で確認するようにしています。
EXPLAINの結果が表示されます。問題なければ「SQLを実行」を行います。
結果が表示されます。結果はCSVでダウンロードすることができます。
おわり
自然言語でDBに問い合わせを行う機能を紹介しました。導入してみて、自分でSQLを書くという作業がほぼなくなりました。参考になれば幸いです。