上級 13 分SQL

ローカルText-to-SQL:言語でデータベースに問い合わせる naturel

ローカルLLMを使ったtext-to-SQLでは、フランス語で質問(「先月の地域別の売上高はいくらですか?」)をして、PostgreSQLまたはMySQLで実行できるSQLクエリを得られます。その際、スキーマもデータも自社のインフラの外には出ません。本ガイドでは、実際の仕組みを解説します。スキーマをコンテキストに組み込み、Ollamaを使ってPythonパイプラインを構築し、特に重要な保護策(読み取り専用、検証、制限)を設けます。これらの保護策なしに、text-to-SQLを本番環境に導入することはできません。

著者 Mohamed Meguedmi·更新 2026-08-27·Windows・macOS・Linuxでテスト済み

#ローカルLLMでText-to-SQLを行う理由

クラウドベースのテキストからSQLへの変換ソリューション(BIアシスタント、データウェアハウスコピロート)は、お客様のスキーマ(テーブル名、カラム名、場合によっては行のサンプル)を第三者サーバーに送信します。顧客データベース、人事管理、財務データなどに該当する場合は、通常、これは受け入れがたいです。スキーマだけでも事業の構造が明らかになり、サンプルデータには個人情報が含まれるためです。

ローカルLLMはこの問題を根本から解決します。モデルは Ollama を通じてあなたのマシン上で動作し、スキーマはローカルメモリに保持され、生成されたクエリはインターネットを介さずにあなたのデータベースに対して実行されます。また、利用料は無料であり、APIの帯域幅制限にも依存しません。

プライバシー
スキーマとデータがネットワークの外に出ることはありません。そのため、GDPR(RGPD)への対応や営業秘密の保護が容易になります。
コスト
リクエストごとの費用はかかりません。データアナリストは、課金されることなく何百回も試行を繰り返せます。
アクセシビリティ
SQLを知らない業務ユーザーが、自然言語でデータベースに問い合わせます。
制御
モデル、プロンプト、安全策はご自身で決められます。遠隔のブラックボックスに頼る必要はありません。
!
テキストからSQLへの変換は魔法ではありません
LLMが生成するのはもっともらしいSQLであり、正しさが保証されたSQLではありません。複雑なスキーマ(複数の結合、曖昧な列など)では、依然として一定の割合で誤りが生じます。出力は検証が必要な提案として扱い、決して正しいと無条件に信頼しないでください。特に、技術に詳しくない人がその出力に基づいて意思決定する場合は注意が必要です。

#実際にどう機能するか

ローカルコパイロットキット

このガイドでモデルの導入まで、キットでエディタ内でコードを書くコパイロットの導入まで進められます。

  • 永久に利用できるオンラインスペース
  • PDF + ファイル
  • 生涯アップデート

LLMによるText to SQLの原理は3つの段階で説明できます。まず、モデルにデータベースのスキーマ(関連するテーブルのDDL)を説明します。次に、ユーザーの質問と厳格な指示(ターゲットのダイアレクトに対してSQLクエリのみを生成すること)を伝えます。最後に、クエリを取得し、検証して、読み取り専用で実行します。

  1. 01
    スキーマのイントロスペクション
    データベースからテーブルの構造(カラム、型、キー)を抽出します。データベースとの同期を保つため、手作業ではなく自動で行います。
  2. 02
    プロンプトの構築
    SQLの方言、関連するスキーマ、ルール(SELECTのみ、LIMIT必須、コメント禁止)を含むシステムプロンプトを組み立てます。
  3. 03
    生成
    ローカルLLMがクエリを返します。そのクエリを整えます(Markdownの ```sql などの区切りがあれば削除します)。
  4. 04
    検証+実行
    SQL文がSELECTであることを確認し、読み取り専用のデータベースロールで実行して、結果の行を返します。

#前提条件

Ollama インストール済み
デーモンはhttp://localhost:11434で待ち受けている必要があります。「ollama ps」で確認してください。
十分な能力を持つモデル
最近のコードモデル(Qwen3-Coder 30B-A3B、Devstral 24B)は、小規模な一般用途モデルよりもSQLでの結果がはるかに優れています(モデルセクションを参照)。
Python 3.10+
対応するデータベースクライアントも必要です:PostgreSQLにはpsycopg2-binary、MySQLにはPyMySQL。
データベースへの読み取り専用アクセス
理想的なのは、SELECTしか実行できない専用のSQLロールです。これが最も重要な安全策です。
ターミナル
# Récupérer un modèle adapté au SQL
ollama pull qwen3-coder:30b

# Dépendances Python
pip install ollama psycopg2-binary sqlparse

#データベースのスキーマをモデルに渡す

この段階は、結果の品質の 80 % を決定します。モデルが正確なクエリを生成できるのは、テーブルとカラムの正確な名前、それらの型、およびそれらの間の関係を把握している場合のみです。アプローチは二つあります。生の DDL を貼り付けるか、データベースをインスペクトしてコンパクトな説明を構築するかです。

小規模なデータベース(20テーブル未満)であれば、すべてを注入できます。それを超えると、スキーマが有用なコンテキストを超えてモデルを埋没させてしまうため、質問に関連するテーブルを選択する必要があります(最初の検索パスやビジネスマッピングを通じて)。以下は、LLMが読み取り可能なスキーマを生成するPostgreSQLのインスペクションです。

schema.py
import psycopg2

def get_schema(conn):
    """Retourne le schéma sous forme de CREATE TABLE simplifiés."""
    query = """
        SELECT table_name, column_name, data_type
        FROM information_schema.columns
        WHERE table_schema = 'public'
        ORDER BY table_name, ordinal_position;
    """
    tables = {}
    with conn.cursor() as cur:
        cur.execute(query)
        for table, col, dtype in cur.fetchall():
            tables.setdefault(table, []).append(f"{col} {dtype}")

    lines = []
    for table, cols in tables.items():
        cols_str = ", ".join(cols)
        lines.append(f"TABLE {table} ({cols_str});")
    return "\n".join(lines)
→
業務上の意味を説明するコメントを追加してください
「ca_ht」という列名だけでは、モデルにとって意味が曖昧です。スキーマに「ca_ht(税抜売上高、ユーロ単位)」のような注釈を追加してください。この数語だけで、列の選択ミスを大幅に減らせます。PostgreSQLでは、COMMENT ON COLUMNで設定したコメントをinformation_schemaやpg_descriptionから取得できます。

#Ollamaを使った完全なPythonパイプライン

以下は、最小限ながら実際に動作するパイプラインです:スキーマ → プロンプト → 生成 → クリーンアップ → 検証 → 実行。Ollamaの公式Pythonクライアントと、読み取り専用のデータベースロールを使います。

text_to_sql.py
import re
import ollama
import psycopg2
import sqlparse

MODEL = "qwen3-coder:30b"

SYSTEM_PROMPT = """Tu es un expert PostgreSQL. Génère UNE seule requête SQL
qui répond à la question de l'utilisateur, en respectant ces règles :
- Uniquement des requêtes SELECT (jamais INSERT/UPDATE/DELETE/DROP).
- Utilise exactement les noms de tables et colonnes du schéma fourni.
- Ajoute toujours LIMIT 100 si la question ne précise pas de limite.
- Réponds UNIQUEMENT avec le SQL, sans explication ni balise Markdown.

Schéma de la base :
{schema}"""

def generate_sql(question, schema):
    resp = ollama.chat(
        model=MODEL,
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT.format(schema=schema)},
            {"role": "user", "content": question},
        ],
        options={"temperature": 0},  # déterminisme : crucial pour du SQL
    )
    return clean_sql(resp["message"]["content"])

def clean_sql(raw):
    # Retire les fences Markdown ```sql ... ``` si le modèle en ajoute
    raw = re.sub(r"```(?:sql)?", "", raw).strip()
    return raw.rstrip(";") + ";"

実行部分では、検証とデータベースへの呼び出しを意図的に分離しています。カーソルを開く前の段階で、単一のSELECT文以外はすべて拒否します。

text_to_sql.py(続き)
def is_read_only(sql):
    statements = sqlparse.parse(sql)
    if len(statements) != 1:
        return False  # une seule requête, pas d'empilement
    stmt = statements[0]
    if stmt.get_type() != "SELECT":
        return False
    forbidden = ("insert", "update", "delete", "drop",
                 "alter", "truncate", "grant", "create")
    lowered = sql.lower()
    return not any(kw in lowered for kw in forbidden)

def run_query(sql):
    if not is_read_only(sql):
        raise ValueError(f"Requête refusée (non lecture seule) : {sql}")
    # Rôle 'readonly' : ne dispose QUE du privilège SELECT côté base
    conn = psycopg2.connect(
        dbname="analytics", user="readonly",
        password="...", host="localhost",
    )
    with conn.cursor() as cur:
        cur.execute("SET statement_timeout = '5s';")  # anti-requête folle
        cur.execute(sql)
        cols = [d[0] for d in cur.description]
        rows = cur.fetchall()
    conn.close()
    return cols, rows

if __name__ == "__main__":
    from schema import get_schema
    ro = psycopg2.connect(dbname="analytics", user="readonly",
                          password="...", host="localhost")
    schema = get_schema(ro)
    question = "Combien de commandes par mois en 2025 ?"
    sql = generate_sql(question, schema)
    print("SQL généré :", sql)
    cols, rows = run_query(sql)
    print(cols)
    for r in rows:
        print(r)
i
temperature = 0
テキストからSQLに変換する場合は、常に温度を0に設定してください。求めているのは創造性ではなく、最も確率が高く、再現性のあるクエリです。温度を高くすると、カラムや結合にばらつきが生じ、実行が失敗します。

#生成されたSQLの信頼性とセキュリティを確保する

このセクションが、デモと実際の運用を分けるポイントです。LLMは、求められれば破壊的なクエリを生成する可能性があります。また、質問に仕込まれたインジェクションによって、意図せずそうしたクエリを生成することもあります。防御をプロンプトだけに頼ってはいけません。データベース側で多層的な防御を施す必要があります。

  1. 01
    データベースの読み取り専用ロール(主な防御策)
    SELECT権限だけを持つSQLロールを作成してください。モデルがDROP TABLEを生成しても、データベースは拒否します。これが唯一、本当に信頼できる安全策です。「GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;」だけを実行し、それ以外の権限は付与しないでください。
  2. 02
    アプリケーション側での検証
    実行前にsqlparseでSQLを解析し、単一のSELECT文以外はすべて拒否してください。データベース側のロールと合わせて、二重の防御になります。
  3. 03
    リクエストのタイムアウト
    SET statement_timeoutは、不適切なクエリ(数百万行に対する直積)がデータベースを過負荷に陥らせるのを防ぎます。
  4. 04
    LIMIT強制
    プロンプト側とコード側の両方でLIMITを必ず設定し、テーブル全体をメモリに読み込むことが決してないようにしてください。
  5. 05
    修正ループ
    SQL実行にエラーが発生した場合、そのエラーメッセージをモデルに返し、修正されたクエリを要求してください(1~2回まで)。
PostgreSQLの読み取り専用ロール
-- À exécuter une fois par un admin
CREATE ROLE readonly WITH LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE analytics TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
-- Les tables créées plus tard héritent aussi du SELECT
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO readonly;
!
質問文をSQLに直接埋め込まないこと
ユーザーの質問はLLMのプロンプトに入れ、クエリに文字列として連結することは決してありません。実行するSQLはモデルが生成したもので、検証したうえで、ユーザーのパラメータを埋め込まずにcur.execute(sql)でそのまま実行します。そのため、従来のインジェクション対策の焦点は、読み取り専用であることの検証に移ります。だからこそ、データベースのロールが重要です。

修正ループによって成功率は大幅に向上します。多くのエラーは、列名のわずかな間違いやSQL方言固有の日付関数といった単純なものです。モデルは、エンジンのエラーメッセージを確認できれば、2回目の試行でそれらを修正します。

修正ループ
def answer(question, schema, max_retries=2):
    sql = generate_sql(question, schema)
    for attempt in range(max_retries + 1):
        try:
            return sql, run_query(sql)
        except Exception as e:
            if attempt == max_retries:
                raise
            # On renvoie l'erreur au modèle pour correction
            fix_prompt = (
                f"La requête suivante a échoué :\n{sql}\n\n"
                f"Erreur PostgreSQL : {e}\n"
                f"Corrige la requête. SQL uniquement."
            )
            resp = ollama.chat(model=MODEL, messages=[
                {"role": "system", "content": SYSTEM_PROMPT.format(schema=schema)},
                {"role": "user", "content": fix_prompt},
            ], options={"temperature": 0})
            sql = clean_sql(resp["message"]["content"])

#SQLに優れたローカルモデルはどれですか

SQLはコードを書くタスクです。「coder」系のコード特化モデルは、同じ規模の汎用モデルを大きく上回ります。2026年には、Qwen3-Coder 30B-A3Bのような新しいコードモデルが状況を一変させます。2B〜8Bの小型モデルは単純なクエリなら何とか作れますが、複数の結合やウィンドウ集計が必要になると失敗します。(SQL用として長く紹介されてきたCodestral 22Bは、現在では本番利用不可のライセンスになっているため、企業では選択肢から外すべきです。)

Qwen3-Coder 30B-A3B
2026年の標準的な選択肢。アクティブパラメータ数が30億のコード向けMoEモデルです。高速で、大規模なスキーマに対応できる256kのコンテキストを備えています。Q4では約19GBで、RTX 4090または最近のMacで動作します。ライセンスはApache 2.0です。
Devstral 24B
Mistral AI が開発したコード特化モデル(Apache 2.0)で、Q4 では約14 GB。RTX 4080 のような16 GB のカードに収まります。比較的控えめな構成のワークステーションで SQL を扱うには、最もバランスのよい選択です。
Qwen 3.8 27B
比較的新しい汎用モデルで、複雑な結合についても堅実な推論ができます(約18GB、262kのコンテキスト、視覚機能)。推論設定を「low」に変更してください。SQLのように高度に構造化されたタスクでは、デフォルト設定だと考えすぎる傾向があります。
小型モデル 2B–8B (Qwen 3.5 4B, Granite 4.2 8B)
非常に単純なスキーマと、単純明快な質問の場合に限ります。データベースに複雑な関係がある場合は避けてください。
→
Q4_K_M量子化
テキストからSQLを生成する場合、Q4_K_Mは品質とVRAM使用量のバランスが最も優れています。この構造化されたタスクでは、Q8と比べた精度の低下は無視できる程度です。一方、VRAMを節約することで、より大きなモデルを使えるようになります。SQLの正確さには、量子化よりもモデルのサイズのほうがはるかに大きく影響します。

#トラブルシューティング

モデルが存在しないカラムを捏造する
スキーマが不完全または大きすぎます。関連するテーブルに絞り、曖昧なカラムに対してビジネス上のコメントを追加してください。
SQLの前後にテキストが付く回答
「SQLのみ、説明は一切不要」という指示を強め、clean_sqlでMarkdownのコードフェンスを除去する処理は残してください。
日付関数のエラー
システムプロンプトに方言を明示してください(PostgreSQLとMySQLではDATE_TRUNC、YEAR()などに違いがあります)。補正ループが残りを補い、修正します。
実行に時間がかかる、またはタイムアウトするクエリ
statement_timeout が正常に機能しています。大きなテーブルを扱う場合は、プロンプトに「常に適切な日付範囲で絞り込む」と追加してください。
Ollamaの「Connection refused」
デーモンが起動していません。「ollama ps」で確認し、サービスが http://localhost:11434 で接続を待ち受けているかも確認してください。

#さらに詳しく

text-to-SQLは、このサイトですでに解説している複数の構成要素を再利用します。以下のガイドは、このガイドの内容をさらに掘り下げます:

REST API を介して Python アプリケーションに Ollama を統合する
このパイプラインをFastAPIのAPI経由で公開し、ストリーミングとJSONモードを扱うために。
OllamaでのFunction callingと構造化JSON出力
テキストを後処理して整えるのではなく、出力(クエリ+説明)の構造を確実に保証するための別の方法です。
量子化の選択(Q4、Q5、Q8、FP16)
SQL用モデルのサイズと、お使いのグラフィックカードで利用可能なVRAMとのバランスを取るため。
このガイドは役に立ちましたか?

ご意見、誤りのご指摘、補足はありますか?ぜひお知らせください。皆さんにとってより良いガイドにするために役立ちます。