March 6, 2026·9 min read·SQLTutorial

SQL JOIN Tutorial: INNER, LEFT, RIGHT, and FULL OUTER Joins Explained

JOINs are the heart of relational SQL — they're how you combine data spread across multiple tables into a single result set. Yet "which JOIN do I use?" remains one of the most common questions for developers learning SQL. This SQL JOIN tutorial explains every join type with practical examples, clarifies the difference between INNER JOIN vs OUTER JOIN, and shows you how to write multi-table queries with confidence.

Why JOINs Exist

Relational databases store data in separate tables to avoid duplication. A typical e-commerce database has a users table, an orders table, and a products table — each with its own rows and a foreign key linking them together. A JOIN lets you pull related data from multiple tables in a single query instead of making separate requests and stitching data together in application code.

Understanding JOINs means understanding one core question: what happens to rows that don't match the join condition? That's the difference between every join type.

INNER JOIN — Only Matching Rows

INNER JOIN is the default join type (you can write JOINinstead of INNER JOIN — they're identical). It returns only rows where the join condition matches in both tables. Rows that don't find a match on either side are excluded from the result.

-- INNER JOIN: only rows that match in BOTH tables
SELECT
  o.id        AS order_id,
  u.name      AS customer_name,
  o.total     AS order_total
FROM orders o
INNER JOIN users u ON o.user_id = u.id;

-- Result: orders WITHOUT a matching user are excluded
-- Result: users WITHOUT any orders are excluded

When to use it: When you only want records that have a complete relationship — e.g., orders that have a valid customer, products that belong to a category, or transactions linked to an account. If you need to see all records from one side even without a match, you need an outer join.

LEFT JOIN (LEFT OUTER JOIN) — All Left Rows

LEFT JOIN returns every row from the left (first) table, plus the matched rows from the right table. If no match exists in the right table, the right table's columns appear as NULL. The "OUTER" keyword is optional — LEFT JOIN and LEFT OUTER JOIN are identical.

-- LEFT JOIN: all rows from the LEFT table, matched rows from the right
SELECT
  u.id        AS user_id,
  u.name      AS customer_name,
  o.id        AS order_id,   -- NULL if no order exists
  o.total     AS order_total -- NULL if no order exists
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

-- Result: ALL users appear — even those with zero orders
-- Orders columns are NULL for users who haven't ordered

When to use it: This is the most commonly used outer join. Use it when you need all records from the primary table regardless of whether related data exists — for example: all users (including those who haven't ordered), all products (including those with no reviews), or all accounts (including inactive ones with no recent activity).

💡 The NULL trick for "not exists" queries

A LEFT JOIN combined with a WHERE right_table.id IS NULL filter gives you all rows in the left table that have no matching row in the right table — equivalent to a NOT EXISTS subquery, often with better performance.

RIGHT JOIN (RIGHT OUTER JOIN) — All Right Rows

RIGHT JOIN is the mirror of LEFT JOIN — it returns all rows from the right (second) table, plus matched rows from the left. In practice, most developers prefer to rearrange the table order and use a LEFT JOIN instead. RIGHT JOIN is equivalent to swapping the table positions.

-- RIGHT JOIN: all rows from the RIGHT table, matched rows from the left
SELECT
  u.name    AS customer_name,  -- NULL if no matching user
  o.id      AS order_id,
  o.total   AS order_total
FROM users u
RIGHT JOIN orders o ON o.user_id = u.id;

-- Equivalent to swapping the tables in a LEFT JOIN:
-- SELECT u.name, o.id, o.total
-- FROM orders o
-- LEFT JOIN users u ON o.user_id = u.id;

When to use it: Use RIGHT JOIN when the query structure makes the "right" table the primary one and rewriting it would be awkward — but consider flipping the table order and using LEFT JOIN for readability consistency in your codebase.

INNER JOIN vs OUTER JOIN — The Key Difference

Join TypeNon-matching left rowsNon-matching right rows
INNER JOINExcludedExcluded
LEFT JOINIncluded (right = NULL)Excluded
RIGHT JOINExcludedIncluded (left = NULL)
FULL OUTER JOINIncluded (right = NULL)Included (left = NULL)

The short version: INNER JOIN is exclusive (only matched pairs), OUTER JOINs are inclusive (one or both sides regardless of match).

FULL OUTER JOIN — All Rows from Both Tables

FULL OUTER JOIN combines LEFT and RIGHT JOIN — it includes all rows from both tables, with NULL filling in wherever no match exists. Note that MySQL does not support FULL OUTER JOIN natively; you emulate it with a UNIONof a LEFT JOIN and a RIGHT JOIN.

-- FULL OUTER JOIN: all rows from BOTH tables
-- (PostgreSQL / SQL Server syntax)
SELECT
  u.name  AS customer_name,
  o.id    AS order_id
FROM users u
FULL OUTER JOIN orders o ON o.user_id = u.id;

-- Result includes:
-- ✅ Users with orders (both sides match)
-- ✅ Users with NO orders (right side NULL)
-- ✅ Orders with NO matching user (left side NULL)

-- MySQL doesn't support FULL OUTER JOIN directly.
-- Emulate it with UNION:
SELECT u.name, o.id
FROM users u LEFT JOIN orders o ON o.user_id = u.id
UNION
SELECT u.name, o.id
FROM users u RIGHT JOIN orders o ON o.user_id = u.id;

When to use it: Data reconciliation — comparing two tables to find records present in one but not the other. For example: comparing an old customer list vs. a new import to find additions, deletions, and mismatches.

CROSS JOIN — Every Combination

CROSS JOIN produces the Cartesian product of two tables — every row from the left paired with every row from the right. There's no join condition. With large tables, this produces enormous result sets (use with caution).

-- CROSS JOIN: every combination of rows from both tables (Cartesian product)
SELECT
  colors.name  AS color,
  sizes.label  AS size
FROM colors
CROSS JOIN sizes;

-- If colors has 3 rows and sizes has 4 rows → 12 rows in result
-- Use case: generating all combinations (e.g. product variants, test data)

When to use it: Generating all possible combinations — product variants (color × size), scheduling permutations, or synthetic test data. Rarely needed in day-to-day analytics, but the right tool when you need it.

SELF JOIN — A Table Joined to Itself

A SELF JOIN isn't a separate keyword — it's any regular join where both sides reference the same table using aliases. The classic use case is hierarchical data, like an employees table where each employee has amanager_id pointing to another row in the same table.

-- SELF JOIN: join a table to itself
-- Find all employees and their managers (both stored in the same table)
SELECT
  e.name      AS employee,
  m.name      AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

-- LEFT JOIN ensures employees with no manager (CEO) still appear

Joining Multiple Tables

Real-world queries routinely join three, four, or more tables. Each additional JOIN is appended in sequence. The query optimizer handles the execution order — you just need to specify the relationships correctly:

-- Joining multiple tables in one query
SELECT
  o.id          AS order_id,
  u.name        AS customer,
  p.name        AS product,
  oi.quantity,
  oi.unit_price
FROM orders o
INNER JOIN users u        ON o.user_id    = u.id
INNER JOIN order_items oi ON oi.order_id  = o.id
INNER JOIN products p     ON oi.product_id = p.id
WHERE o.status = 'completed'
ORDER BY o.id, p.name;

Best practices for multi-table JOINs:

  • Always use table aliases to keep column references unambiguous
  • Explicitly name the join type (INNER JOIN vs bare JOIN) for readability
  • Add an index on foreign key columns to keep multi-join queries fast
  • Use EXPLAIN to verify the optimizer is using your indexes

Common JOIN Mistakes to Avoid

  • Forgetting the ON condition: Without a join condition, most databases either error out or produce a CROSS JOIN accidentally.
  • Using INNER JOIN when you need LEFT JOIN: If your results are missing rows you expect to see, you've probably excluded non-matching rows unintentionally.
  • Duplicate rows from one-to-many joins: Joining a table with a one-to-many relationship (e.g., one order with many items) multiplies the left table's rows. Use GROUP BY or aggregate functions to handle this.
  • Ambiguous column names: When two joined tables share a column name (e.g., both have id), always qualify with the table alias:u.id, not just id.

Write complex JOIN queries instantly

RegSQL generates schema-aware SQL from plain English — JOINs, aggregations, subqueries, and more. Describe what you need and get clean, dialect-correct SQL in seconds.

🗄️ Try RegSQL SQL Generator Free →

SQL JOIN Quick Reference

  • INNER JOIN: Only rows matching in both tables
  • LEFT JOIN: All left rows + matched right rows (NULLs for no match)
  • RIGHT JOIN: All right rows + matched left rows (NULLs for no match)
  • FULL OUTER JOIN: All rows from both tables (NULLs where no match)
  • CROSS JOIN: Cartesian product — every row × every row
  • SELF JOIN: Table joined to itself via aliases

The key to picking the right join every time: ask yourself whether you need rows from the left table that have no match on the right (→ LEFT JOIN), rows from the right with no match on the left (→ RIGHT JOIN), both (→ FULL OUTER JOIN), or only confirmed matches (→ INNER JOIN). Get that decision right and the rest follows.