Lesson 45

SQL NATURAL JOIN: How It Works and Why to Avoid It

Understand how NATURAL JOIN matches columns automatically so you can recognise it in existing code and know when to use an explicit JOIN instead.

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.

Key idea: NATURAL JOIN matches on column names, not on meaning. If two columns share a name by coincidence, the join uses both, and the results are wrong.

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.

Key idea: NATURAL JOIN is schema-dependent. Explicit JOINs are query-dependent. Explicit JOINs stay correct as schemas evolve.

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.
Exercise 1 Easy
Write a query to return the order_id, customer_id, and status for every row in the orders table where status is equal to 'delivered'.
Hint: SELECT the three columns from orders and filter with WHERE on the status column.
Solution
SELECT o.order_id, o.customer_id, o.status
FROM orders o
WHERE o.status = 'delivered'
Exercise 2 Easy
Write a query to return the payment_id, order_id, amount, and status for every row in the payments table, sorted by amount descending.
Hint: SELECT the four columns from payments and sort with ORDER BY amount DESC.
Solution
SELECT pay.payment_id, pay.order_id, pay.amount, pay.status
FROM payments pay
ORDER BY pay.amount DESC
Exercise 3 Medium
Write a query to return the 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.
Hint: JOIN customers and orders on customer_id with an ON clause.
Solution
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
Exercise 4 Medium
Write a query to return the 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.
Hint: JOIN customers and orders on customer_id, filter with WHERE on total_amount, then ORDER BY.
Solution
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
Exercise 5 Medium
Write a query to return the 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'.
Hint: JOIN orders and payments on order_id with an ON clause, then filter with WHERE on pay.method.
Solution
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'
Frequently Asked Questions

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.