Using AI at work

Check an AI-written SQL query against hand-computed SQLite results

Treat an AI-generated SQL query as a draft and test it on a tiny SQLite database with known answers. Compare the AI result with a manually calculated expected table before using the query on real data.

Show contents

Who this is forThis guide is for people who use AI to draft SQL and want a small reproducible test before running the query on important data.

What you need
  • Python 3.12 and a terminal command that starts that version.
  • A working folder where the scripts can create folders beneath outputs.
  • Basic familiarity with SELECT, JOIN, GROUP BY, and aggregate functions.
  • Only the Python standard library is required: csv, pathlib, and sqlite3.

01State the business question before reviewing SQL

Suppose the requirement is: for every customer, report the total value of PAID orders placed during August 2026. Customers with no qualifying orders must still appear with a total of 0.

An AI assistant proposes an INNER JOIN between customers and orders, followed by WHERE conditions for status and date. The query looks reasonable, but the INNER JOIN removes customers who have no matching order rows. That violates the requirement to include zero-total customers.

02Create a synthetic SQLite database

The following database is synthetic and was written specifically for this article. It has three customers and six orders chosen to test paid versus unpaid status, month boundaries, and a customer with no August paid order.

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

03Compute the correct answer by hand

Ana has two PAID August orders: 40 and 60, so her total is 100. Her cancelled order does not count. Ben has one qualifying August order worth 25; his September order does not count. Cara has only a cancelled August order, so she must still appear with 0.

customer_idcustomer_nameExpected August paid total
1Ana100
2Ben25
3Cara0

The expected grand total is 125. There should be exactly 3 output rows because the requirement says every customer must appear.

04Compare the AI query with a revised query

The AI query below filters qualifying orders in the WHERE clause after an INNER JOIN. Cara has no matching PAID August order, so she disappears entirely.

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;

The revised query starts from all customers and uses a LEFT JOIN. The order filters are placed inside the ON condition so nonmatching customers remain. COALESCE converts the resulting NULL aggregate to 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;

05Run both queries and compare them with the expected rows

Save the following script as ai_sql_check.py. It runs both queries, compares their results with the hand-computed expected table, and writes a CSV review report. It does not modify the synthetic database.

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

06Check the expected output

The AI query should return only Ana and Ben, so it produces 2 rows and does not match the expected result. The revised query should return all 3 customers and exactly match the hand-computed table.

QueryRows returnedExpected grand totalMatches expected table
AI2125No
Revised3125Yes

The AI result can still have the correct grand total of 125 even though it is wrong because Cara's required zero row is missing. This is why checking only totals is insufficient.

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

07Add checks and understand the limits

  • Check row count as well as totals.
  • Include customers with zero matching records in the synthetic data when the requirement says they must appear.
  • Test date boundaries such as August 31 and September 1.
  • Include excluded statuses such as CANCELLED.
  • Run the checker again without changing OUTPUT_DIR. It should stop with FileExistsError rather than overwrite the report.
Common problemWhat to inspect
Unexpected missing rowsCheck JOIN type and whether filters in WHERE remove unmatched LEFT JOIN rows.
Totals too largeLook for one-to-many joins that duplicate rows before aggregation.
Wrong date rangeUse an explicit lower bound and exclusive upper bound appropriate to the stored date format.
NULL instead of zeroDecide whether missing matches should stay NULL or be converted with COALESCE.
Correct total but wrong detailCompare complete expected rows, not only the grand total.

This test proves only that the revised query matches this small synthetic example. Real schemas may contain duplicate relationships, NULL values, timestamps, time zones, refunds, or additional business rules. Expand the test data whenever those conditions matter.

Execution and verification record

2026-09-20 · hand-checked example · target: Python 3.12 · standard library: csv, pathlib, sqlite3 · no execution

  • Manually calculated Ana's qualifying August total as 40 + 60 = 100.
  • Manually calculated Ben's qualifying August total as 25 and excluded his September order.
  • Manually determined that Cara must appear with 0 because her only August order is CANCELLED.
  • Manually calculated the expected grand total as 125 and expected row count as 3.
  • Inspected the AI query and determined that its INNER JOIN and WHERE filters omit Cara, producing 2 rows.
  • Inspected the revised LEFT JOIN query and expected it to return the three hand-computed rows.
  • Derived the expected console output and report comparison by hand.
Verification limits
  • The code was not executed by the author of this response; SQLite and filesystem behavior were not tested here.
  • Only the listed synthetic rows were evaluated; NULL amounts, duplicate joins, timestamps, time zones, refunds, and large datasets were not tested.
  • Matching this example does not prove the query is correct for every production schema or business rule.
  • The official documentation URLs were provided from known documentation locations but were not checked live.

Site-wide writing and verification principles

References

The explanations and examples were written for this site. See the official sources below for the related behavior and concepts.