Loading content...
SQL has evolved over decades, and this evolution is visible in the multiple ways you can express the same join operation. Legacy syntax from the 1980s coexists with modern ANSI standards from the 1990s and beyond. Understanding these variations is essential because you'll encounter all of them in production codebases, especially when maintaining legacy systems or working across different database platforms.
The syntax you choose affects more than aesthetics—it impacts readability, maintainability, error-proneness, and even portability across database systems. This page explores all major join syntax variations, their history, tradeoffs, and recommendations for modern SQL development.
By the end, you'll understand why modern syntax exists, when you might encounter legacy patterns, and how to write joins that are clear, correct, and portable.
You will master ANSI-style JOIN ... ON syntax, understand legacy comma-joined syntax and its pitfalls, learn the USING clause for cleaner equi-joins, compare syntax across major databases, and develop guidelines for writing portable, maintainable join queries.
Understanding why multiple syntaxes exist requires a brief journey through SQL's evolution:
Pre-1992: The Comma Era
Early SQL implementations (1970s-1980s) used a simple approach:
This was based on the mathematical definition: Cartesian product (comma) + selection (WHERE) = join.
1992: ANSI SQL-92 Standard
The SQL-92 standard introduced explicit JOIN syntax:
INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOINON clause for join conditionsUSING clause for common column namesThis wasn't just syntax sugar—it separated join logic from filter logic and enabled outer joins that were awkward with comma syntax.
Today: Modern Practice
While legacy syntax remains valid in all databases (for backward compatibility), modern SQL uses ANSI-style joins almost universally. Legacy comma joins persist in old code, quick queries, and by developers who learned SQL before 1992.
| Era | Primary Syntax | Characteristics | Status Today |
|---|---|---|---|
| Pre-1992 | FROM A, B WHERE A.key = B.key | Comma-separated, conditions in WHERE | Deprecated but functional |
| SQL-92 | FROM A JOIN B ON A.key = B.key | Explicit join keywords, ON clause | Current standard |
| SQL-92 | FROM A JOIN B USING (key) | Simplified equi-join syntax | Convenient for simple joins |
| SQL-92 | FROM A NATURAL JOIN B | Implicit join on same-named columns | Generally discouraged |
Database vendors never remove old syntax—doing so would break millions of existing queries. This is why comma joins still work. But 'works' doesn't mean 'recommended.' Just because you can write joins the old way doesn't mean you should.
The ANSI-style join uses explicit JOIN keywords with the ON clause specifying the join condition. This is the modern standard and recommended approach for all new SQL development.
12345678910111213141516171819202122232425262728293031323334353637
-- ANSI-style INNER JOINSELECT c.name AS customer_name, o.order_id, o.order_date, o.total_amountFROM Customers AS cINNER JOIN Orders AS o ON c.customer_id = o.customer_idWHERE o.order_date >= '2024-01-01' AND o.total_amount > 100; -- Structure analysis:-- FROM clause: Specifies tables and HOW they relate (JOIN type + ON condition)-- WHERE clause: Filters the joined result (only non-join conditions) -- Multiple ANSI joinsSELECT c.name AS customer_name, o.order_id, p.product_name, oi.quantityFROM Customers AS cINNER JOIN Orders AS o ON c.customer_id = o.customer_idINNER JOIN Order_Items AS oi ON o.order_id = oi.order_idINNER JOIN Products AS p ON oi.product_id = p.product_idWHERE o.status = 'shipped'ORDER BY o.order_date DESC; -- Mixed INNER and OUTER joinsSELECT c.name AS customer_name, o.order_id, r.return_dateFROM Customers AS cLEFT JOIN Orders AS o ON c.customer_id = o.customer_idLEFT JOIN Returns AS r ON o.order_id = r.order_idWHERE c.registration_date >= '2023-01-01';LEFT JOIN tells you rows might be unmatched.JOIN B without an ON clause (syntax error).Unless you have a compelling reason (legacy code maintenance, specific database quirk), always use ANSI-style joins. They're clearer, safer, and the universal standard. Your future self and colleagues will thank you.
The legacy comma syntax (also called implicit join or theta-style join) lists tables separated by commas in the FROM clause, with all conditions in the WHERE clause. This was the only way to join before SQL-92.
1234567891011121314151617181920212223242526272829
-- Legacy comma syntax: Tables comma-separated, conditions in WHERESELECT c.name AS customer_name, o.order_id, o.order_dateFROM Customers c, Orders oWHERE c.customer_id = o.customer_id -- Join condition mixed with... AND o.order_date >= '2024-01-01' -- Filter conditions AND o.status = 'completed'; -- Multiple tables with comma syntaxSELECT c.name, o.order_id, p.product_name, oi.quantityFROM Customers c, Orders o, Order_Items oi, Products pWHERE c.customer_id = o.customer_id -- 4 tables need 3+ join conditions AND o.order_id = oi.order_id AND oi.product_id = p.product_id AND o.total_amount > 500; -- Filter mixed in with joins -- The problem: Forgetting a condition creates a Cartesian product-- This query has a "missing" join conditionSELECT c.name, o.order_id, p.product_nameFROM Customers c, Orders o, Products pWHERE c.customer_id = o.customer_id;-- Missing: AND o.order_id = oi.order_id AND oi.product_id = p.product_id-- Result: Every order paired with EVERY product (Cartesian explosion!)FROM Customers c, Orders o
WHERE c.id = o.customer_id
AND o.date > '2024-01-01'
Join and filter mixed together. Easy to miss a condition.
FROM Customers c
JOIN Orders o ON c.id = o.customer_id
WHERE o.date > '2024-01-01'
Join logic in JOIN, filters in WHERE. Clear separation.
Despite being deprecated, comma joins appear in: legacy systems written before the 1990s, quick ad-hoc queries by experienced developers, generated SQL from old ORM tools, database examples in older textbooks. Know how to read them, but don't write new ones.
Before ANSI outer joins were widely adopted, Oracle developed a proprietary syntax for outer joins using the (+) operator. You may encounter this in legacy Oracle systems.
The (+) Operator:
The (+) is placed after the column name on the 'optional' side of the join—the side that might not have matching rows.
12345678910111213141516171819202122232425
-- Oracle proprietary LEFT OUTER JOIN with (+)-- The (+) goes on the side that might be NULL (the optional side)SELECT c.name, o.order_idFROM Customers c, Orders oWHERE c.customer_id = o.customer_id(+);-- The (+) after o.customer_id means: "Orders is optional"-- Equivalent to: Customers LEFT OUTER JOIN Orders -- ANSI equivalent (preferred)SELECT c.name, o.order_idFROM Customers cLEFT OUTER JOIN Orders o ON c.customer_id = o.customer_id; -- Oracle proprietary RIGHT OUTER JOINSELECT c.name, o.order_idFROM Customers c, Orders oWHERE c.customer_id(+) = o.customer_id;-- The (+) after c.customer_id means: "Customers is optional"-- Equivalent to: Customers RIGHT OUTER JOIN Orders -- Complex (+) example with multiple conditionsSELECT c.name, o.order_id, o.statusFROM Customers c, Orders oWHERE c.customer_id = o.customer_id(+) AND o.status(+) = 'completed'; -- (+) needed on all optional-side conditions!Even if you're working exclusively with Oracle, use ANSI-style joins. The (+) syntax is a relic. It's less readable, less portable, and officially discouraged by Oracle themselves in favor of standard JOIN syntax.
The USING clause is an alternative to ON for equi-joins when the joining columns have identical names in both tables. It's a convenience syntax that reduces redundancy.
1234567891011121314151617181920212223242526272829303132
-- Standard ON clause (explicit, always works)SELECT c.name, o.order_idFROM Customers cJOIN Orders o ON c.customer_id = o.customer_id; -- USING clause (when columns have same name in both tables)SELECT c.name, o.order_idFROM Customers cJOIN Orders o USING (customer_id); -- USING with multiple columns (composite key)SELECT oi.quantity, sh.shipped_dateFROM Order_Items oiJOIN Shipment_Lines sh USING (order_id, line_number);-- Equivalent to: ON oi.order_id = sh.order_id -- AND oi.line_number = sh.line_number -- Important behavior: USING columns appear ONCE in result-- With ON:SELECT * -- customer_id appears TWICE (c.customer_id and o.customer_id)FROM Customers cJOIN Orders o ON c.customer_id = o.customer_id; -- With USING:SELECT * -- customer_id appears ONCE (merged column)FROM Customers cJOIN Orders o USING (customer_id); -- USING with OUTER joinsSELECT c.name, o.order_idFROM Customers cLEFT JOIN Orders o USING (customer_id);Microsoft SQL Server (T-SQL) does NOT support the USING clause. If portability to SQL Server is a concern, stick with ON syntax. PostgreSQL, MySQL, Oracle, and SQLite all support USING.
When to Use USING:
When to Avoid USING:
customer_id vs cust_id)NATURAL JOIN is the most implicit join syntax—it automatically matches all columns with identical names between tables, with no explicit condition specified.
12345678910111213141516171819202122232425262728
-- NATURAL JOIN: No explicit condition, matches same-named columnsSELECT c.name, d.department_nameFROM Employees cNATURAL JOIN Departments d; -- If both tables have 'department_id', this is equivalent to:SELECT c.name, d.department_nameFROM Employees cJOIN Departments d USING (department_id); -- And equivalent to:SELECT c.name, d.department_nameFROM Employees cJOIN Departments d ON c.department_id = d.department_id; -- THE DANGER: NATURAL JOIN includes ALL matching column names! -- If Employees has: id, name, department_id, created_at, updated_by-- And Departments has: id, department_name, department_id, created_at, updated_by-- NATURAL JOIN matches on: id, department_id, created_at, updated_by (4 columns!) -- This is almost certainly NOT what you wanted-- Employee 1 only matches Department 1 AND where created_at matches, etc. -- NATURAL OUTER joins also existSELECT c.name, d.department_nameFROM Employees cNATURAL LEFT JOIN Departments d;NATURAL JOIN is almost universally condemned in professional SQL development. Its implicit behavior makes queries fragile—adding a column named 'status' or 'created_at' to a table silently changes join behavior. Use explicit ON conditions for predictable, maintainable queries.
If your SQL needs to run on multiple database platforms, understanding syntax portability is crucial. Not all join features are universally supported.
| Feature | PostgreSQL | MySQL | SQL Server | Oracle | SQLite |
|---|---|---|---|---|---|
| INNER JOIN ... ON | ✓ | ✓ | ✓ | ✓ | ✓ |
| LEFT/RIGHT JOIN ... ON | ✓ | ✓ | ✓ | ✓ | ✓ |
| FULL OUTER JOIN | ✓ | ✗ (emulate) | ✓ | ✓ | ✓ (3.39+) |
| CROSS JOIN | ✓ | ✓ | ✓ | ✓ | ✓ |
| JOIN ... USING | ✓ | ✓ | ✗ | ✓ | ✓ |
| NATURAL JOIN | ✓ | ✓ | ✗ | ✓ | ✓ |
| Comma join (legacy) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Oracle (+) syntax | ✗ | ✗ | ✗ | ✓ | ✗ |
1234567891011121314151617181920212223242526272829303132333435
-- MOST PORTABLE: Use INNER/LEFT/RIGHT JOIN with ON-- Works on ALL major databasesSELECT c.name, o.order_idFROM Customers cINNER JOIN Orders o ON c.customer_id = o.customer_id; -- EMULATING FULL OUTER JOIN in MySQL-- MySQL doesn't support FULL OUTER JOIN, but you can emulate:SELECT c.name, o.order_idFROM Customers cLEFT JOIN Orders o ON c.customer_id = o.customer_idUNION ALLSELECT c.name, o.order_id FROM Customers cRIGHT JOIN Orders o ON c.customer_id = o.customer_idWHERE c.customer_id IS NULL; -- Only unmatched from right side -- LATERAL JOIN / CROSS APPLY variations (advanced)-- PostgreSQL: LATERALSELECT c.name, recent.order_idFROM Customers cLEFT JOIN LATERAL ( SELECT order_id FROM Orders o WHERE o.customer_id = c.customer_id ORDER BY order_date DESC LIMIT 3) recent ON true; -- SQL Server: CROSS APPLY / OUTER APPLYSELECT c.name, recent.order_idFROM Customers cOUTER APPLY ( SELECT TOP 3 order_id FROM Orders o WHERE o.customer_id = c.customer_id ORDER BY order_date DESC) recent;If you're building an application that might migrate databases, or a library/ORM that targets multiple backends, stick to the lowest common denominator: INNER/LEFT/RIGHT JOIN with ON clause. If you're locked to one database forever, you can use its specific features more freely.
Let's consolidate all the join syntax variations with side-by-side comparisons to reinforce understanding:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
-- =====================================================-- ALL SYNTAXES FOR THE SAME INNER JOIN-- ===================================================== -- 1. ANSI-style with ON (RECOMMENDED)SELECT e.name, d.dept_nameFROM Employees eINNER JOIN Departments d ON e.department_id = d.department_id; -- 2. ANSI-style with USING (when column names match)SELECT e.name, d.dept_name FROM Employees eINNER JOIN Departments d USING (department_id); -- 3. NATURAL JOIN (NOT RECOMMENDED)SELECT e.name, d.dept_nameFROM Employees eNATURAL JOIN Departments d; -- 4. Legacy comma syntax (DEPRECATED)SELECT e.name, d.dept_nameFROM Employees e, Departments dWHERE e.department_id = d.department_id; -- =====================================================-- ALL SYNTAXES FOR LEFT OUTER JOIN-- ===================================================== -- 1. ANSI-style with ON (RECOMMENDED)SELECT e.name, d.dept_nameFROM Employees eLEFT OUTER JOIN Departments d ON e.department_id = d.department_id; -- 2. ANSI-style with USINGSELECT e.name, d.dept_nameFROM Employees eLEFT OUTER JOIN Departments d USING (department_id); -- 3. Oracle proprietary (+) (DEPRECATED, Oracle only)SELECT e.name, d.dept_nameFROM Employees e, Departments dWHERE e.department_id = d.department_id(+); -- 4. Legacy comma cannot express LEFT JOIN in standard SQL-- (No standard way to preserve unmatched employees with comma syntax)| Syntax | Recommendation | When to Use |
|---|---|---|
| JOIN ... ON | ✓✓✓ Strongly Recommended | All new SQL development |
| JOIN ... USING | ✓✓ Acceptable | Equi-joins with matching column names (not SQL Server) |
| NATURAL JOIN | ✗ Avoid | Never in production code |
| Legacy comma | ✗ Avoid | Only for reading legacy code |
| Oracle (+) | ✗ Avoid | Only for reading legacy Oracle code |
Congratulations! You've completed the Join Concepts module. You now have a comprehensive understanding of SQL joins—from the foundational concepts to practical syntax variations. Let's consolidate everything:
What's Next:
With a solid conceptual foundation, the next modules dive deeper into specific join types. You'll explore INNER JOINs in detail, then move to LEFT, RIGHT, and FULL OUTER JOINs with complex real-world scenarios. After that, you'll learn about subqueries—a powerful alternative that sometimes complements or replaces joins.
You've mastered the conceptual foundation of SQL joins. You understand why joins exist, how conditions work, the different join types, the importance of aliases, and the various syntax options with their tradeoffs. You're now ready to apply these concepts in deeper, more specialized modules.