OllamaとText-to-SQLでPostgreSQLを自然言語で操作する方法|PythonとローカルLLMでSQLを自動生成して業務データを安全に検索する手順

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)ローカルLLM > OllamaとText-to-SQLでPostgreSQLを自然言語で操作する方法|PythonとローカルLLMでSQLを自動生成して業務データを安全に検索する手順
「社内の業務DBにAIで問い合わせたいが、クラウドサービスに業務データを送るのは社のポリシー上NG」
「RAGやpgvectorを試したが、整備済みのテーブルに対しては、ベクター検索よりSQLで直接クエリする方が精度が高い」

そんな場面で使えるのが、ローカルLLMを使ったText-to-SQLです。この記事では、OllamaとPythonを組み合わせ、日本語の質問をPostgreSQLのSELECT文に変換して業務データを検索するパイプラインを構築する手順を解説します。
スキーマの動的取得からOllamaへのプロンプト設計、読み取り専用ユーザーによる安全な実行、SQLインジェクション対策まで、Ubuntu Server上でそのまま動かせる実装を一通りカバーします。社内にPostgreSQLが既にある場合、この構成はRAGとは別の有力な選択肢になります。

この記事のポイント

・OllamaのPOST /api/generateに「CREATE TABLEスキーマ+質問」を渡すとSQL文が得られる
・information_schemaでスキーマを動的取得すれば、テーブル追加後もプロンプト更新不要になる
・生成SQLは読み取り専用ユーザー+SELECTのみ許可フィルタで安全に実行できる
・few-shot例をプロンプトに追加するとJOINや集計クエリの生成精度が大幅に向上する


OllamaとText-to-SQLでPostgreSQLを自然言語で操作する方法|PythonとローカルLLMでSQLを自動生成して業務データを安全に検索する手順

「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Text-to-SQLとRAGの違い|どちらを使うべき場面の判断基準

Text-to-SQLとRAGは、どちらも「質問に答える」仕組みですが、向いている場面が異なります。
RAGは非構造化テキスト(PDF・マニュアル・議事録)を対象とし、ベクター検索で関連チャンクを取得してLLMに渡します。一方、Text-to-SQLは整形されたリレーショナルDBが前提で、質問をSQLに変換してDBに直接クエリします。

・業務DBに月次売上・顧客情報・在庫数量などの構造化データがある ~ Text-to-SQLが適している
・社内マニュアル・FAQドキュメント・ログ集などのテキスト資料がある ~ RAGが適している

両者は排他ではなく、構造化データにはText-to-SQL、非構造化データにはRAGと使い分けるのが実務的な判断です。
Ollamaの構築がまだの場合は、Ubuntu ServerでローカルLLMを構築する方法で環境を整えてから戻ってきてください。

前提環境の確認|PostgreSQL・Ollama・Pythonのバージョン要件

この記事の手順は以下の環境を前提とします。
・Ubuntu Server 22.04 LTS または 24.04 LTS
・PostgreSQL 14 以上
・Ollama 0.3.0 以降(/api/generate が安定動作するバージョン)
・Python 3.10 以上

Ollamaが動作しているかを事前に確認します。

# Ollamaのサービス状態を確認 $ systemctl status ollama # llama3.3またはmistralのモデルがあるか確認 $ ollama list NAME ID SIZE MODIFIED llama3.3:70b-instruct-q4_0 ab12cd34ef56 43 GB 3 days ago mistral:7b-instruct-q4_0 ff11aa22bb33 4.1 GB 5 days ago

Text-to-SQLはモデルの推論能力に依存するため、精度を優先するなら`llama3.3:70b-instruct-q4_0`、速度優先なら`mistral:7b-instruct-q4_0`が選択肢になります。
モデルの使い分け基準についてはローカルLLMのモデルを比較する方法も参照してください。

PostgreSQLにサンプルデータベースを用意する手順

1. サンプルテーブルとデータの作成

今回は社内の受注管理DBを想定したサンプルを使います。psqlでデータベースとテーブルを作成します。

# postgres ユーザーでpsqlを起動 $ sudo -u postgres psql -- データベースとロールの作成 postgres=# CREATE DATABASE bizdb; postgres=# CREATE USER bizadmin WITH PASSWORD 'admin_pw'; postgres=# GRANT ALL PRIVILEGES ON DATABASE bizdb TO bizadmin; postgres=# CREATE USER bizreader WITH PASSWORD 'reader_pw'; postgres=# \c bizdb bizdb=# GRANT CONNECT ON DATABASE bizdb TO bizreader; -- サンプルテーブルの作成 bizdb=# CREATE TABLE customers ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, prefecture VARCHAR(20), created_at DATE DEFAULT CURRENT_DATE ); bizdb=# CREATE TABLE orders ( id SERIAL PRIMARY KEY, customer_id INTEGER REFERENCES customers(id), product_name VARCHAR(100) NOT NULL, amount INTEGER NOT NULL, order_date DATE NOT NULL ); -- サンプルデータの投入 bizdb=# INSERT INTO customers (name, prefecture) VALUES ('株式会社アルファ', '東京都'), ('ベータ工業', '大阪府'), ('ガンマ商事', '愛知県'); bizdb=# INSERT INTO orders (customer_id, product_name, amount, order_date) VALUES (1, 'Linux研修パッケージ', 198000, '2026-07-15'), (2, 'サーバー保守契約', 480000, '2026-07-20'), (1, 'クラウド移行支援', 320000, '2026-08-01'), (3, 'Linux研修パッケージ', 198000, '2026-08-10'); bizdb=# \q

2. 読み取り専用ユーザーへのSELECT権限付与

Text-to-SQLで生成したSQLはbizreaderユーザーで実行します。SELECT権限のみを付与することで、誤ったSQLが生成されてもデータが書き換わるリスクを排除します。

$ sudo -u postgres psql -d bizdb bizdb=# GRANT USAGE ON SCHEMA public TO bizreader; bizdb=# GRANT SELECT ON ALL TABLES IN SCHEMA public TO bizreader; bizdb=# ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO bizreader; bizdb=# \q # 接続確認(bizreaderでログイン) $ psql -U bizreader -d bizdb -h localhost -c "SELECT count(*) FROM orders;" count ------- 4 (1 row)

読み取り専用ユーザーで接続できれば準備完了です。

テーブルスキーマをinformation_schemaから動的取得する

1. スキーマ情報を文字列で取得する関数

Text-to-SQLプロンプトには、LLMがSQLを生成するためのテーブル定義情報が必要です。information_schemaから自動取得することで、テーブルが追加・変更された後もコード修正不要になります。

import psycopg2 def get_schema_ddl(conn, schema_name="public"): cur = conn.cursor() cur.execute( """ SELECT c.table_name, c.column_name, c.data_type, c.is_nullable FROM information_schema.columns c WHERE c.table_schema = %s ORDER BY c.table_name, c.ordinal_position """, (schema_name,) ) rows = cur.fetchall() tables = {} for table_name, col_name, data_type, nullable in rows: if table_name not in tables: tables[table_name] = [] col_def = f" {col_name} {data_type.upper()}" if nullable == "NO": col_def += " NOT NULL" tables[table_name].append(col_def) ddl = "" for table_name, cols in tables.items(): ddl += f"CREATE TABLE {table_name} (\n" ddl += ",\n".join(cols) ddl += "\n);\n\n" return ddl.strip() # 動作確認 conn = psycopg2.connect( dbname="bizdb", user="bizreader", password="reader_pw", host="localhost" ) print(get_schema_ddl(conn))

実行するとcustomersとordersのCREATE TABLE定義が文字列で取得できます。この文字列をそのままプロンプトに埋め込みます。

OllamaでSQL生成プロンプトを設計して自然言語クエリを実装する

1. プロンプトテンプレートの設計

SQL生成の精度はプロンプト設計に大きく左右されます。重要なポイントは3つです。
・テーブル定義をCREATE TABLE形式で提示する(LLMがカラム名と型を正確に把握できる)
・SELECT文のみを生成するよう明示的に制約する
・コードブロック記号(```)なしでSQLのみを出力させる

def build_prompt(schema_ddl, question): return f"""あなたはPostgreSQLの専門エンジニアです。 以下のデータベーススキーマを参照して、質問に対応するSQLクエリを生成してください。 制約: - SELECT文のみ生成する。INSERT/UPDATE/DELETE/DROPは使わない - コードブロック記号を含めず、SQLのみを返す - 結果が多い場合はLIMIT 20を付ける スキーマ: {schema_ddl} 質問: {question} SQL:"""

2. OllamaのAPIを呼び出してSQLを取得する

import requests def generate_sql(prompt, model="llama3.3:70b-instruct-q4_0"): resp = requests.post( "http://localhost:11434/api/generate", json={ "model": model, "prompt": prompt, "stream": False, "options": { "temperature": 0.1, # 決定論的に生成したいので低めに設定 "num_ctx": 4096 } }, timeout=120 ) resp.raise_for_status() sql = resp.json()["response"].strip() # コードブロック記号が含まれていた場合は除去 if "```" in sql: lines = sql.split("\n") sql = "\n".join( l for l in lines if not l.strip().startswith("```") ) return sql.strip()

`temperature: 0.1`と低く設定するのがポイントです。SQL生成では創造性より正確性が優先されるため、ランダム性を抑えた方が安定した出力になります。

生成されたSQLを安全に実行するパイプライン

1. SQLインジェクション対策のバリデーション関数

LLMが生成したSQLは必ず実行前にバリデーションします。SELECTで始まるかチェックし、データ変更・DDL系のキーワードが含まれていないかを検証します。

import re DANGEROUS = re.compile( r"\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|" r"GRANT|REVOKE|EXECUTE|COPY|VACUUM)\b", re.IGNORECASE ) def validate_sql(sql): if not sql.strip().upper().startswith("SELECT"): return False, "SELECT文で始まっていません" if DANGEROUS.search(sql): m = DANGEROUS.search(sql) return False, f"危険なキーワードが含まれています: {m.group()}" # 末尾以外のセミコロン(複数クエリ)を検出 if ";" in sql.rstrip(";"): return False, "複数のクエリが含まれています" return True, "OK"

2. バリデーション通過後にPostgreSQLで実行する

def run_text_to_sql(question): conn = psycopg2.connect( dbname="bizdb", user="bizreader", password="reader_pw", host="localhost" ) schema_ddl = get_schema_ddl(conn) prompt = build_prompt(schema_ddl, question) sql = generate_sql(prompt) ok, reason = validate_sql(sql) if not ok: conn.close() return {"error": reason, "sql": sql, "rows": []} cur = conn.cursor() try: cur.execute(sql) columns = [desc[0] for desc in cur.description] rows = cur.fetchall() except psycopg2.Error as e: conn.rollback() conn.close() return {"error": str(e.pgerror), "sql": sql, "rows": []} conn.close() return {"sql": sql, "columns": columns, "rows": rows} # 実行例 result = run_text_to_sql( "東京都の顧客の注文を合計金額が高い順に見せてください" ) print("生成SQL:", result["sql"]) for row in result.get("rows", []): print(row)

実行例の出力です。

# 実行例の出力 生成SQL: SELECT c.name, o.product_name, o.amount FROM customers c JOIN orders o ON c.id = o.customer_id WHERE c.prefecture = '東京都' ORDER BY o.amount DESC LIMIT 20; ('株式会社アルファ', 'クラウド移行支援', 320000) ('株式会社アルファ', 'Linux研修パッケージ', 198000)

JOINを含む複合クエリを自然言語から正確に生成できています。「SQLを書けない担当者が業務データを参照できる」という活用が実現できます。
社内AIツールの導入背景については社内でChatGPTが使えないときの代替手段も参考にしてください。

精度を高める工夫|few-shot例と列コメントの活用

1. few-shot例をプロンプトに追加する

プロンプトに「質問 → SQL」のサンプルを数例追加すると、生成精度が大幅に上がります。特に日本語カラム値のLIKE句、JOINの使い方、集計関数などを例示すると効果的です。

FEW_SHOT = """ 質問: 全顧客の一覧を見せてください SQL: SELECT id, name, prefecture, created_at FROM customers ORDER BY id; 質問: 2026年8月の注文件数を教えてください SQL: SELECT count(*) AS order_count FROM orders WHERE order_date >= '2026-08-01' AND order_date < '2026-09-01'; 質問: 顧客ごとの合計注文金額を高い順に見せてください SQL: SELECT c.name, sum(o.amount) AS total_amount FROM customers c JOIN orders o ON c.id = o.customer_id GROUP BY c.name ORDER BY total_amount DESC; """ def build_prompt(schema_ddl, question): return f"""(上記プロンプトのスキーマ部分は同じ) 参考例: {FEW_SHOT} 質問: {question} SQL:"""

2. PostgreSQLのCOMMENT ONでカラムの意味を補足する

カラム名だけでは意味が分かりにくい場合、COMMENT ON COLUMNでコメントを追加してスキーマ取得時に含める方法も効果的です。

$ sudo -u postgres psql -d bizdb bizdb=# COMMENT ON COLUMN orders.amount IS '注文金額(税込、円単位)'; bizdb=# COMMENT ON COLUMN customers.prefecture IS '都道府県名(例: 東京都、大阪府)'; bizdb=# \q

`get_schema_ddl()`関数を拡張してcol_descriptionも取得するようにすれば、プロンプト内でLLMが意味を正確に理解できるようになります。カラムコメントはpg_catalog.pg_descriptionから取得できます。

よくあるエラーと対処法|Text-to-SQL実装時のトラブルシューティング

エラー1: 存在しないカラム名のSQLが生成される

モデルがスキーマを誤読してカラム名を捏造することがあります。特にMistral 7Bなど小さなモデルで起こりやすいです。
psycopg2のcur.execute()をtry/exceptで囲み、PostgreSQLのエラーをキャッチしてユーザーに返すのが基本対策です。精度が低い場合は`llama3.3:70b-instruct-q4_0`への切り替えと、few-shot例の追加を先に試してください。

try: cur.execute(sql) rows = cur.fetchall() except psycopg2.Error as e: conn.rollback() return {"error": f"SQL実行エラー: {e.pgerror}", "sql": sql, "rows": []}

エラー2: Ollamaのレスポンスがタイムアウトする

70Bモデルでプロンプトが長い場合、CPUのみの環境では生成に数分かかることがあります。`requests.post()`のtimeout引数を180以上に設定します。
またOllamaのsystemd override.confで`OLLAMA_NUM_PARALLEL=1`と`OLLAMA_MAX_LOADED_MODELS=1`を設定するとメモリ消費が安定します。

# systemdのoverride.confでOllama環境変数を設定 $ sudo systemctl edit ollama # 追加するセクション [Service] Environment="OLLAMA_NUM_PARALLEL=1" Environment="OLLAMA_MAX_LOADED_MODELS=1" $ sudo systemctl daemon-reload && sudo systemctl restart ollama

エラー3: バリデーションが正当なSQLを弾く

ANALYZEなどのキーワードが集計クエリ(EXPLAIN ANALYZE等)に含まれていると誤検出されることがあります。その場合はDANGEROUSの正規表現からANALYZEを外し、EXPLAINを追加するなどパターンを調整してください。本番環境では要件に合わせてホワイトリスト方式も検討します。

まとめ|OllamaとText-to-SQLで自然言語からPostgreSQLを安全に検索する要点

OllamaとPythonによるText-to-SQLパイプラインは、整備済みの業務DBに対して「SQLを書かずにデータを参照したい」という現場ニーズに直接応えられます。
pgvectorやRAGが非構造化テキスト向けであるのに対し、Text-to-SQLはリレーショナルDBと相性がよく、月次売上・在庫・顧客情報の検索に強みを発揮します。
工程 実装ポイント
スキーマ取得 SELECT ... FROM information_schema.columns WHERE table_schema='public'
プロンプト設計 CREATE TABLE定義+few-shot例+「SELECTのみ生成」制約を明示
SQL生成API POST http://localhost:11434/api/generate(temperature: 0.1)
SQLバリデーション SELECTで始まるか+DANGEROUS_KEYWORDSの正規表現チェック
SQL実行 GRANT SELECT ON ALL TABLESの読み取り専用ユーザーで接続して実行
精度向上 few-shot例の追加、COMMENT ON COLUMNによる意味補足、70Bモデル利用

Text-to-SQLとローカルLLM活用を2日間のハンズオンで体験する

OllamaによるText-to-SQL構築から、pgvectorを使ったRAGパイプラインの実装まで、本記事で解説した内容を実機のGPUサーバーで試したい方向けに、「ローカルAIマスターセミナー」を開催しています。
少人数(最大8名)ZOOMハンズオン形式で実施しています。

>> ローカルAIマスターセミナーの詳細を確認する
ローカルLLMの構築・運用に関する関連記事もあわせて参考にしてください。

Ubuntu ServerでローカルLLMを構築する方法|Ollamaで機密データを外に出さず業務AIを動かす完全ガイド
社内でChatGPTが使えないときの代替手段|機密データを守るローカルLLMという選択肢
ローカルLLMのモデルを比較する方法|Llama3.3・Mistral・Gemma・Phi-4をUbuntuで使い分けるポイント

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。