Loading content...
Now that you understand why we join tables and how join conditions work, it's time to explore the types of joins available in SQL. Each join type answers a fundamentally different question about data relationships, and choosing the right type is critical for getting correct results.
The key distinction between join types isn't about what gets matched—they all use conditions for matching. The distinction is about what happens to unmatched rows. Do you want to see only perfectly paired data? Or do you need to include data that doesn't have a partner? These design decisions drive join type selection.
This page provides a comprehensive overview of all major join types, their behaviors, use cases, and mental models. By the end, you'll have a complete framework for selecting the right join type for any query scenario.
You will understand the six major join types in SQL, how each handles matched and unmatched rows, Venn diagram conceptualizations, practical use cases for each type, and how to choose the correct join type for different business requirements.
SQL joins can be classified into two main categories based on how they handle rows that don't find a match:
Inner Joins — Return only rows that have matching partners in both tables. Unmatched rows are excluded entirely.
Outer Joins — Return matched rows plus some or all unmatched rows, filling in NULLs where no match exists.
Within these categories, specific join types define exactly which unmatched rows to preserve:
| Join Type | Matched Rows | Unmatched Left | Unmatched Right | Use Case |
|---|---|---|---|---|
| INNER JOIN | ✓ Included | ✗ Excluded | ✗ Excluded | Only complete data pairs |
| LEFT OUTER JOIN | ✓ Included | ✓ Included (with NULLs) | ✗ Excluded | All left rows, optionally matched |
| RIGHT OUTER JOIN | ✓ Included | ✗ Excluded | ✓ Included (with NULLs) | All right rows, optionally matched |
| FULL OUTER JOIN | ✓ Included | ✓ Included (with NULLs) | ✓ Included (with NULLs) | Complete union of both sides |
| CROSS JOIN | All combinations | N/A | N/A | Cartesian product |
| NATURAL JOIN | ✓ Included | ✗ Excluded | ✗ Excluded | Auto-match on same-name columns |
Understanding 'Unmatched Rows':
An unmatched row is a row from one table that has no matching row in the other table according to the join condition. For example:
The join type determines whether these orphan rows appear in results or are silently dropped.
In most SQL dialects, 'LEFT JOIN' is synonymous with 'LEFT OUTER JOIN', and similarly for RIGHT and FULL. The OUTER keyword is optional but adds clarity. Many style guides recommend including it for explicit documentation of intent.
The INNER JOIN is the most common join type and is often the default when you write simply JOIN. It returns only the rows where the join condition finds a match in both tables. Rows without matches are completely excluded from results.
Mental Model:
Think of INNER JOIN as finding the intersection of matching relationships. Like a Venn diagram where only the overlapping region is included.
12345678910111213141516171819202122232425262728293031
-- INNER JOIN: Only customers WITH orders appearSELECT c.customer_id, c.name AS customer_name, o.order_id, o.order_dateFROM Customers cINNER JOIN Orders o ON c.customer_id = o.customer_id; -- Result excludes:-- - Customers who never placed an order-- - Orders with invalid/NULL customer_id (shouldn't exist if FK enforced) -- Sample result (customers without orders are NOT here):-- | customer_id | customer_name | order_id | order_date |-- |-------------|---------------|----------|-------------|-- | 1 | John Smith | 101 | 2024-01-15 |-- | 1 | John Smith | 102 | 2024-02-20 |-- | 2 | Jane Doe | 103 | 2024-01-18 |-- (Customer 3 with no orders is MISSING from results) -- Multiple INNER JOINs: All relationships must existSELECT c.name AS customer_name, p.name AS product_name, oi.quantityFROM Customers cINNER JOIN Orders o ON c.customer_id = o.customer_idINNER JOIN Order_Items oi ON o.order_id = oi.order_idINNER JOIN Products p ON oi.product_id = p.product_id;-- Only shows complete chains: Customer → Order → Item → ProductWhen you write just 'JOIN' (without INNER, LEFT, RIGHT, etc.), most databases treat it as an INNER JOIN. Being explicit with 'INNER JOIN' is recommended for clarity, especially in complex queries with multiple join types.
The LEFT OUTER JOIN (or simply LEFT JOIN) returns all rows from the left table, regardless of whether they have a match in the right table. For unmatched left rows, the right table columns are filled with NULL.
Mental Model:
Think of LEFT JOIN as: "Start with everything from the left table. Add right table data where available. Use NULL where not available."
12345678910111213141516171819202122232425262728293031323334
-- LEFT JOIN: ALL customers appear, even those without ordersSELECT c.customer_id, c.name AS customer_name, o.order_id, o.order_dateFROM Customers cLEFT JOIN Orders o ON c.customer_id = o.customer_id; -- Sample result (customer without orders appears with NULLs):-- | customer_id | customer_name | order_id | order_date |-- |-------------|---------------|----------|-------------|-- | 1 | John Smith | 101 | 2024-01-15 |-- | 1 | John Smith | 102 | 2024-02-20 |-- | 2 | Jane Doe | 103 | 2024-01-18 |-- | 3 | Bob Wilson | NULL | NULL | ← No orders! -- Finding unmatched rows (customers who never ordered)SELECT c.customer_id, c.name AS customer_nameFROM Customers cLEFT JOIN Orders o ON c.customer_id = o.customer_idWHERE o.order_id IS NULL;-- Result: Only customers with NO orders (useful for marketing!) -- Counting with LEFT JOIN preserves all left rowsSELECT c.name, COUNT(o.order_id) AS order_count -- COUNT ignores NULLsFROM Customers cLEFT JOIN Orders o ON c.customer_id = o.customer_idGROUP BY c.customer_id, c.name;-- Returns order count for ALL customers (0 for those without)When using LEFT JOIN with aggregations, remember that NULL values from unmatched rows affect functions differently. SUM(NULL) returns NULL, but COUNT(column) returns 0 for NULLs. Use COALESCE or COUNT carefully to get expected behavior.
The RIGHT OUTER JOIN is the mirror image of LEFT JOIN. It returns all rows from the right table, with matching left table data where available and NULLs where not.
Mental Model:
RIGHT JOIN is less commonly used because you can always rewrite it as a LEFT JOIN by swapping table order. However, it's useful when the business logic emphasizes the right table's completeness.
1234567891011121314151617181920212223
-- RIGHT JOIN: ALL orders appear, even those with invalid customersSELECT c.name AS customer_name, o.order_id, o.order_dateFROM Customers cRIGHT JOIN Orders o ON c.customer_id = o.customer_id; -- Sample result (order with missing customer data):-- | customer_name | order_id | order_date |-- |---------------|----------|-------------|-- | John Smith | 101 | 2024-01-15 |-- | Jane Doe | 103 | 2024-01-18 |-- | NULL | 105 | 2024-03-01 | ← Order without valid customer! -- Right join is equivalent to swapped left join:-- This RIGHT JOIN:SELECT c.name, o.order_idFROM Customers c RIGHT JOIN Orders o ON c.customer_id = o.customer_id; -- Is equivalent to this LEFT JOIN:SELECT c.name, o.order_idFROM Orders o LEFT JOIN Customers c ON c.customer_id = o.customer_id;Most SQL style guides recommend using LEFT JOIN exclusively and reordering tables as needed. This creates consistency throughout codebases and reduces cognitive load when reading queries. RIGHT JOIN is a valid SQL feature but rarely necessary in practice.
The FULL OUTER JOIN combines the behaviors of LEFT and RIGHT joins. It returns:
Mental Model:
FULL OUTER JOIN gives you the complete union of both tables' rows, matching where possible and preserving everything regardless of match status.
1234567891011121314151617181920212223242526272829303132333435363738
-- FULL OUTER JOIN: All customers AND all orders appearSELECT COALESCE(c.customer_id, o.customer_id) AS customer_id, c.name AS customer_name, o.order_id, o.order_dateFROM Customers cFULL OUTER JOIN Orders o ON c.customer_id = o.customer_id; -- Sample result:-- | customer_id | customer_name | order_id | order_date |-- |-------------|---------------|----------|-------------|-- | 1 | John Smith | 101 | 2024-01-15 | ← Matched-- | 2 | Jane Doe | 103 | 2024-01-18 | ← Matched-- | 3 | Bob Wilson | NULL | NULL | ← Left only-- | 99 | NULL | 105 | 2024-03-01 | ← Right only -- Reconciliation query: Find all discrepanciesSELECT COALESCE(inv.account_id, pay.account_id) AS account_id, inv.invoice_total, pay.payment_total, COALESCE(inv.invoice_total, 0) - COALESCE(pay.payment_total, 0) AS balanceFROM Invoices invFULL OUTER JOIN Payments pay ON inv.account_id = pay.account_idWHERE inv.account_id IS NULL OR pay.account_id IS NULL OR inv.invoice_total <> pay.payment_total; -- Note: MySQL doesn't support FULL OUTER JOIN directly-- Workaround using UNION:SELECT c.*, o.*FROM Customers cLEFT JOIN Orders o ON c.customer_id = o.customer_idUNIONSELECT c.*, o.*FROM Customers cRIGHT JOIN Orders o ON c.customer_id = o.customer_id;MySQL (and MariaDB before 10.2) does not support FULL OUTER JOIN syntax. You must emulate it using a UNION of LEFT JOIN and RIGHT JOIN. PostgreSQL, SQL Server, Oracle, and SQLite all support FULL OUTER JOIN natively.
The CROSS JOIN produces the Cartesian product of two tables—every row from the first table paired with every row from the second table. There is no join condition; all combinations are generated.
Result Size: If Table A has M rows and Table B has N rows, CROSS JOIN produces M × N rows.
Mental Model:
CROSS JOIN is useful for generating all possible combinations, such as test data, calendar matrices, or configuration grids.
123456789101112131415161718192021222324252627282930313233343536
-- CROSS JOIN: Generate all size/color combinations-- Sizes: Small, Medium, Large (3 rows)-- Colors: Red, Blue, Green (3 rows)-- Result: 9 combinations SELECT s.size_name, c.color_name, CONCAT(s.size_code, '-', c.color_code) AS sku_suffixFROM Sizes sCROSS JOIN Colors c; -- Result:-- | size_name | color_name | sku_suffix |-- |-----------|------------|------------|-- | Small | Red | S-R |-- | Small | Blue | S-B |-- | Small | Green | S-G |-- | Medium | Red | M-R |-- | Medium | Blue | M-B |-- | Medium | Green | M-G |-- | Large | Red | L-R |-- | Large | Blue | L-B |-- | Large | Green | L-G | -- Generate a calendar grid: all dates × all employeesSELECT e.employee_name, d.calendar_dateFROM Employees eCROSS JOIN Calendar_Dates dWHERE d.calendar_date BETWEEN '2024-01-01' AND '2024-01-31'; -- Alternative syntax (implicit cross join):SELECT s.size_name, c.color_nameFROM Sizes s, Colors c; -- Comma without WHERE = cross joinCROSS JOIN is rarely wanted accidentally. If you forget a join condition, you get a cross join. For a 10,000 × 10,000 row scenario, this produces 100,000,000 rows! Always verify join conditions exist for every table pair in multi-table queries.
The NATURAL JOIN automatically joins tables on all columns that have the same name in both tables. You don't specify a condition—the database infers it from column name matches.
How It Works:
12345678910111213141516171819202122232425262728
-- Tables with matching column names:-- Employees: employee_id, name, department_id, manager_id-- Departments: department_id, department_name, location -- NATURAL JOIN: Automatically joins on department_idSELECT *FROM EmployeesNATURAL JOIN Departments; -- Equivalent to explicit INNER JOIN:SELECT e.employee_id, e.name, e.department_id, -- Only appears once in natural join e.manager_id, d.department_name, d.locationFROM Employees eINNER JOIN Departments d ON e.department_id = d.department_id; -- DANGER: If both tables have 'id' or 'name' columns...-- Employees: id, name, department_id-- Departments: id, name, manager_id -- NATURAL JOIN now matches on id AND name!-- This is almost certainly NOT what you intendedSELECT * FROM Employees NATURAL JOIN Departments;-- Equivalent to: WHERE e.id = d.id AND e.name = d.nameMost SQL experts and style guides strongly discourage NATURAL JOIN in production code. Its implicit nature makes queries fragile—adding a column with a common name (like 'status', 'created_at', 'updated_by') can silently change join behavior and break queries. Always use explicit JOIN conditions.
With multiple join types available, selecting the right one requires clear thinking about your data requirements. Ask yourself these questions:
Question 1: Do I need ALL rows from a specific table?
Question 2: What should happen to unmatched rows?
Question 3: Am I generating combinations?
| Business Requirement | Recommended Join | Reasoning |
|---|---|---|
| Show orders with customer details | INNER JOIN | Only valid orders have customers |
| List all customers and their orders (if any) | LEFT JOIN | Include customers without orders |
| Find customers who never ordered | LEFT JOIN + WHERE IS NULL | Identify unmatched left rows |
| Reconcile two data sources for discrepancies | FULL OUTER JOIN | Find unmatched rows on either side |
| Generate all product/size/color combinations | CROSS JOIN | Intentional Cartesian product |
| Employee with department (dept required) | INNER JOIN | Business rule: employees must have dept |
| Employee with department (dept optional) | LEFT JOIN | Some employees may lack assignment |
INNER JOIN should be your default choice. Most business queries assume complete data relationships. Switch to OUTER JOINs only when you explicitly need to preserve unmatched rows—for finding orphans, generating complete reports, or handling optional relationships.
You now have a complete overview of SQL join types. Each type serves a distinct purpose based on how you want to handle unmatched rows. Let's consolidate:
What's Next:
Now that you understand the join types, the next page covers table aliases—the essential technique for writing readable, maintainable multi-table queries. We'll explore alias syntax, naming conventions, and how aliases prevent ambiguity in complex joins.
You now have a complete mental model for SQL join types. You understand the crucial distinction between INNER and OUTER joins, when to use each type, and the special cases of CROSS and NATURAL joins. Next, we'll explore table aliases for cleaner query syntax.