mcpField
S
sql-query-generation

sql-query-generation

github.com/seb1nデータ・データベース232124 回インストール11 週間前に更新
リポジトリ

sql-query-generation について

自然言語の質問を、提示されたデータベーススキーマに合わせたSQLへ変換するスキルです。分析用の構文に対応し、EXPLAINプランから性能上の問題を調べて改善案を示します。

主な機能

  1. 01自然言語の要件解析
  2. 02スキーマに沿ったクエリ構築
  3. 03JOIN、CTE、ウィンドウ関数
  4. 04EXPLAINによる性能分析
  5. 05クエリの平易な説明

活用シーン

  1. 01質問から分析クエリを生成
  2. 02遅いクエリの実行プラン確認
  3. 03期間別のデータ比較

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/seb1n/awesome-ai-agent-skills --skill sql-query-generation

インストール後、Claude Code で/sql-query-generationを入力して読み込まれたか確認します。

詳細

SQL クエリ生成

このスキルを使うと、AI エージェントは自然言語の質問を、正確で効率的な SQL クエリに変換できます。エージェントは対象のデータベーススキーマに従い、ユーザーの意図を適切なクエリ構文(結合、集計、ウィンドウ関数、CTE、サブクエリ)に対応付けます。また、EXPLAIN プランを使ってクエリのパフォーマンスを分析し、インデックス作成、述語のプッシュダウン、クエリの再構成などの最適化を推奨します。

ワークフロー

  1. 自然言語のリクエストを解析します。 分析の意図を抽出します。求められている指標、関係するエンティティ、適用するフィルター、結果の並べ替えやグループ化の方法を特定します。集計結果の要約を求めているのか、行単位の詳細を求めているのかを区別します。

  2. データベーススキーマに対応付けます。 スキーマから関連するテーブルと列を特定します。曖昧な参照(たとえば、「sales」が orders テーブルまたは revenue 列を指す場合)を解決します。結果セットが不必要に膨らまないよう、外部キーの関係を使ってテーブル間の結合経路を判断します。

  3. 適切なクエリ構文を選択します。 複雑さに応じて、単純な集計、ウィンドウ関数、CTE、サブクエリから選択します。複数段階の計算には、可読性を高めるためにCTEを使います。累積合計、順位付け、パーティション内の比較にはウィンドウ関数を使います。暗黙的なカンマ区切りの結合より、明示的なJOINを優先します。

  4. SQLクエリを生成します。 一貫した書式(キーワードは大文字、識別子は小文字、テーブルには別名を付け、句をインデント)で、構文的に正しいSQLを記述します。複雑なロジックにはコメントを付けます。計算式には必ず列の別名を指定します。

  5. 検証して最適化します。 生成したクエリに対してEXPLAIN(またはEXPLAIN ANALYZE)を実行し、実行計画を確認します。全テーブルスキャン、大規模なテーブルでのハッシュ結合、インデックスのない列に対するソート処理を確認します。推定コストが高い場合は、インデックスやクエリの書き換えを推奨します。

  6. 説明を添えて結果を返します。 クエリとともに、その処理内容、想定される出力形式、スキーマやデータについて置いた前提を、平易な言葉で説明します。

対応テクノロジー

  • PostgreSQL — CTE、ウィンドウ関数、LATERAL結合、EXPLAIN ANALYZE
  • MySQL — 共通テーブル式(8.0以降)、ウィンドウ関数(8.0以降)、EXPLAIN FORMAT=JSON
  • SQLite — 軽量なクエリ、ウィンドウ関数(3.25以降)
  • SQL Server — T-SQL拡張、CROSS APPLY、クエリストア分析
  • BigQuery — STRUCT/ARRAY型、QUALIFY句、スロットベースのコスト見積もり

使い方

エージェントに自然言語の質問とデータベーススキーマ(DDL、スキーマ図、またはテーブルと列の一覧)を渡します。必要に応じて、対象のSQL方言、パフォーマンス上の制約、EXPLAIN分析を含めるかどうかも指定します。

例

例 1:自然言語から複雑な分析クエリを作成する

リクエスト:「過去12か月の総支出額が多い上位5人の顧客と、最も頻繁に注文した商品カテゴリ、およびその支出額を直前の12か月間と比較した結果を表示してください。」

WITH current_period AS (
    SELECT
        c.customer_id,
        c.customer_name,
        SUM(oi.quantity * oi.unit_price) AS total_spent,
        COUNT(DISTINCT o.order_id) AS order_count
    FROM customers c
    JOIN orders o ON c.customer_id = o.customer_id
    JOIN order_items oi ON o.order_id = oi.order_id
    WHERE o.order_date >= CURRENT_DATE - INTERVAL '12 months'
    GROUP BY c.customer_id, c.customer_name
),
previous_period AS (
    SELECT
        c.customer_id,
        SUM(oi.quantity * oi.unit_price) AS prev_total_spent
    FROM customers c
    JOIN orders o ON c.customer_id = o.customer_id
    JOIN order_items oi ON o.order_id = oi.order_id
    WHERE o.order_date >= CURRENT_DATE - INTERVAL '24 months'
      AND o.order_date < CURRENT_DATE - INTERVAL '12 months'
    GROUP BY c.customer_id
),
top_categories AS (
    SELECT DISTINCT ON (c.customer_id)
        c.customer_id,
        pc.category_name AS favorite_category,
        COUNT(*) AS category_orders
    FROM customers c
    JOIN orders o ON c.customer_id = o.customer_id
    JOIN order_items oi ON o.order_id = oi.order_id
    JOIN products p ON oi.product_id = p.product_id
    JOIN product_categories pc ON p.category_id = pc.category_id
    WHERE o.order_date >= CURRENT_DATE - INTERVAL '12 months'
    GROUP BY c.customer_id, pc.category_name
    ORDER BY c.customer_id, COUNT(*) DESC
)
SELECT
    cp.customer_name,
    cp.total_spent,
    cp.order_count,
    tc.favorite_category,
    pp.prev_total_spent,
    ROUND(
        (cp.total_spent - COALESCE(pp.prev_total_spent, 0))
        / NULLIF(pp.prev_total_spent, 0) * 100, 1
    ) AS spending_change_pct
FROM current_period cp
LEFT JOIN previous_period pp ON cp.customer_id = pp.customer_id
LEFT JOIN top_categories tc ON cp.customer_id = tc.customer_id
ORDER BY cp.total_spent DESC
LIMIT 5;

-- Expected output:
-- customer_name | total_spent | order_count | favorite_category | prev_total_spent | spending_change_pct
-- Acme Corp     | 284,500.00  | 47          | Electronics       | 198,200.00       | 43.5
-- GlobalTech    | 231,800.00  | 38          | Software          | 245,100.00       | -5.4
-- ...
例 2:EXPLAIN分析で遅いクエリを最適化する

元の遅いクエリ(500万行で12.4秒かかります):

SELECT product_name, SUM(quantity * unit_price) AS revenue
FROM order_items oi, products p, orders o
WHERE oi.product_id = p.product_id
  AND oi.order_id = o.order_id
  AND o.order_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY product_name
ORDER BY revenue DESC;

EXPLAIN ANALYZEの出力(問題を示す指標):

Seq Scan on orders o  (cost=0.00..98456.00 rows=1245000)
  Filter: (order_date >= '2024-01-01' AND order_date <= '2024-12-31')
  Rows Removed by Filter: 3755000
Hash Join  (cost=98456.00..245678.00 rows=3200000)
Sort  (cost=312456.00..312460.00 rows=8500)
  Sort Method: external merge  Disk: 4096kB

特定された問題:

  1. orders のシーケンシャルスキャン — order_date にインデックスがありません
  2. 暗黙的な結合構文により、オプティマイザーから結合順序が分かりません
  3. work_mem が不足しているため、ソートがディスクにスピルしています

最適化後のクエリ:

-- Step 1: Create index (one-time)
CREATE INDEX idx_orders_date ON orders (order_date)
    INCLUDE (order_id);

-- Step 2: Rewrite with explicit joins and date index hint
SELECT
    p.product_name,
    SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY p.product_name
ORDER BY revenue DESC;

-- After optimization: 0.34 seconds (36x faster)
-- EXPLAIN now shows:
-- Index Scan on idx_orders_date (rows=1245000, actual=1243892)
-- Merge Join (cost reduced by 85%)
-- Sort Method: quicksort  Memory: 512kB

ベストプラクティス

  • 意図を明確にし、意図しないクロス結合を防ぐため、カンマ区切りの暗黙的な結合ではなく、常に明示的な JOIN 構文を使用してください。
  • 読みやすさを高め、複雑なクエリでの曖昧さを避けるため、すべてのテーブルと計算列に別名を付けてください。
  • 複雑なクエリは、サブクエリを深くネストするのではなく、CTE を使って名前付きの論理的なステップに分割してください。
  • 早い段階でフィルターを適用してください。結合によって行数が増える前にデータセットを絞り込めるよう、駆動テーブルに WHERE 条件を指定してください。
  • 1 対多の関係によるカウントの水増しを避けるため、結合したテーブルでは COUNT(*) より COUNT(DISTINCT col) を優先してください。
  • 生成したクエリを最終版として提示する前に、必ず実際のスキーマに対してテストしてください。自然言語の説明にある列名や型は、実際の DDL と異なることがよくあります。

エッジケース

  • 曖昧な列名。 複数のテーブルに同じ名前の列(例:id、name、status)がある場合は、必ずテーブルの別名を使って修飾してください。自然言語によるリクエストに本当に曖昧さがある場合は、ユーザーに確認してください。
  • 集計での NULL の扱い。 SUM、AVG、COUNT(col) は NULL を暗黙的に無視します。NULL に意味がある場合(例:「売上なし」)は、集計前に COALESCE(col, 0) を使い、その前提を明記してください。
  • 計算指標でのゼロ除算。 分母を NULLIF(denominator, 0) で囲み、エラーの代わりに NULL を返すようにしてから、プレゼンテーション層で NULL を処理してください。
  • 日付とタイムゾーンの不一致。 タイムスタンプ列を日付でフィルターする場合は、境界を明確にしてください。終端境界の午前 0 時も含まれる BETWEEN ではなく、>= '2024-01-01' AND < '2025-01-01' を使用してください。
  • 非常に大きな結果セット。 探索的なクエリには必ず LIMIT を指定してください。本番クエリでは、深いページでのパフォーマンスを高めるため、OFFSET/FETCH またはキーセットページネーションでページネーションを追加してください。

コメント