2023.03.31

ChatGPT × MySQL Shell:SQLをAIに考えてもらうプラグイン

OpenAI Chat completions APIを用いて,PingCAP社が提供する「Chat2Query」ライクな,AIによるSQL生成機能を持つMySQL Shellプラグインを実装するお話です。そこそこ良い感じのSQLを生成してくれるものができましたが,トークンが多くなりがちなので,その点は工夫が必要です。

こんにちは,S.T.です。巷で盛り上がっているChatGPTですが,OpenAIが提供するChat completions APIで同様の機能をアプリケーションから利用することが可能です。Pythonをはじめとするプログラミング言語のSDKから簡単にAPIを呼び出すことができるので,MySQL Shellに組み込んでみました。

1.Chat2Queryとは

実装をする前に,今回作成する機能と類似の機能を提供しているChat2Queryを軽く見ておきましょう。Chat2QueryはPingCAP社が提供する「自然言語からSQLを生成する」サービスです。現在は,同じくPingCAP社が提供するマネージドのNewSQLであるTiDB Cloudのブラウザ上で動作するクエリエディタで利用可能です。「–」からはじまるコメントでディレクションすると,それに応じたSQLが生成されます。日本語にも対応しています。

図1は,実際に生成したクエリとその実行結果です。自然言語によるディレクションに従って,適切なテーブルを選び,適切な条件でSELECTするSQLが生成されました。

図1:Chat2Queryで生成されたSQLと実行結果

また,図2のように「Window関数RANK()を使ったサンプルを見せて」といった,「データを問わないSQLの書き方」に関する質問をしても適切な回答を出してくれます。ただし,Window関数は若干苦手なようで,明示的にWindow関数を使うことを記述したり,プロンプトを英語にしたり,数回生成をやりなおしたりする必要がありました(おそらく,学習データにWindow関数を使ったSQLが少ないのでしょう)。

図2:Chat2QueryによるWindow関数のサンプル生成

Chat2QueryによるSQL生成で注目すべき点は,「(ユーザが)スキーマやテーブルの構造について一切触れずに」「SQLよりも“大幅に短い”自然言語によるディレクション」で適切なSQLを生成してくれる,という点です。プログラミング言語のコード生成では,仕様や入出力を正確にディレクションする必要がありますが,SQLでは入力や仕様に相当する「テーブルの構造を伝えるプロンプト」を自動生成することができるため,「出力してほしいもの」だけを伝えれば良いのです。この優位性から,SQLの自動生成は現段階でも割と実用的であると考えます。

※ ただし,RDBにクエリを投げるときは思考言語が自然言語ではなくSQLになっているので,脳内でSQL->自然言語の翻訳が発生する,という人間側の課題が残されています。

2.ChatGPTにSQLを考えてもらう

ChatGPTを使って,Chat2QueryのようにSQLの生成が可能かどうか,試してみましょう。基本的な方針は以下の通りです。

  • MySQLに精通したDBAのロールプレイをさせる
  • スキーマ,テーブル,カラムの情報をCSV形式で入力する(ひとまずINDEXやPARTITIONは考慮しない)
  • 説明は省略してSQLだけを回答してもらう

これらを考慮して,適当なサンプルのテーブルを仮定してプロンプトを作成し,ChatGPTに入力をすると,図3のような結果が得られました。

図3:ChatGPTによるSQL生成

CSVの入力からスキーマ名,テーブル名,カラム名,そしてカラムの意味をくみ取って,正しいSQLを生成してくれました。テーブルの選択も正しい上に,「ヨーロッパ」というキーワードに正しく反応し,EURのプライスカラムを使っています。少しいじわるをして,airplanesテーブルに「price」カラムを用意した上に,国名が入りそうな雰囲気の「country_of_origin」カラムを用意しておきましたが,騙されなかったようです(まあ,ヨーロッパは国名ではないですが)。nameの重複を考慮している点も,抜かりありません。

テーブルの情報をプロンプトに含めるため,プロンプトが非常に長くなる,という難点はありますが,ひとまずはこの方法で自然言語からSQLを生成することができそうです。

3.MySQL Shellとは

MySQL Shellは,MySQLのオフィシャルな製品として提供されている,高機能なMySQLクライアントです。「よく使うmysqlコマンド」の超高機能版です。通常のSQLインタフェースはもちろん,JavaScriptとPythonのインタプリタを備えており,ad hocなスクリプトを実行したり,プラグインで機能を拡張したりすることが可能です。インタプリタとMySQLサーバの通信は,通常のテキスト(SQL)ベースのプロトコルのほか,X Dev APIも利用することが可能です。

MySQL Shellが便利であることは間違いないのですが,SQLベースのインタフェースのほうが手に馴染んでいることもあり,積極的に活用されている方はまだ少数ではないでしょうか。私もまだまだ使いこなせていません。まずは,どのようなものなのかを軽く見てみましょう。図4に,MySQL ShellをPythonモードで起動して,適当なスクリプトを実行した画面を示します。

図4:MySQL Shellの実行例

この画面は,

mysqlsh --py -uuser -p

コマンドでlocalhostで起動しているmysql-serverに接続し,スキーマをリストアップしたものです。JavaScript,Pythonモードでは,あらかじめsessionやshellなどのオブジェクトが利用可能な状態になっており,これらを用いてMySQLを操作することが可能です。ちょっとした処理であれば,わざわざMySQLのコネクタをインストールしてSQLを使ってゴリゴリ処理を行うスクリプトを書くよりも,MySQL Shellで動作するスクリプトを書いたほうが楽,ということもあります。

MySQL Shellの機能は,プラグインで拡張することができます。プラグインは,sessionやshellといったオブジェクトと同様,MySQL Shellからは1つのオブジェクトとして見えます。例えば,オフィシャルのドキュメントで紹介されている例をプラグインとして読み込むと,図5のように利用することができます。この機能を利用して,SQLを自動生成するプラグインを作成してみましょう。

図5:MySQL Shellプラグインのサンプル

4.ChatGPT × MySQL Shell

SQLを自動生成するプラグインには,以下の要素が必要です。

  • ユーザからプロンプトを受け取る
  • スキーマ,テーブル,カラムの情報を接続中のMySQLから取得する
  • 上記の情報をあわせてプロンプトを構築する
  • OpenAIのAPIを呼び出し,結果を表示する

プロンプトは引数で受け取れば良いでしょう。カラムの情報は,INFORMATION_SCHEMAをクエリするのが良さそうです。プロンプトの構築は,C言語ならともかくPythonなら朝飯前です。OpenAIのAPIは,SDKから一発で呼び出すことが可能です。びっくりするほど簡単に作れそうです。今回はMySQL 8.0.32を使って実装してみます。

プラグイン自体は,オフィシャルのドキュメントで紹介されている例をベースに簡単に作ることができます。まずは,プラグインに引数を渡しましょう。add_extension_object_memberのparametersで,プラグインに必要な引数を指定します。今回は,クエリを生成したいMySQLにつながっているsessionと,プロンプトのstringを受け取ることにします。これで,「プラグイン名.generate(session, “プロンプト”)」という呼び出しができるようになります。

def generate(session, query):
    pass
# 略
shell.add_extension_object_member(\
    plugin_obj,\
    "generate",\
    generate,\
    {"brief": "Generate SQL",
     "parameters": [
        {"name": "session", "type": "object", "class": "Session", "brief": "session"},
        {"name": "query", "type": "string", "brief": "query string"}
    ]}\
)

カラムの情報を取るために,INFORMATION_SCHEMA.COLUMNSをSELECTします。sessionからのメソッドチェーンで簡単にクエリを実行できます。

i_s = session.get_schema("information_schema")
columns = i_s.get_table("COLUMNS")\
    .select("table_schema", "table_name", "column_name", "column_type")\
    .where(f"table_schema NOT IN ({','.join(SYS_SCHEMAS)})")\
    .execute()\
    .fetch_all()

MySQL ShellからOpenAIのSDKを利用するために,pipでインストールします。これは通常の手順と少々異なりますが,コマンド数回で簡単にインストールが可能です。

wget https://bootstrap.pypa.io/get-pip.py
mysqlsh --py -f get-pip.py
mysqlsh --pym pip install --user openai

OpenAIのChat completion APIの呼び出しは,もはやこの記事での解説は不要でしょう。プロンプトは,図3のものをそのまま使います。これらをまとめると,最終的に以下のようなコードになります。

from mysqlsh import mysqlx
import openai


SYS_SCHEMAS = list(map(lambda x: f"'{x}'", ["sys", "mysql", "information_schema", "performance_schema"]))

PROMPT_SYSTEM = "あなたはMySQLに精通したDBAであり,問い合わせに対して最適なSQLを提示することができます。"
PROMPT_TABLE = "次のようなテーブルがあるMySQLを仮定します。\n"
PROMPT_MAIN = "問い合わせは「{q}」です。問い合わせへの回答となるSQLのみを出力してください。説明や装飾は不要です。コードブロックで囲まないでください。SQLを出力できない場合は,何も出力しないでください。"

MODEL = "gpt-3.5-turbo"
openai.api_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

def generate(session, query):
    # INFORMATION_SCHEMA.COLUMNSからカラムの情報を取得する
    i_s = session.get_schema("information_schema")
    columns = i_s.get_table("COLUMNS")\
        .select("table_schema", "table_name", "column_name", "column_type")\
        .where(f"table_schema NOT IN ({','.join(SYS_SCHEMAS)})")\
        .execute()\
        .fetch_all()

    # カラムの情報をCSV形式にしてプロンプトに含める
    csv = "table_schema,table_name,column_name,column_type\n" + "\n".join(list(map(lambda x: ",".join(x), columns)))
    messages = [
        {"role": "system", "content": PROMPT_SYSTEM},
        {"role": "user", "content": PROMPT_TABLE + csv},
        {"role": "user", "content": PROMPT_MAIN.format(q=query)}
    ]

    # APIを呼び出して結果が正常に取得できていたら出力する
    completion = openai.ChatCompletion.create(
        model=MODEL,
        messages=messages
    )
    if ("choices" in completion and len(completion["choices"]) > 0 and
        "message" in completion["choices"][0] and "content" in completion["choices"][0]["message"]):
        print(completion["choices"][0]["message"]["content"])


plugin_obj = shell.create_extension_object()

shell.add_extension_object_member(\
    plugin_obj,\
    "generate",\
    generate,\
    {"brief": "Generate SQL",
     "parameters": [
        {"name": "session", "type": "object", "class": "Session", "brief": "session"},
        {"name": "query", "type": "string", "brief": "query string"}
    ]}\
)

shell.register_global(\
    "mc2q",\
    plugin_obj,\
    {"brief": "my_Chat2Query"}\
)

今回作成したプラグインは,Chat2Queryに敬意を込めて,「my_Chat2Query」と命名しました。このコードを「~/.mysqlsh/plugins/my_chat2query/init.py」として配置すれば,準備完了です(API_KEYの設定をお忘れなく)。

早速使ってみましょう。オフィシャルのExample Databaseのworldsをインポートして,このデータベースに対するクエリを生成してもらいます。worldsデータベースは,国,都市,人口,言語などの情報が格納されています。図5にmy_Chat2Queryで生成したSQLを示します。

図5:MySQL Shellプラグインのサンプル

シンプルなクエリも,2つのJOINが必要なちょっと複雑なクエリも,完璧にこなしてくれました。注目ポイントは,レコードの情報がないのに,それっぽいWHERE句を付けてくれている点です。Language=’English’だけでなく,enum(‘T’, ‘F’)もきちんと理解していて偉いですね。どちらのSQLも,引数で入力したプロンプトよりも圧倒的に長いため,人間の作業量を減らすことができた,と言えるでしょう。今回のデータベースの規模は3テーブル,合計24カラムで,total_tokensは500弱でした。

生成されたSQLを実際に実行すると,図6のような結果が得られます。

図6:生成されたSQLの実行結果

良さそうです。ツッコミどころは,私がMySQL Shellを使わずにクエリを実行していることくらいしかありません。Chat2Queryほどの性能はないかもしれませんが,なかなか使えそうなものができたのではないでしょうか?

5.まとめ

今回はOpenAI Chat completions APIを用いてSQLを自動生成するMySQL Shellプラグインを実装しました。自分で記述したコード量は非常に少ないですが,生成されるSQLは十分使えそうなレベルに達しています。人間でも簡単にかける程度の複雑さのものを,人間よりも高速に生成してくれる,という点に価値があるのではないでしょうか。

一方,今回のようなシンプルな実装の場合プロンプトが長くなるため,大規模なデータベースではプロンプトの長さの制限にひっかかったり,課金額が大きくなったりする,という問題点もあります。これらの問題をクリアすれば,頼れるアシスタントプラグインになりそうです。

次世代システム研究室では,グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方,次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら,ぜひ募集職種一覧からご応募をお願いします。

参考

PingCAP – ChatGPT搭載の革新的なSQLジェネレータ「Chat2Query」とは
lefred’s blog – MySQL Shell and extra Python modules
MySQL Shell API 8.0.32
MySQL – 7.2.4 MySQL Shell 拡張オブジェクトの例
Other MySQL Documentation (Example Databases)
OpenAI API Documentation

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事