You're reviewing a colleague's query and you see a JOIN with no ON clause. No column specified, no condition, nothing. The query still runs and returns rows. That's a NATURAL JOIN. It looks concise, but understanding what it's actually doing, and what it can do wrong, is what this lesson covers.
How NATURAL JOIN Matches Columns
NATURAL JOIN removes the ON clause entirely. Instead of you specifying which columns to match, the database scans both tables and joins on every column name they share.
The syntax looks like this:
-- conceptual example: not all databases support this syntax
SELECT *
FROM orders
NATURAL JOIN customers
In the ecommerce schema, orders has a customer_id column. customers also has a customer_id column. A NATURAL JOIN between these two tables would match on customer_id automatically, because that's the only column name they share.
The equivalent explicit [INNER JOIN](sqlround.com/lessons/sql-inner-join) looks like this, and this is what you should actually run:
SELECT o.order_id, o.order_date, c.first_name, c.last_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
Same result, but you control exactly which column is used for matching. Nothing happens implicitly.
Where NATURAL JOIN Gets Dangerous
The ecommerce schema illustrates the risk clearly. Look at orders and payments:
orders has: order_id, customer_id, employee_id, order_date, status, total_amount
payments has: payment_id, order_id, payment_date, amount, method, status
These two tables share two column names: order_id and status. A NATURAL JOIN between them would match on both columns simultaneously. That means it would only return rows where orders.order_id = payments.order_id AND orders.status = payments.status. If an order has status = 'delivered' but the linked payment has status = 'completed', that row disappears from the results entirely, with no warning.
The correct join uses only the intended key:
SELECT o.order_id, o.status AS order_status, pay.status AS payment_status, pay.amount
FROM orders o
JOIN payments pay ON o.order_id = pay.order_id
This returns every order-payment pair correctly, regardless of whether the status values match. The explicit ON clause gives you control. NATURAL JOIN takes that control away.
The same problem appears between customers and employees. Both tables have first_name, last_name, and email. A NATURAL JOIN on those two tables would match on all three columns at once, which is meaningless and would produce no useful rows.
When You Might See NATURAL JOIN in the Wild
Some SQL textbooks introduce NATURAL JOIN as a shortcut for clean schemas where every join column has a unique name. In those controlled environments, it works. You join orders to customers and the only shared column is customer_id, so the join is unambiguous.
In practice, though, schemas grow. Tables gain new columns. Names collide. A NATURAL JOIN that worked correctly last year can silently return wrong results after a schema change, because a new shared column name gets pulled into the join condition automatically.
This is why most professional teams treat NATURAL JOIN as something to recognise and replace, not something to write. If you encounter it in existing code, convert it to an explicit JOIN with an ON clause. The behaviour becomes visible, the intent becomes clear, and the query stays correct even if the schema changes.
How NATURAL JOIN Connects to Other Concepts
NATURAL JOIN is a variant of [INNER JOIN](sqlround.com/lessons/sql-inner-join). The matching logic is the same: only rows that satisfy the join condition appear in the result. The difference is who specifies the condition.
The [USING clause](sqlround.com/lessons/sql-join-using) is a middle ground between NATURAL JOIN and a full ON clause. Instead of matching all shared column names automatically, USING lets you name exactly one column to join on, without repeating the table prefix. It's safer than NATURAL JOIN and slightly more concise than ON.
Common Mistakes
1. Assuming NATURAL JOIN only matches on the intended key
When two tables share more than one column name, NATURAL JOIN uses all of them, which filters out rows you didn't intend to exclude.
-- wrong: this would match on both order_id AND status if NATURAL JOIN were supported
SELECT *
FROM orders
NATURAL JOIN payments
Use an explicit ON clause to control exactly which column connects the two tables.
-- correct
SELECT o.order_id, o.status AS order_status, pay.amount
FROM orders o
JOIN payments pay ON o.order_id = pay.order_id
2. Writing NATURAL JOIN in a database that doesn't support it
NATURAL JOIN is not supported in SQLite and a few other SQL engines. The query errors immediately with no useful output.
-- wrong: will error in SQLite and other engines that don't support NATURAL JOIN
SELECT *
FROM orders
NATURAL JOIN customers
Write an explicit INNER JOIN instead. It works across all major SQL databases.
-- correct
SELECT o.order_id, c.first_name, c.last_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
3. Using SELECT * with NATURAL JOIN and expecting duplicate columns
NATURAL JOIN deduplicates the shared column in the output, returning only one copy of it. This can cause confusion if you're expecting to alias or reference both sides of the join column separately.
-- wrong assumption: both customer_id columns won't appear; only one does
SELECT *
FROM orders
NATURAL JOIN customers
With an explicit JOIN, you see exactly which columns come from which table and can alias them clearly.
-- correct
SELECT o.order_id, o.customer_id, c.first_name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
Quick Recap
- NATURAL JOIN joins two tables automatically on every column name they share, with no ON clause required.
- When tables share more than one column name, NATURAL JOIN uses all of them, which often produces incorrect results silently.
- NATURAL JOIN is not supported in all SQL engines, including SQLite.
- An explicit INNER JOIN with an ON clause does the same work, stays correct as schemas change, and makes the join condition visible to anyone reading the query.
- If you see NATURAL JOIN in existing code, convert it to an explicit JOIN with ON before making any other changes.
- The USING clause is a safer alternative to NATURAL JOIN when you want a slightly shorter syntax without giving up control over the join column.
order_id, customer_id, and status for every row in the orders table where status is equal to 'delivered'.orders and filter with WHERE on the status column.SELECT o.order_id, o.customer_id, o.status
FROM orders o
WHERE o.status = 'delivered'
payment_id, order_id, amount, and status for every row in the payments table, sorted by amount descending.payments and sort with ORDER BY amount DESC.SELECT pay.payment_id, pay.order_id, pay.amount, pay.status
FROM payments pay
ORDER BY pay.amount DESC
first_name and last_name of each customer alongside the order_id and order_date of every order they placed, using an explicit JOIN on customer_id.customers and orders on customer_id with an ON clause.SELECT c.first_name, c.last_name, o.order_id, o.order_date
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
first_name and last_name of each customer alongside their order_id and total_amount, but only for orders where total_amount is greater than 200, sorted by total_amount descending.customers and orders on customer_id, filter with WHERE on total_amount, then ORDER BY.SELECT c.first_name, c.last_name, o.order_id, o.total_amount
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.total_amount > 200
ORDER BY o.total_amount DESC
order_id and total_amount from orders alongside the amount and method from payments, joined explicitly on order_id, for orders where the payment method is 'credit_card'.orders and payments on order_id with an ON clause, then filter with WHERE on pay.method.SELECT o.order_id, o.total_amount, pay.amount, pay.method
FROM orders o
JOIN payments pay ON o.order_id = pay.order_id
WHERE pay.method = 'credit_card'
NATURAL JOIN automatically joins two tables on every column that shares the same name in both tables. You don't write an ON clause; the database figures out the matching columns itself.
When the matching column is correct, the result is the same as an INNER JOIN with an ON clause. The difference is that NATURAL JOIN picks the join columns automatically, which can produce wrong results if column names match unexpectedly.
Because it depends entirely on column names. If two tables happen to share a column name that isn't meant to be a join key, NATURAL JOIN silently joins on that column and returns wrong data with no error.
No. NATURAL JOIN is not supported in all SQL engines. SQLite, for example, does not support it. MySQL and PostgreSQL do, but best practice in most teams is to use explicit JOINs with ON clauses instead.