Home About Contact
SQLite , Pokémon , Deno , Claude

Claude Code で 自然文からSQLを生成 Text-to-SQL

自然文を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: "ゼニガメ" }
]

できました。

Claude Code で SQL を生成

ここまでで作成した 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に見せるのは憚られる場合がある。