Search⌘ K
AI Features

Text-to-SQL over BigQuery

Explore how to build accurate text-to-SQL queries over BigQuery by understanding data grain and join effects like fanout. Learn to validate SQL with dry-run cost controls, enforce security boundaries, and ensure precise revenue calculations. This lesson helps you trace aggregation errors, apply business rules correctly, and write testable regressions to defend query correctness in real scenarios.

The analytics agent reports $300 of East paid-order revenue for September, although the two qualifying order headers each contain $100. Money is stored in integer cents: 10000 cents = $100. The SQL ran successfully within the scan budget. Trace the source rows to find what those checks missed.

Tracing the reported revenue

The independent example uses two tables:

  • orders: order_id, region, business_date, status, revenue_cents.

  • order_lines: line_id, order_id, product_code.

Here are all six order headers, with money stored as integer cents:

O0 East 2026-08-31 paid 6000
O1 East 2026-09-01 paid 10000
O2 East 2026-09-30 paid 10000
O3 West 2026-09-15 paid 8000
O4 East 2026-09-20 cancelled 5000
O5 East 2026-10-01 paid 7000

O1 has lines L11 and L12, both product P. O2 has L21, product Q. Every other order has one product-Q line. Consequently, this dataset does not yet test orders without lines.

The generated query is:

SELECT o.region, SUM(o.revenue_cents) AS revenue_cents
FROM orders o
JOIN order_lines l ON o.order_id = l.order_id
WHERE o.region = 'East' AND o.status = 'paid'
AND o.business_date >= '2026-09-01'
AND o.business_date < '2026-10-01'
GROUP BY o.region;
  • Line 1: Select the region and total the header amounts, naming the output revenue_cents.

  • Lines 2–3: Match each order with every line bearing its order ID.

  • Lines 4–6: Retain East paid orders within the September interval.

  • Line 7: Produce one aggregate group per region.

Before tracing it, predict which source fact explains the extra $100.

Trace the Extra 10000 Cents

Q1. The query above reports 30000 cents, but qualifying order headers total 10000 + 10000 = 20000 cents. Which diagnosis and concrete source fact explain the discrepancy?

Do you find this helpful?

Inspect the intermediate rows to check which facts enter the total.

Reading the joined rows

An alias is a short name: o means orders, and l means order_lines. A qualified column, such as o.order_id, identifies which table supplies the value.

A table’s grain states what one row represents. The order table has one row per order; the line table has one row per order line. Read this query conceptually in the following order; the database optimizer can choose a different physical execution plan.

  1. FROM and JOIN: Combine matching rows. O1 matches two lines, so its header amount appears twice.

  2. WHERE: Filter that combined collection by region, status, and date.

  3. GROUP BY: Put the surviving rows into region groups.

  4. SUM: Add the amounts within each group.

  5. SELECT: Emit the requested output columns.

The surviving joined rows are:

order_id line_id header revenue_cents
O1 L11 10000
O1 L12 10000
O2 L21 10000

The sum is 10000 + 10000 + 10000 = 30000 cents. This repetition of a parent fact across child rows is called join fanout.

Here, COUNT(*) counts three joined rows, while COUNT(DISTINCT o.order_id) counts two orders. An inner join also removes an order with no matching line; the supplied dataset cannot expose that omission ...

Two qualifying order headers connect to three matching order lines, producing three joined rows with header revenue repeated: O1 contributes 10000 cents through L11 and L12, and O2 contributes 10000 cents through L21. The joined total is 30000 cents (10000 + 10000 + 10000), versus 20000 cents (10000 + 10000) with header-only aggregation.
Two qualifying order headers connect to three matching order lines, producing three joined rows with header revenue repeated: O1 contributes 10000 cents through L11 and L12, and O2 contributes 10000 cents through L21. The joined total is 30000 cents (10000 + 10000 + 10000), versus 20000 cents (10000 + 10000) with header-only aggregation.
...