1. はじめに
Oracleデータベース用のコマンドラインツール SQLcl に MCP サーバーの機能が追加されました。この機能により自然言語で OracleDB のデータに問い合わせすること(Text-to-SQL)ができると謳われています。
Introducing MCP Server for Oracle Database
ただし、机上で調べたところ、以下の理由から力不足では?という印象を持ちました。
- 自然言語による問い合わせテキストに対して、どのテーブルが必要か判断する機能が提供されていない
- テーブルのメタデータを取得する機能が提供されていない
(SQLcl MCP サーバーが提供する機能はこちらに一覧があります。)
そこで、実際にできるかどうかを検証してみました。
結論から言うと、
ツール | モデル | 結果 |
---|---|---|
GitHub Copilot | GPT-4.1 | × |
GitHub Copilot | Claude Sonnet 3.5 | × |
Codex CLI | GPT-5-codex | ○ |
でした。
この記事ではうまくいった3番目の Codex CLI + GPT-5-codex の組み合わせにおける結果について紹介・考察したいと思います。
2. データモデル
今回の検証では以下のような簡単なデータモデルを利用しました。
DDL 文は以下になります。
create table item_categories (
category_id integer primary key,
category_name varchar(100) not null
);
comment on table item_categories is '商品カテゴリ';
comment on column item_categories.category_id is 'カテゴリID';
comment on column item_categories.category_name is 'カテゴリ名';
create table skus (
sku_id integer primary key,
sku_name varchar(100) not null,
category_id integer not null references item_categories(category_id)
);
comment on table skus is 'SKU';
comment on column skus.sku_id is 'SKU ID';
comment on column skus.sku_name is 'SKU名';
comment on column skus.category_id is 'カテゴリID';
create table daily_sales (
sales_date date,
sku_id integer,
sales_quantity integer not null,
primary key (sales_date, sku_id)
);
comment on table daily_sales is '日次売上';
comment on column daily_sales.sales_date is '売上日';
comment on column daily_sales.sku_id is 'SKU ID';
comment on column daily_sales.sales_quantity is '売上数';
create table daily_stock (
stock_date date,
sku_id integer,
stock_count integer not null,
primary key (stock_date, sku_id)
);
comment on table daily_stock is '日次在庫';
comment on column daily_stock.stock_date is '在庫計上日';
comment on column daily_stock.sku_id is 'SKU ID';
comment on column daily_stock.stock_count is '在庫数';
3. テキストによる問い合わせ
3-1. ケース1
以下の問い合わせを投げてみます。
salesデータベースに接続して、商品カテゴリごとの売上数合計を集計して。
この結果、Codex CLI は MCP サーバーのツールを使いながら処理を行います。
- DB への接続
tool success, duration: 9.02s
SQLcl.connect({"connection_name":"sales","model":"gpt-5-codex","mcp_client":"CodexCLI"})
- テーブル一覧の取得
tool success, duration: 2.08s
SQLcl.run-sql({"sql":"SELECT /* LLM in use is gpt-5-codex */ table_name FROM user_tables ORDER BY table_name;","model":"gpt-5-codex","mcp_client":"CodexCLI"})
"TABLE_NAME""DAILY_SALES""DAILY_STOCK""ITEM_CATEGORIES""SKUS"
- daily_sales テーブルの定義取得
tool success, duration: 626ms
SQLcl.run-sql({"sql":"SELECT /* LLM in use is gpt-5-codex */ column_name, data_type FROM user_tab_columns WHERE table_name = 'DAILY_SALES' ORDER BY
column_id;","model":"gpt-5-codex","mcp_client":"CodexCLI"})
"COLUMN_NAME","DATA_TYPE""SALES_DATE","DATE""SKU_ID","NUMBER""SALES_QUANTITY","NUMBER"
- SKUテーブルの定義取得
tool success, duration: 116ms
SQLcl.run-sql({"sql":"SELECT /* LLM in use is gpt-5-codex */ column_name, data_type FROM user_tab_columns WHERE table_name = 'SKUS' ORDER BY
column_id;","model":"gpt-5-codex","mcp_client":"CodexCLI"})
- ITEM_CATEGORIESテーブルの定義取得
tool success, duration: 87ms
SQLcl.run-sql({"sql":"SELECT /* LLM in use is gpt-5-codex */ column_name, data_type FROM user_tab_columns WHERE table_name = 'ITEM_CATEGORIES' ORDER BY
column_id;","model":"gpt-5-codex","mcp_client":"CodexCLI"})
"COLUMN_NAME","DATA_TYPE""CATEGORY_ID","NUMBER""CATEGORY_NAME","VARCHAR2"
- 集計 SQL 文の実行
tool success, duration: 1.27s
SQLcl.run-sql({"sql":"SELECT /* LLM in use is gpt-5-codex */ ic.category_name, SUM(ds.sales_quantity) AS total_sales_quantity\nFROM daily_sales ds\nJOIN
skus s ON ds.sku_id = s.sku_id\nJOIN item_categories ic ON s.category_id = ic.category_id\nGROUP BY ic.category_name\nORDER BY total_sales_quantity
DESC;","model":"gpt-5-codex","mcp_client":"CodexCLI"})
"CATEGORY_NAME","TOTAL_SALES_QUANTITY""カテゴリA",19833"カテゴリB",9956
- 結果の回答
> カテゴリ別の売上数量合計は以下の通りです:
- カテゴリA: 19,833
- カテゴリB: 9,956
必要なら期間や追加条件を指定した再集計にも対応できます。
流れを見ると、
- テーブル名一覧の取得
- クエリに関係しそうなテーブルの定義の取得
- 集計 SQL 文の作成と実行
という流れになっています。
気になるポイントは
-
- でクエリに関係しそうかどうかをテーブル名からのみ判断している
- カラム名を参照しないとクエリに関係するか判断できないテーブルは取得できるのか?
- テーブル定義取得ではカラムの物理名とデータ型のみ取得している
- 今回のケースでは、テーブル名・カラム名は分かりやすく、問い合わせテキストでも類推可能な用語を使っているが、そうでない場合はテーブル・カラムに付与したコメントも参照してほしいところ
という点です。
3-2. ケース2
次に、別のセッションで問い合わせをしました。
salesデータベースに接続して、商品カテゴリごと、月ごとの在庫回転日数を計算してください。在庫回転日数 = 日別合計在庫の月平均 / 日別売上数合計の月平均 です。
Codex のすべての実行結果は載せませんが、ケース1と追加で以下の処理が動いています。
- DAILY_STOCKテーブルの定義取得
tool success, duration: 156ms
SQLcl.run-sql({"model":"gpt-5-codex","mcp_client":"Codex CLI","sql":"SELECT /* LLM in use is gpt-5-codex */ column_name, data_type FROM user_tab_columns WHERE
table_name = 'DAILY_STOCK' ORDER BY column_id;"})
"COLUMN_NAME","DATA_TYPE""STOCK_DATE","DATE""SKU_ID","NUMBER""STOCK_COUNT","NUMBER"
- 集計 SQL 文の実行
tool success, duration: 1.43s
SQLcl.run-sql({"model":"gpt-5-codex","mcp_client":"Codex CLI","sql":"WITH stock_daily AS (\n SELECT sk.category_id,\n ds.stock_date AS
data_date,\n SUM(ds.stock_count) AS total_stock\n FROM daily_stock ds\n JOIN skus sk ON sk.sku_id = ds.sku_id\n GROUP BY sk.category_id,
ds.stock_date\n),\nstock_monthly AS (\n SELECT category_id,\n TRUNC(data_date, 'MM') AS month_start,\n AVG(total_stock)
AS avg_daily_stock\n FROM stock_daily\n GROUP BY category_id, TRUNC(data_date, 'MM')\n),\nsales_daily AS (\n SELECT sk.category_id,
\n dsa.sales_date AS data_date,\n SUM(dsa.sales_quantity) AS total_sales\n FROM daily_sales dsa\n JOIN skus sk ON sk.sku_id
= dsa.sku_id\n GROUP BY sk.category_id, dsa.sales_date\n),\nsales_monthly AS (\n SELECT category_id,\n TRUNC(data_date, 'MM') AS
month_start,\n AVG(total_sales) AS avg_daily_sales\n FROM sales_daily\n GROUP BY category_id, TRUNC(data_date, 'MM')\n),\ncombined AS
(\n SELECT category_id, month_start FROM stock_monthly\n UNION\n SELECT category_id, month_start FROM sales_monthly\n)\nSELECT /* LLM in use
is gpt-5-codex */ ic.category_name,\n TO_CHAR(c.month_start, 'YYYY-MM') AS year_month,\n sm.avg_daily_stock,\n slm.avg_daily_sales,
\n CASE\n WHEN slm.avg_daily_sales IS NULL OR slm.avg_daily_sales = 0 THEN NULL\n ELSE sm.avg_daily_stock / slm.avg_daily_sales\n
END AS inventory_turnover_days\nFROM combined c\nJOIN item_categories ic ON ic.category_id = c.category_id\nLEFT JOIN stock_monthly sm ON sm.category_id =
c.category_id AND sm.month_start = c.month_start\nLEFT JOIN sales_monthly slm ON slm.category_id = c.category_id AND slm.month_start = c.month_start\nORDER BY
ic.category_name, c.month_start;"})
"CATEGORY_NAME","YEAR_MONTH","AVG_DAILY_STOCK","AVG_DAILY_SALES","INVENTORY_TURNOVER_DAYS""カテゴリA","2025-04",834.7,217.6,3.8359375"カ
テゴリA","2025-05",842.16129032258064516129032258064516129,224,3.75964861751152073732718894009216589862"カテゴリA","2025-
06",807.8,212.033333333333333333333333333333333333,3.80977833673950636692343971073730545512"カテゴリB","2025-04",474.666666666666666666666666666666...
- 最終的な回答
> Calculated monthly inventory turnover days by category using average daily stock and sales aggregates. Key results:
- カテゴリA: 2025-04 → 3.8359日, 2025-05 → 3.7596日, 2025-06 → 3.8098日
- カテゴリB: 2025-04 → 4.3654日, 2025-05 → 4.0183日, 2025-06 → 3.6645日
Turnover is avg_daily_stock ÷ avg_daily_sales; months with zero or missing sales return NULL. Let me know if you’d like rounded values or additional filters.
なぜか回答が英語になっていますが、正しく集計できているようです。今回の処理はドリルアクロスといわれる比較的複雑な処理なのですが、問題なく SQL 文を組み立てられているようです。
一応、実行された SQL 文を整形したものを以下に載せておきます。
WITH stock_daily AS (
SELECT
sk.category_id,
ds.stock_date AS data_date,
SUM(ds.stock_count) AS total_stock
FROM daily_stock ds
JOIN skus sk ON sk.sku_id = ds.sku_id
GROUP BY sk.category_id, ds.stock_date
),
stock_monthly AS (
SELECT category_id,
TRUNC(data_date, 'MM') AS month_start,
AVG(total_stock) AS avg_daily_stock
FROM stock_daily
GROUP BY category_id, TRUNC(data_date, 'MM')
),
sales_daily AS (
SELECT sk.category_id,
dsa.sales_date AS data_date,
SUM(dsa.sales_quantity) AS total_sales
FROM daily_sales dsa
JOIN skus sk ON sk.sku_id = dsa.sku_id
GROUP BY sk.category_id, dsa.sales_date
),
sales_monthly AS (
SELECT category_id,
TRUNC(data_date, 'MM') AS month_start,
AVG(total_sales) AS avg_daily_sales
FROM sales_daily
GROUP BY category_id, TRUNC(data_date, 'MM')
),
combined AS
(
SELECT category_id, month_start FROM stock_monthly
UNION
SELECT category_id, month_start FROM sales_monthly
)
SELECT /* LLM in use is gpt-5-codex */
ic.category_name,
TO_CHAR(c.month_start, 'YYYY-MM') AS year_month,
sm.avg_daily_stock,
slm.avg_daily_sales,
CASE WHEN slm.avg_daily_sales IS NULL OR slm.avg_daily_sales = 0
THEN NULL
ELSE sm.avg_daily_stock / slm.avg_daily_sales
END AS inventory_turnover_days
FROM combined c
JOIN item_categories ic ON ic.category_id = c.category_id
LEFT JOIN stock_monthly sm ON sm.category_id = c.category_id AND sm.month_start = c.month_start
LEFT JOIN sales_monthly slm ON slm.category_id = c.category_id AND slm.month_start = c.month_start
ORDER BY
ic.category_name,
c.month_start
4. 考察
(途中です)