SQL Natural Join and JOIN... USING Clause
SQL provides several syntax options for joining tables. Beyond the standard JOIN ... ON syntax you likely already know, two alternatives exist that can make your queries shorter and cleaner: NATURAL JOIN and JOIN ... USING. Both rely on matching column names between tables to determine how rows should be combined.
This guide explains how each one works, shows real examples with outputs, highlights the serious risks that come with NATURAL JOIN, and helps you decide when these shortcuts are convenient and when they should be avoided entirely.
What Is a NATURAL JOIN?
A SQL NATURAL JOIN automatically joins two tables on all columns that share the same name in both tables. You do not write an ON clause or specify which columns to match. The database engine inspects the table definitions, finds every column name that appears in both tables, and uses all of them as the join condition.
Basic Syntax
SELECT *
FROM table_a
NATURAL JOIN table_b;
That is the entire syntax. No ON, no USING, no explicit column list. The database does the matching for you.
A Simple Example
Consider two tables with a clearly shared column:
CREATE TABLE departments (
department_id INT PRIMARY KEY,
department_name VARCHAR(100)
);
CREATE TABLE employees (
employee_id INT PRIMARY KEY,
employee_name VARCHAR(100),
department_id INT,
salary DECIMAL(10,2)
);
INSERT INTO departments VALUES (1, 'Engineering');
INSERT INTO departments VALUES (2, 'Marketing');
INSERT INTO departments VALUES (3, 'Sales');
INSERT INTO employees VALUES (101, 'Alice', 1, 85000);
INSERT INTO employees VALUES (102, 'Bob', 1, 78000);
INSERT INTO employees VALUES (103, 'Carol', 2, 72000);
INSERT INTO employees VALUES (104, 'Dave', NULL, 65000);
Now join them naturally:
SELECT *
FROM employees
NATURAL JOIN departments;
Output:
| department_id | employee_id | employee_name | salary | department_name |
|---|---|---|---|---|
| 1 | 101 | Alice | 85000 | Engineering |
| 1 | 102 | Bob | 78000 | Engineering |
| 2 | 103 | Carol | 72000 | Marketing |
The database found that department_id exists in both tables and used it as the join condition automatically. It also deduplicated the shared column in the output: department_id appears only once, not twice.
Notice that Dave (who has a NULL department_id) and the Sales department (which has no employees) are both absent. A NATURAL JOIN performs an inner join by default.
You can also write NATURAL LEFT JOIN, NATURAL RIGHT JOIN, or NATURAL FULL JOIN to change the join type while still using automatic column matching.
-- Keep all employees, even those without a department
SELECT *
FROM employees
NATURAL LEFT JOIN departments;
| employee_id | employee_name | department_id | salary | department_name |
|---|---|---|---|---|
| 101 | Alice | 1 | 85000 | Engineering |
| 102 | Bob | 1 | 78000 | Engineering |
| 103 | Carol | 2 | 72000 | Marketing |
| 104 | Dave | null | 65000 | null |
The Risks of NATURAL JOIN
The convenience of NATURAL JOIN comes with significant dangers that make it unsuitable for most production code. The core problem is that join behavior is determined by column names, which can change without warning.
Risk 1: Unintended Column Matches
Imagine a developer adds an innocent-looking status column to both tables during a routine update:
ALTER TABLE departments ADD COLUMN status VARCHAR(20) DEFAULT 'active';
ALTER TABLE employees ADD COLUMN status VARCHAR(20) DEFAULT 'active';
Now the same NATURAL JOIN query silently changes its behavior:
SELECT *
FROM employees
NATURAL JOIN departments;
Before the schema change, the query joined on department_id alone. After the change, it joins on both department_id AND status. The query text did not change at all, but the results could be completely different.
This is the single biggest problem with NATURAL JOIN. A schema change in one table can silently alter the logic of queries in a completely different part of your application.
NATURAL JOIN creates an implicit contract with your schema. Any column added to either table that happens to share a name with a column in the other table will be silently absorbed into the join condition. This can produce wrong results without any error or warning.
Risk 2: No Visibility Into the Join Condition
When reading a NATURAL JOIN query, you cannot tell which columns are being used for the join without inspecting both table definitions. This makes code reviews harder and debugging slower.
-- What columns does this join on? You have no idea without checking the schema.
SELECT *
FROM invoices
NATURAL JOIN payments;
Compare that with an explicit join:
-- Immediately clear: joining on invoice_id
SELECT *
FROM invoices AS i
JOIN payments AS p ON i.invoice_id = p.invoice_id;
Risk 3: Common Utility Column Names
Many database schemas include columns like created_at, updated_at, name, status, type, or description across multiple tables. If two tables both have a created_at column, a NATURAL JOIN will try to match rows where the creation timestamps are identical, which is almost never what you want.
Example of silent breakage:
CREATE TABLE authors (
author_id INT PRIMARY KEY,
name VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE books (
book_id INT PRIMARY KEY,
author_id INT,
name VARCHAR(200),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
SELECT *
FROM authors
NATURAL JOIN books;
This query joins on three columns: author_id, name, and created_at. It almost certainly returns zero rows because it requires the author's name to equal the book's name and both records to have been created at the exact same timestamp. The query produces no error. It simply returns an empty result set, and you are left wondering why.
The correct explicit version:
SELECT
a.author_id,
a.name AS author_name,
b.book_id,
b.name AS book_title
FROM authors AS a
JOIN books AS b ON a.author_id = b.author_id;
Risk 4: Not Supported Everywhere the Same Way
While NATURAL JOIN is part of the SQL standard and supported by PostgreSQL, MySQL, SQLite, and Oracle, some database systems handle edge cases differently. SQL Server, notably, does not support NATURAL JOIN at all. If you write queries with NATURAL JOIN and later need to migrate to SQL Server, every one of those queries must be rewritten.
The USING Clause: A Safer Shortcut
The JOIN ... USING clause sits between the fully implicit NATURAL JOIN and the fully explicit JOIN ... ON. You still specify the join columns explicitly, but the syntax is shorter and the shared column is deduplicated in the result, just like with NATURAL JOIN.
Syntax
SELECT *
FROM table_a
JOIN table_b USING (shared_column);
You can list multiple columns inside USING when the join involves a composite key:
JOIN table_b USING (column1, column2)
Example
Using the same employees and departments tables from before:
SELECT *
FROM employees
JOIN departments USING (department_id);
Output:
| department_id | employee_id | employee_name | salary | department_name |
|---|---|---|---|---|
| 1 | 101 | Alice | 85000 | Engineering |
| 1 | 102 | Bob | 78000 | Engineering |
| 2 | 103 | Carol | 72000 | Marketing |
The result is identical to the NATURAL JOIN output. The difference is that you explicitly told the database to join on department_id. If someone adds a status column to both tables, this query continues to join only on department_id. It is immune to schema changes affecting unrelated columns.
USING with Multiple Columns
When a join requires more than one column, list them all inside the parentheses:
CREATE TABLE enrollments (
student_id INT,
course_id INT,
semester VARCHAR(10),
grade CHAR(2)
);
CREATE TABLE attendance (
student_id INT,
course_id INT,
semester VARCHAR(10),
classes_attended INT
);
SELECT *
FROM enrollments
JOIN attendance USING (student_id, course_id, semester);
This is cleaner than the equivalent ON version:
SELECT *
FROM enrollments AS e
JOIN attendance AS a
ON e.student_id = a.student_id
AND e.course_id = a.course_id
AND e.semester = a.semester;
Both produce the same result, but USING reduces repetition when the column names are identical in both tables.
USING Deduplicates the Shared Column
One subtle but important difference between USING and ON is how SELECT * handles the joined column.
With ON:
SELECT *
FROM employees AS e
JOIN departments AS d ON e.department_id = d.department_id;
| employee_id | employee_name | department_id | salary | department_id | department_name |
|---|---|---|---|---|---|
| 101 | Alice | 1 | 85000 | 1 | Engineering |
| 102 | Bob | 1 | 78000 | 1 | Engineering |
| 103 | Carol | 2 | 72000 | 2 | Marketing |
The department_id column appears twice in the output.
With USING:
SELECT *
FROM employees
JOIN departments USING (department_id);
| employee_id | employee_name | department_id | salary | department_name |
|---|---|---|---|---|
| 101 | Alice | 1 | 85000 | Engineering |
| 102 | Bob | 1 | 78000 | Engineering |
| 103 | Carol | 2 | 72000 | Marketing |
The department_id column appears once. This deduplication is part of the SQL standard behavior for USING.
While SELECT * is generally discouraged in production code, the deduplication behavior of USING can be helpful during exploratory queries and debugging sessions.
USING vs ON: A Side-by-Side Comparison
| Feature | JOIN ... ON | JOIN ... USING |
|---|---|---|
| Explicit column specification | Yes | Yes |
| Columns must have the same name | No | Yes |
Supports non-equality conditions (<, >, !=) | Yes | No |
| Supports joining on expressions | Yes | No |
Deduplicates shared columns in SELECT * | No | Yes |
| Works in all major databases | Yes | Most (not all versions) |
| Requires table alias to reference join column | Yes (must qualify) | No (unqualified reference works) |
When You Cannot Use USING
The USING clause requires that the join columns have the same name in both tables. If they differ, you must use ON:
-- Column names differ: "id" vs "customer_id"
-- USING will not work here
SELECT *
FROM customers AS c
JOIN orders AS o ON c.id = o.customer_id;
You also cannot use USING for non-equality join conditions:
-- Range join: USING cannot express this
SELECT *
FROM products AS p
JOIN price_history AS ph
ON p.product_id = ph.product_id
AND ph.effective_date <= CURRENT_DATE
AND ph.expiry_date > CURRENT_DATE;
When to Use Each Approach
Use NATURAL JOIN Only For...
Honestly, the safest advice is: avoid NATURAL JOIN in application code entirely. The risk of silent breakage from schema changes far outweighs the convenience of saving a few keystrokes.
That said, there are two narrow scenarios where NATURAL JOIN is acceptable:
- Ad hoc exploration. You are investigating data in a terminal session and you know the schema well. Speed of typing matters more than long-term maintainability.
- Disposable scripts. One-time data migration scripts that will never be run again and will be deleted after use.
Even in these cases, USING is almost always a better choice because it takes only a few extra characters and gives you explicit control.
Use USING When...
- The join columns have identical names in both tables.
- You want a cleaner, shorter syntax than
ONwithout sacrificing explicitness. - You are joining on a single shared column or a composite key where all parts share names.
- You are writing queries that will be read by a team and you want the intent to be immediately clear.
-- Clean and explicit
SELECT
order_id,
order_date,
product_name,
quantity
FROM orders
JOIN order_items USING (order_id)
JOIN products USING (product_id);
Use ON When...
- The join columns have different names across tables.
- You need non-equality conditions in the join (ranges, inequalities, expressions).
- You want maximum compatibility across all database engines.
- You are working in a codebase where the team standard is
ONfor consistency. - The join condition is complex and involves multiple unrelated columns or computed values.
-- Different column names
SELECT *
FROM users AS u
JOIN audit_log AS a ON u.user_id = a.performed_by;
-- Complex condition
SELECT *
FROM orders AS o
JOIN discounts AS d
ON o.order_date BETWEEN d.start_date AND d.end_date
AND o.region = d.applicable_region;
Common Mistake: Mixing USING and Table-Qualified References
When you use USING, the shared column becomes a single unqualified column in the result set. Some databases will raise an error if you try to qualify it with a table alias.
Wrong (may fail in PostgreSQL and others):
SELECT
e.department_id, -- Error: column reference is ambiguous with USING
e.employee_name,
d.department_name
FROM employees AS e
JOIN departments AS d USING (department_id);
Correct:
SELECT
department_id, -- Unqualified: it belongs to neither table specifically
e.employee_name,
d.department_name
FROM employees AS e
JOIN departments AS d USING (department_id);
The behavior of qualifying a USING column varies between databases. PostgreSQL enforces that the column must be unqualified. MySQL allows the qualification. To write portable SQL, always leave USING columns unqualified.
Combining USING in Multi-Table Joins
USING works naturally in multi-table join chains, just like ON:
SELECT
customer_id,
c.first_name,
order_id,
o.order_date,
product_id,
p.product_name,
oi.quantity,
oi.unit_price
FROM customers AS c
JOIN orders AS o USING (customer_id)
JOIN order_items AS oi USING (order_id)
JOIN products AS p USING (product_id)
ORDER BY order_id;
Compare the same query with ON:
SELECT
c.customer_id,
c.first_name,
o.order_id,
o.order_date,
p.product_id,
p.product_name,
oi.quantity,
oi.unit_price
FROM customers AS c
JOIN orders AS o ON c.customer_id = o.customer_id
JOIN order_items AS oi ON o.order_id = oi.order_id
JOIN products AS p ON oi.product_id = p.product_id
ORDER BY o.order_id;
Both produce identical results. The USING version is slightly shorter, and the shared key columns do not need alias prefixes. Choose whichever style your team prefers, but be consistent.
Quick Reference Table
| Approach | Explicitness | Safety | Brevity | Best For |
|---|---|---|---|---|
NATURAL JOIN | None | Low | Highest | Ad hoc exploration only |
JOIN ... USING | Moderate | High | Moderate | Same-name columns, clean syntax |
JOIN ... ON | Full | Highest | Lowest | All scenarios, maximum control |
Summary
NATURAL JOIN is a SQL feature that automatically matches tables on all shared column names. While it produces compact syntax, it is fragile and dangerous for production use because schema changes can silently alter query behavior. Unrelated columns that happen to share a name will be included in the join condition without any warning.
JOIN ... USING provides a middle ground. You explicitly list which columns to join on, protecting against schema drift, while enjoying a cleaner syntax than ON. It deduplicates the shared column in results and reads well in multi-table join chains.
JOIN ... ON remains the most versatile and universally supported option. It handles different column names, complex conditions, and works identically across all major databases.
For most teams and projects, the recommendation is straightforward:
- Default to
ONfor maximum clarity and flexibility. - Use
USINGwhen column names match and you want cleaner queries. - Avoid
NATURAL JOINin any code that will be maintained, reviewed, or run in production.