AI 업무 활용

AI가 작성한 SQL을 손으로 계산한 SQLite 결과와 비교하기

AI가 만든 SQL을 완성된 답이 아니라 초안으로 취급하고, 정답을 알고 있는 작은 SQLite 데이터베이스에서 시험합니다. 실제 데이터에 적용하기 전에 AI 결과를 손으로 계산한 예상 표와 비교합니다.

목차 보기

이런 분께 맞아요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에서 상태와 날짜를 필터링하는 SQL을 제안했다고 가정합니다. 문법적으로는 그럴듯하지만 INNER JOIN은 조건에 맞는 주문이 없는 고객을 제거합니다. 따라서 0원 고객도 포함해야 한다는 요구사항을 만족하지 못합니다.

02합성 SQLite 데이터베이스 만들기

아래 데이터베이스는 이 글을 위해 작성한 합성 데이터입니다. 고객 3명과 주문 6개로 구성되며, 결제 여부, 월 경계, 8월의 결제 완료 주문이 없는 고객을 모두 시험할 수 있도록 구성했습니다.

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 두 건이므로 합계는 100입니다. CANCELLED 주문 30은 제외합니다. Ben은 8월 PAID 주문 25 한 건만 해당하며 9월 주문 50은 제외합니다. Cara는 8월 주문이 있지만 CANCELLED 상태뿐이므로 반드시 0으로 표시되어야 합니다.

customer_idcustomer_name예상 8월 PAID 합계
1Ana100
2Ben25
3Cara0

예상 전체 합계는 125입니다. 요구사항에 모든 고객을 포함한다고 했으므로 출력 행은 정확히 3개여야 합니다.

04AI SQL과 수정 SQL 비교하기

아래 AI SQL은 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;

수정 SQL은 모든 고객에서 시작해 LEFT JOIN을 사용합니다. 주문 필터를 ON 조건 안에 두므로 조건에 맞는 주문이 없는 고객도 남습니다. 집계 결과가 NULL이면 COALESCE로 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;

05두 SQL을 실행해 예상 행과 비교하기

다음 스크립트를 ai_sql_check.py로 저장하세요. 두 SQL을 실행하고 결과를 손으로 계산한 예상 표와 비교한 뒤 검토용 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 SQL은 Ana와 Ben만 반환하므로 행이 2개이며 예상 표와 일치하지 않아야 합니다. 수정 SQL은 고객 3명을 모두 반환하고 손으로 계산한 표와 정확히 일치해야 합니다.

SQL반환 행예상 전체 합계예상 표와 일치
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건 고객도 포함되어야 한다면 그런 고객을 합성 데이터에 반드시 넣으세요.
  • 8월 31일과 9월 1일처럼 날짜 경계를 시험하세요.
  • CANCELLED처럼 제외해야 하는 상태도 포함하세요.
  • OUTPUT_DIR을 바꾸지 않고 다시 실행합니다. 이전 보고서를 덮어쓰지 않고 FileExistsError로 중단되어야 합니다.
자주 발생하는 문제확인할 사항
예상보다 행이 적음JOIN 종류와 WHERE 조건이 LEFT JOIN의 일치하지 않는 행을 제거하는지 확인하세요.
합계가 너무 큼집계 전에 일대다 JOIN으로 행이 중복되는지 확인하세요.
날짜 범위 오류저장된 날짜 형식에 맞는 명시적인 시작 경계와 배타적 종료 경계를 사용하세요.
0 대신 NULL일치 항목이 없을 때 NULL을 유지할지 COALESCE로 0으로 바꿀지 결정하세요.
전체 합계는 맞지만 상세가 틀림전체 합계뿐 아니라 예상 행 전체를 비교하세요.

이 테스트는 수정 SQL이 이 작은 합성 예제와 일치한다는 것만 보여줍니다. 실제 스키마에는 중복 관계, NULL, 타임스탬프, 시간대, 환불, 추가 업무 규칙이 있을 수 있습니다. 그런 조건이 중요하다면 합성 테스트 데이터에도 추가해야 합니다.

실행·검증 기록

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 SQL의 INNER JOIN과 WHERE 필터 때문에 Cara가 누락되어 2행이 된다는 점을 검토했습니다.
  • 수정된 LEFT JOIN SQL이 손으로 계산한 3개 행을 반환해야 함을 검토했습니다.
  • 예상 콘솔 출력과 보고서 비교 결과를 손으로 도출했습니다.
검증 한계
  • 이 응답의 작성자는 코드를 실행하지 않았으며, SQLite 동작이나 파일 시스템 출력을 실제로 시험하지 않았습니다.
  • 목록에 있는 합성 행만 검토했으며 NULL 금액, 중복 JOIN, 타임스탬프, 시간대, 환불, 대규모 데이터는 시험하지 않았습니다.
  • 이 예제와 일치한다고 해서 모든 실제 스키마와 업무 규칙에서 SQL이 정확하다고 증명되는 것은 아닙니다.
  • 공식 문서 URL은 알려진 문서 위치를 사용했지만 실시간으로 접속해 확인하지 않았습니다.

사이트 전체 작성·검증 원칙

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.