AIの業務活用

AIが作成したSQLを手計算したSQLite結果と照合する

AIが生成したSQLクエリは完成版ではなく下書きとして扱い、正解が分かっている小さなSQLiteデータベースでテストします。実データに使う前に、AIの結果を手計算した期待表と比較します。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English

対象読者AIを使ってSQLの下書きを作成し、重要なデータで実行する前に小さく再現可能なテストを行いたい人向けです。

準備するもの
  • Python 3.12と、そのバージョンを起動できるターミナルコマンドが必要です。
  • スクリプトがoutputs配下にフォルダを作成できる作業フォルダが必要です。
  • SELECT、JOIN、GROUP BY、集計関数の基本的な知識が必要です。
  • 必要なのはPython標準ライブラリのcsv、pathlib、sqlite3だけです。

01SQLを確認する前に業務上の質問を明確にする

要件が「すべての顧客について、2026年8月に発生したPAID注文の合計金額を報告する。条件に合う注文がない顧客も合計0で表示する」だとします。

AIアシスタントがcustomersとordersをINNER JOINし、その後WHEREでstatusとdateを絞り込むクエリを提案したとします。見た目は妥当に見えますが、INNER JOINでは一致する注文行がない顧客が削除されます。これは合計0の顧客も含めるという要件に反します。

02合成SQLiteデータベースを作成する

以下のデータベースは合成データで、このページ専用に作成したものです。顧客3人と注文6件があり、PAIDと非PAID、月の境界、8月にPAID注文がない顧客を確認できるように構成しています。

python
import sqlite3
from pathlib import Path

SOURCE_DIR = Path("outputs") / "ai_sql_demo"
DATABASE = SOURCE_DIR / "orders.sqlite"

CUSTOMERS = [
    (1, "Ana"),
    (2, "Ben"),
    (3, "Cara"),
]

ORDERS = [
    (101, 1, "2026-08-03", "PAID", 40),
    (102, 1, "2026-08-20", "PAID", 60),
    (103, 1, "2026-08-25", "CANCELLED", 30),
    (104, 2, "2026-08-10", "PAID", 25),
    (105, 2, "2026-09-01", "PAID", 50),
    (106, 3, "2026-08-12", "CANCELLED", 80),
]

SOURCE_DIR.parent.mkdir(parents=True, exist_ok=True)
SOURCE_DIR.mkdir()  # Stop if the synthetic source already exists.

connection = sqlite3.connect(DATABASE)
try:
    connection.executescript("""
        CREATE TABLE customers (
            customer_id INTEGER PRIMARY KEY,
            customer_name TEXT NOT NULL
        );

        CREATE TABLE orders (
            order_id INTEGER PRIMARY KEY,
            customer_id INTEGER NOT NULL,
            order_date TEXT NOT NULL,
            status TEXT NOT NULL,
            amount INTEGER NOT NULL,
            FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
        );
    """)
    connection.executemany(
        "INSERT INTO customers VALUES (?, ?)", CUSTOMERS
    )
    connection.executemany(
        "INSERT INTO orders VALUES (?, ?, ?, ?, ?)", ORDERS
    )
    connection.commit()
finally:
    connection.close()
text
python create_ai_sql_demo.py

03正しい答えを手計算する

Anaには8月のPAID注文が40と60の2件あるため、合計は100です。CANCELLED注文は数えません。Benには対象となる8月の注文が25の1件あり、9月の注文は数えません。Caraには8月のCANCELLED注文しかないため、それでも0として表示される必要があります。

customer_idcustomer_name期待される8月のPAID合計
1Ana100
2Ben25
3Cara0

期待される総合計は125です。要件で全顧客を表示するとしているため、出力行は正確に3行である必要があります。

04AIのクエリと修正版クエリを比較する

以下のAIクエリはINNER JOINの後、WHERE句で対象注文を絞り込みます。Caraには条件に合う8月のPAID注文がないため、結果から完全に消えます。

sql
SELECT
    c.customer_id,
    c.customer_name,
    SUM(o.amount) AS august_paid_total
FROM customers AS c
JOIN orders AS o
    ON o.customer_id = c.customer_id
WHERE o.status = 'PAID'
  AND o.order_date >= '2026-08-01'
  AND o.order_date < '2026-09-01'
GROUP BY c.customer_id, c.customer_name
ORDER BY c.customer_id;

修正版クエリはすべての顧客から始め、LEFT JOINを使います。注文のフィルタ条件をON句の中に置くことで、一致する注文がない顧客も残ります。COALESCEはその結果のNULL集計値を0に変換します。

sql
SELECT
    c.customer_id,
    c.customer_name,
    COALESCE(SUM(o.amount), 0) AS august_paid_total
FROM customers AS c
LEFT JOIN orders AS o
    ON o.customer_id = c.customer_id
   AND o.status = 'PAID'
   AND o.order_date >= '2026-08-01'
   AND o.order_date < '2026-09-01'
GROUP BY c.customer_id, c.customer_name
ORDER BY c.customer_id;

052つのクエリを実行し、期待行と比較する

以下のスクリプトをai_sql_check.pyとして保存してください。2つのクエリを実行し、その結果を手計算した期待表と比較し、CSVレビュー報告を書き出します。合成データベース自体は変更しません。

python
import csv
import sqlite3
from pathlib import Path

DATABASE = Path("outputs") / "ai_sql_demo" / "orders.sqlite"
OUTPUT_DIR = Path("outputs") / "ai_sql_check_result"
REPORT = OUTPUT_DIR / "sql_check.csv"

AI_SQL = """
SELECT c.customer_id, c.customer_name, SUM(o.amount)
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.customer_id
WHERE o.status = 'PAID'
  AND o.order_date >= '2026-08-01'
  AND o.order_date < '2026-09-01'
GROUP BY c.customer_id, c.customer_name
ORDER BY c.customer_id;
"""

REVISED_SQL = """
SELECT c.customer_id, c.customer_name, COALESCE(SUM(o.amount), 0)
FROM customers AS c
LEFT JOIN orders AS o
    ON o.customer_id = c.customer_id
   AND o.status = 'PAID'
   AND o.order_date >= '2026-08-01'
   AND o.order_date < '2026-09-01'
GROUP BY c.customer_id, c.customer_name
ORDER BY c.customer_id;
"""

EXPECTED = [
    (1, "Ana", 100),
    (2, "Ben", 25),
    (3, "Cara", 0),
]


def run_query(connection, sql):
    return connection.execute(sql).fetchall()


def main() -> None:
    if not DATABASE.is_file():
        raise FileNotFoundError(f"Database not found: {DATABASE}")
    if OUTPUT_DIR.exists():
        raise FileExistsError(f"Output folder already exists: {OUTPUT_DIR}")

    connection = sqlite3.connect(f"file:{DATABASE.resolve()}?mode=ro", uri=True)
    try:
        ai_rows = run_query(connection, AI_SQL)
        revised_rows = run_query(connection, REVISED_SQL)
    finally:
        connection.close()

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()

    with REPORT.open("x", encoding="utf-8", newline="") as stream:
        writer = csv.writer(stream)
        writer.writerow(["query", "rows", "matches_expected"])
        writer.writerow(["AI", repr(ai_rows), ai_rows == EXPECTED])
        writer.writerow(["REVISED", repr(revised_rows), revised_rows == EXPECTED])

    print(f"Expected rows: {len(EXPECTED)}.")
    print(f"AI rows: {len(ai_rows)}; matches expected: {ai_rows == EXPECTED}.")
    print(
        f"Revised rows: {len(revised_rows)}; "
        f"matches expected: {revised_rows == EXPECTED}."
    )
    print(f"Report: {REPORT.as_posix()}")

    if revised_rows != EXPECTED:
        raise RuntimeError("Revised SQL does not match the expected result.")


if __name__ == "__main__":
    main()
text
python ai_sql_check.py

06期待される出力を確認する

AIクエリはAnaとBenだけを返すため2行となり、期待結果とは一致しないはずです。修正版クエリは3人すべての顧客を返し、手計算した表と完全に一致するはずです。

クエリ返される行数期待される総合計期待表と一致
AI2125いいえ
修正版3125はい

AIの結果は、Caraに必要な0の行が欠けていて誤っているにもかかわらず、総合計125だけは正しいままになる可能性があります。これが、合計だけを確認するのでは不十分な理由です。

text
Expected rows: 3.
AI rows: 2; matches expected: False.
Revised rows: 3; matches expected: True.
Report: outputs/ai_sql_check_result/sql_check.csv

07追加確認と限界を理解する

  • 合計だけでなく行数も確認します。
  • 要件で0件の顧客も表示するとしている場合、そのような顧客を合成データに含めます。
  • August 31やSeptember 1のような日付境界をテストします。
  • CANCELLEDのように除外されるstatusも含めます。
  • OUTPUT_DIRを変更せずにチェッカーを再実行します。レポートを上書きせず、FileExistsErrorで停止するはずです。
よくある問題確認する点
予想外に行が欠けるJOINの種類と、WHEREのフィルタがLEFT JOINの不一致行を削除していないか確認します。
合計が大きすぎる集計前に1対多のJOINで行が重複していないか確認します。
日付範囲が間違っている保存されている日付形式に合った明示的な下限と排他的な上限を使います。
0ではなくNULLになる一致がない場合にNULLを残すのか、COALESCEで0にするのか決めます。
総合計は正しいが詳細が間違っている総合計だけでなく、期待される各行を完全に比較します。

このテストで分かるのは、修正版クエリがこの小さな合成例と一致することだけです。実際のスキーマには、重複関係、NULL値、timestamps、time zones、refunds、追加の業務ルールが含まれる場合があります。そうした条件が重要なら、テストデータにも追加してください。

実行・検証の記録

2026-09-20 · 手作業で確認した例 · 対象: Python 3.12 · 標準ライブラリ: csv, pathlib, sqlite3 · 未実行

  • Anaの対象となる8月合計を40 + 60 = 100として手計算しました。
  • Benの対象となる8月合計を25として手計算し、9月の注文を除外しました。
  • Caraの8月の注文はCANCELLEDのみなので0として表示される必要があると手作業で判断しました。
  • 期待される総合計を125、期待される行数を3として手計算しました。
  • AIクエリを確認し、INNER JOINとWHEREのフィルタによってCaraが除外され、2行になることを確認しました。
  • 修正版のLEFT JOINクエリを確認し、手計算した3行を返すと予想しました。
  • 期待されるコンソール出力とレポート比較結果を手作業で導きました。
検証範囲の限界
  • この回答の作成者はコードを実行しておらず、SQLiteやファイルシステムの動作はここではテストしていません。
  • 掲載した合成行だけを評価しており、NULLのamount、重複JOIN、timestamps、time zones、refunds、大規模データセットはテストしていません。
  • この例と一致しても、すべての本番スキーマや業務ルールでクエリが正しいことを証明するものではありません。
  • 公式ドキュメントのURLは既知の文書位置を使用していますが、リアルタイムでは確認していません。

サイト全体の執筆・検証方針

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。