自然文をSQLに変換する技術(Text-to-SQL)というのは LLM が登場する以前から存在しているらしい。 LLMを使うことでこれが業務に耐えられる品質になったとかなんとか(よくしらない)。 実際に Claude Code を使ってこれを試してみると確かにこれは機能する。 大量のテーブルがあるデータベースに対して、スキーマとその注釈(コラム名への日本語名マップのような情報)を用意して Claude Code に、「2025年1月から10月までの売上を知りたい」のように自然文で問いかけるとSQLを生成してくれる。
もちろん次のような基本のプロンプトは事前に与えておく:
schema.sql にデータベースの情報があるのでそれを把握した上で質問に答えてください。
これから知りたいことを質問しますが、それを得るための SQL ステートメントを教えてください。
schema.sql は sqlite であれば次のようなコマンドで書き出しできる:
sqlite3 database.db .schema > schema.sql
本題から外れますが、もし Postgres でスキーマを取り出すにはこんな感じです。 pokemondb というデータベースの pokemons テーブルのスキーマを書き出す例:
pg_dump -s -t public.pokemons pokemondb > pokemons_schema.sql
https://github.com/kotofurumiya/pokemon_dataに ポケモンデータベースを JSON 形式で公開してくださっている方があったので、これを使います。
なお、このページで説明するコードの詳細はそのプロジェクトをフォークしたこちら https://github.com/mindboard/pokemon_data にあります。
動作環境:
$ deno --version
deno 2.4.1 (stable, release, x86_64-unknown-linux-gnu)
v8 13.7.152.6-rusty
typescript 5.8.3
まず https://github.com/kotofurumiya/pokemon_dataの ./data/pokemon_data.json を JSON.parse する:
// main.ts
const pokemonData = await Deno.readTextFile("./data/pokemon_data.json")
const pokemons = JSON.parse(pokemonData)
番号とポケモン名だけを取得することにしましょう。
わかりやすいように Pokemon 型を定義:
type Pokemon = {
pokemonNo: number
name: string
}
JSON.parse したポケモンデータを Pokemon[] へ変換して、10件だけ console.log する:
const pokemonObjects: Pokemon[] = pokemons.map((pokemon)=>{
return {
pokemonNo: pokemon.no,
name: pokemon.name
}
})
console.log( take(pokemonObjects, 10) )
take はこれ:
const take = <T>(l: T[], n: number): T[] => { return l.slice(0, n) }
実行:
$ deno --allow-read main.ts
[
{ pokemonNo: 1, name: "フシギダネ" },
{ pokemonNo: 2, name: "フシギソウ" },
{ pokemonNo: 3, name: "フシギバナ" },
{ pokemonNo: 3, name: "メガフシギバナ" },
{ pokemonNo: 4, name: "ヒトカゲ" },
{ pokemonNo: 5, name: "リザード" },
{ pokemonNo: 6, name: "リザードン" },
{ pokemonNo: 6, name: "メガリザードンX" },
{ pokemonNo: 6, name: "メガリザードンY" },
{ pokemonNo: 7, name: "ゼニガメ" }
]
sqlite データベースを構築します。
テーブル pokemons を用意:
import { DatabaseSync } from "node:sqlite"
const db = new DatabaseSync("./pokemon.db")
db.exec(`
CREATE TABLE IF NOT EXISTS pokemons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pokemonNo INTEGER,
name TEXT
);
`)
このテーブルにポケモンデータを入れるには次のようにします:
const insertStmtForPokemons = db.prepare(`
INSERT INTO pokemons (
pokemonNo,
name
) VALUES (?,?);
`)
pokemonObjects.forEach((it)=>{
const row = insertStmtForPokemons.run(
it.pokemonNo,
it.name)
console.log(row)
})
実行します。
$ deno --allow-read --allow-write main.ts
{ lastInsertRowid: 1, changes: 1 }
{ lastInsertRowid: 2, changes: 1 }
{ lastInsertRowid: 3, changes: 1 }
{ lastInsertRowid: 4, changes: 1 }
{ lastInsertRowid: 5, changes: 1 }
...
次に構築したデータベースを使ってデータ問い合わせをしてみます。
// query.ts
import { DatabaseSync } from "node:sqlite"
const db = new DatabaseSync("./pokemon.db")
const rows = db.prepare('SELECT * FROM pokemons LIMIT 10').all()
console.log(rows)
db.close()
実行します:
$ deno --allow-read --allow-write query.ts
[
[Object: null prototype] { id: 1, pokemonNo: 1, name: "フシギダネ" },
[Object: null prototype] { id: 2, pokemonNo: 2, name: "フシギソウ" },
[Object: null prototype] { id: 3, pokemonNo: 3, name: "フシギバナ" },
[Object: null prototype] { id: 4, pokemonNo: 3, name: "メガフシギバナ" },
[Object: null prototype] { id: 5, pokemonNo: 4, name: "ヒトカゲ" },
[Object: null prototype] { id: 6, pokemonNo: 5, name: "リザード" },
[Object: null prototype] { id: 7, pokemonNo: 6, name: "リザードン" },
[Object: null prototype] { id: 8, pokemonNo: 6, name: "メガリザードンX" },
[Object: null prototype] { id: 9, pokemonNo: 6, name: "メガリザードンY" },
[Object: null prototype] { id: 10, pokemonNo: 7, name: "ゼニガメ" }
]
できました。
ここまでで作成した pokemons テーブルだけの状態では AI に SQL 生成してもらうまでもないので、 ここからは、このプロジェクト https://github.com/mindboard/pokemon_data の main.ts を実行してデータベースを構築したとして話をすすめます。
次のようにして schema.sql を書き出します。
$ sqlite3 pokemon.db .schema > schema.sql
pokemon.db は main.ts を実行した結果生成されたデータベースファイルです。
書き出された schema.sql :
CREATE TABLE pokemons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pokemonNo INTEGER,
name TEXT,
form TEXT,
isMegaEvolution INTEGER,
hp INTEGER,
attack INTEGER,
defence INTEGER,
spAttack INTEGER,
spDefence INTEGER,
speed INTEGER
);
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE pokemonTypes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pokemonId INTEGER,
name TEXT
);
CREATE TABLE abilities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pokemonId INTEGER,
name TEXT
);
CREATE TABLE hiddenAbilities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pokemonId INTEGER,
name TEXT
);
この schema.sql をカレントディクトリにおいた状態で claude を(Claude Code)を実行します。
プロンプトとして次を与えます:
> sqlite で作成したデータベーススキーマは ./schema.sql です。ヒットポイントの高い順に10のポケモンを知りたい。SQL :
次のような回答がありました。 ( Claude Sonnet 4.5 を使用しています。)
⏺ データベーススキーマを確認して、適切なSQLクエリを作成します。
⏺ Read(schema.sql)
⎿ Read 30 lines
⏺ スキーマを確認しました。pokemonsテーブルにhpカラムがあります。ヒットポイントの高い順に10のポケモンを取得するSQLクエリは以下の通りです
:
SELECT name, form, hp
FROM pokemons
ORDER BY hp DESC
LIMIT 10;
もしポケモンの番号も含めたい場合は:
SELECT pokemonNo, name, form, hp
FROM pokemons
ORDER BY hp DESC
LIMIT 10;
このクエリは:
- hpの値で降順(DESC)にソート
- 上位10件のみを取得(LIMIT 10)
コマンドラインから sqlite3 コマンドを使って実際にSQLを実行してみます:
$ sqlite3 pokemon.db
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> SELECT pokemonNo, name, form, hp
...> FROM pokemons
...> ORDER BY hp DESC
...> LIMIT 10;
242|ハピナス||255
113|ラッキー||250
799|アクジキング||223
718|ジガルデ|パーフェクトフォルム|216
202|ソーナンス||190
321|ホエルオー||170
594|ママンボウ||165
143|カビゴン||160
289|ケッキング||150
426|フワライド||150
ヒットポイントの高い順に 10のポケモンを取り出すことができました。
Claude Sonnet 4.5 にとって簡単すぎる問題だったようです。 次に複数のテーブルを駆使しないと算出できないことたずねてみます。
> くさタイプに限定して hp の高い順に上位10のポケモンを出力したい
⏺ くさタイプのポケモンに絞り込むには、pokemonTypesテーブルとJOINする必要があります。SQLクエリは以下の通りです:
SELECT p.pokemonNo, p.name, p.form, p.hp
FROM pokemons p
INNER JOIN pokemonTypes pt ON p.id = pt.pokemonId
WHERE pt.name = 'くさ'
ORDER BY p.hp DESC
LIMIT 10;
もしタイプ名が英語で格納されている場合は:
SELECT p.pokemonNo, p.name, p.form, p.hp
FROM pokemons p
INNER JOIN pokemonTypes pt ON p.id = pt.pokemonId
WHERE pt.name = 'Grass'
ORDER BY p.hp DESC
LIMIT 10;
このクエリは:
- pokemonsテーブルとpokemonTypesテーブルをpokemonIdでJOIN
- pokemonTypes.nameが'くさ'(または'Grass')のレコードに絞り込み
- hpの降順でソート
- 上位10件を取得
生成されたSQLが機能するか確認します:
sqlite> SELECT p.pokemonNo, p.name, p.form, p.hp
...> FROM pokemons p
...> INNER JOIN pokemonTypes pt ON p.id = pt.pokemonId
...> WHERE pt.name = 'くさ'
...> ORDER BY p.hp DESC
...> LIMIT 10;
673|ゴーゴート||123
591|モロバレル||114
251|セレビィ||100
465|モジャンボ||100
492|シェイミ|ランドフォルム|100
492|シェイミ|スカイフォルム|100
357|トロピウス||99
103|ナッシー||95
103|ナッシー|アローラのすがた|95
389|ドダイトス||95
できました。
「くさ」タイプかどうか確認するため、出力に「タイプ」列を追加してもらいましょう。 プロンプトを入れます:
> タイプもあわせて出力できますか?
次のSQLが生成されました:
⏺ タイプも含めて出力する場合、ポケモンは複数のタイプを持つことがあるので、GROUP_CONCATを使ってタイプをまとめます:
SELECT p.pokemonNo, p.name, p.form, GROUP_CONCAT(pt.name, ', ') AS types, p.hp
FROM pokemons p
INNER JOIN pokemonTypes pt ON p.id = pt.pokemonId
WHERE p.id IN (
SELECT pokemonId
FROM pokemonTypes
WHERE name = 'くさ'
)
GROUP BY p.id, p.pokemonNo, p.name, p.form, p.hp
ORDER BY p.hp DESC
LIMIT 10;
実行してみます。
sqlite> SELECT p.pokemonNo, p.name, p.form, GROUP_CONCAT(pt.name, ', ') AS types, p.hp
...> FROM pokemons p
...> INNER JOIN pokemonTypes pt ON p.id = pt.pokemonId
...> WHERE p.id IN (
(x1...> SELECT pokemonId
(x1...> FROM pokemonTypes
(x1...> WHERE name = 'くさ'
(x1...> )
...> GROUP BY p.id, p.pokemonNo, p.name, p.form, p.hp
...> ORDER BY p.hp DESC
...> LIMIT 10;
673|ゴーゴート||くさ|123
591|モロバレル||くさ, どく|114
251|セレビィ||エスパー, くさ|100
465|モジャンボ||くさ|100
492|シェイミ|ランドフォルム|くさ|100
492|シェイミ|スカイフォルム|くさ, ひこう|100
357|トロピウス||くさ, ひこう|99
103|ナッシー||くさ, エスパー|95
103|ナッシー|アローラのすがた|くさ, ドラゴン|95
389|ドダイトス||くさ, じめん|95
複数のタイプをもつポケモンがあるので GROUP_CONCAT でカンマ区切りにしていますね・・・
業務で使用するもっと複雑なデータベースでも試しましたが、この場合と同じく機能しました。 スキーマしか見せていないので Claude で判断できない場合(今回の例では、タイプ名が日本語の「くさ」なのか英語の「Grass」なのか)があるので、 そのあたりは前提として Claude に情報提供しておく(README.md に書いておくなど)必要があります。 データベース自体も Claude に操作させて、そのあたりもAIが自力解決できるように出来る気がしますが、 業務用の場合データをAIに見せるのは憚られる場合がある。