Back to Blog

Why Your AI's Join Returned Millions of Rows

Asked for orders with customer names and got a huge result or a timeout. The JOIN has no ON, or ON 1=1, or a comma-style FROM a, b. That's a Cartesian product — every order times every customer.

You ask for "orders with customer name." The AI writes a join. You get a timeout, a 500-row truncate, or a revenue number with extra zeros. There is no real join key. Every order is paired with every customer.

A join without a key is a multiplication

-- Cartesian product
SELECT o.id, c.name
FROM orders o
JOIN customers c          -- no ON
-- or
FROM orders o, customers c
-- or
FROM orders o
JOIN customers c ON 1 = 1

10,000 orders × 8,000 customers = 80 million rows. Valid SQL. Useless answer. This is not fan-out from line items (a few extra rows per order). Fan-out is "3× too high." This is "thousands of times too high," or the query never finishes (timeout).

The model does this when it hasn't called describe_table, can't find customer_id, and still wants both tables in one SELECT — the same skip as guessing column names.

How to catch it

COUNT(*) FROM orders
COUNT(*) FROM customers
COUNT(*) of your join with no LIMIT
If join ≈ orders × customers, the ON is missing. Show the ON clause.

Also look at distinct order_id vs row count. If every order id repeats once per customer, it's a cross join wearing an inner-join costume.

The fix

SELECT o.id AS order_id, c.id AS customer_id, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id

Qualify ids so you don't also hit the wrong-id-after-join bug. If there is no customer_id, say so — don't invent ON 1=1.

The prompt that heads this off

Every JOIN needs an ON that matches a real key (usually table_id).
Never FROM a, b and never ON 1=1. If you can't find the key,
describe_table both sides and ask me — don't cross-join.
After a join, compare COUNT(*) to the left table before summing money.

The short version

No ON (or ON 1=1) is not "a loose join." It is every row times every row. If the result exploded or timed out, read the ON before you raise the timeout.


Read-only MCP gateway: mcpserver.design.

Related reading: