Skip to content
joinsintermediatefundamentals

Complete Guide to SQL JOINs with Visual Examples

10 min readMasterSQL

SQL JOINs combine rows from two or more tables based on a related column. The most common types are INNER JOIN (matching rows only), LEFT JOIN (all rows from left table plus matches), and RIGHT JOIN (all rows from right table plus matches). Understanding when to use each type is the difference between correct and incorrect query results.

JOINs are the most important concept in relational databases. If you understand JOINs, you understand relational data. If you do not understand JOINs, you will write incorrect queries and get wrong results without realizing it.

The reason JOINs matter so much is that relational databases store data across multiple tables by design. A single query often needs data from users, orders, products, and categories all at once. Without JOINs, you would have to make separate queries and combine the results in your application code, which is slow and error-prone. Getting JOINs right means your database does the heavy lifting for you.

The most common SQL bugs are not syntax errors or performance issues. They are wrong JOIN types. A LEFT JOIN where an INNER JOIN was needed returns extra rows. An INNER JOIN where a LEFT JOIN was needed silently drops data. These bugs are hard to catch because the query runs without errors.

JOIN Types at a Glance

JOIN TypeReturnsUse WhenMySQL Support
INNER JOINOnly matching rowsYou need rows that exist in both tablesNative
LEFT JOINAll left rows + matchesYou need all rows from the left tableNative
RIGHT JOINAll right rows + matchesRarely (rewrite as LEFT JOIN)Native
FULL OUTER JOINAll rows from both tablesYou need all rows from both sidesUNION emulation
CROSS JOINEvery combinationGenerating test data or combinationsNative
Self JoinTable joined to itselfHierarchical data, comparisonsNative

The Data We Will Use

Let me set up two simple tables for all the examples. This is the same schema you would use for a basic e-commerce system.

CREATE TABLE users (
  id INT PRIMARY KEY,
  name VARCHAR(100),
  email VARCHAR(100)
);

CREATE TABLE orders (
  id INT PRIMARY KEY,
  user_id INT,
  total DECIMAL(10, 2),
  created_at DATE
);

-- Sample data
INSERT INTO users VALUES (1, 'Alice', '[email protected]');
INSERT INTO users VALUES (2, 'Bob', '[email protected]');
INSERT INTO users VALUES (3, 'Charlie', '[email protected]');

INSERT INTO orders VALUES (1, 1, 150.00, '2026-01-15');
INSERT INTO orders VALUES (2, 1, 75.50, '2026-02-20');
INSERT INTO orders VALUES (3, 2, 200.00, '2026-03-10');

Notice that Charlie has no orders. This is important for understanding the difference between JOIN types.

INNER JOIN: Only Matching Rows

INNER JOIN returns only rows that have matches in both tables. If a user has no orders, they do not appear in the result.

SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

-- Result:
-- Alice   | 150.00
-- Alice   | 75.50
-- Bob     | 200.00
-- (Charlie is missing because he has no orders)

Use INNER JOIN when you only want rows that exist in both tables. This is the most common JOIN type. If you do not specify a JOIN type, MySQL uses INNER JOIN by default.

LEFT JOIN: All Rows from Left Table

LEFT JOIN returns all rows from the left table, even if there are no matches in the right table. If a user has no orders, they still appear with NULL values for the order columns.

SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

-- Result:
-- Alice   | 150.00
-- Alice   | 75.50
-- Bob     | 200.00
-- Charlie | NULL    (included even though no orders)

Use LEFT JOIN when you want all rows from the left table, regardless of matches. This is the second most common JOIN type. A common use case is finding records that do not have related records:

-- Find users with no orders
SELECT u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

-- Result:
-- Charlie

RIGHT JOIN: All Rows from Right Table

RIGHT JOIN is the opposite of LEFT JOIN. It returns all rows from the right table, even if there are no matches in the left table.

SELECT u.name, o.total
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

-- Result:
-- Alice   | 150.00
-- Alice   | 75.50
-- Bob     | 200.00
-- (Only orders with matching users appear)

RIGHT JOIN is rarely used in practice. Most developers rewrite RIGHT JOINs as LEFT JOINs by swapping the table order. It is easier to reason about LEFT JOINs because you always know the "left" table is the primary one. If you see RIGHT JOIN in existing code, rewrite it as a LEFT JOIN to keep your queries consistent and easier to reason about.

FULL OUTER JOIN: All Rows from Both Tables

FULL OUTER JOIN returns all rows from both tables. If there is no match, the missing side gets NULL values.

-- MySQL does not support FULL OUTER JOIN directly.
-- Simulate it with UNION of LEFT JOIN and RIGHT JOIN:
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
UNION
SELECT u.name, o.total
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

-- Result:
-- Alice   | 150.00
-- Alice   | 75.50
-- Bob     | 200.00
-- Charlie | NULL

MySQL does not support FULL OUTER JOIN syntax in any version. The UNION of LEFT JOIN and RIGHT JOIN pattern produces the same result. PostgreSQL and SQL Server support FULL OUTER JOIN natively, but MySQL users need the UNION approach.

CROSS JOIN: Every Combination

CROSS JOIN returns every possible combination of rows from both tables. If table A has 3 rows and table B has 4 rows, the result has 12 rows.

SELECT u.name, p.product_name
FROM users u
CROSS JOIN products p;

-- If 3 users and 4 products: 12 rows total

CROSS JOIN is rarely used directly. It is useful for generating test data or creating combinations (like a size/color matrix for products). Be careful: CROSS JOINs can produce enormous result sets.

Self Join: Joining a Table to Itself

A self join joins a table to itself. This is useful for hierarchical data, like finding a user's manager or comparing rows within the same table.

-- Table with manager hierarchy
CREATE TABLE employees (
  id INT PRIMARY KEY,
  name VARCHAR(100),
  manager_id INT
);

-- Find each employee and their manager
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

-- Result:
-- Alice   | NULL    (CEO, no manager)
-- Bob     | Alice
-- Charlie | Alice
-- Dave    | Bob

Self joins are powerful but confusing. The key is using different aliases for the same table. Think of it as two separate copies of the table.

ON vs WHERE in JOINs

This confuses almost everyone. In a JOIN, the ON clause defines how tables are related. The WHERE clause filters the result. They are not the same.

-- This filters AFTER the join (wrong for counting all users)
SELECT u.name, COUNT(o.id)
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NOT NULL
GROUP BY u.name;

-- This filters during the join (correct for counting all users)
SELECT u.name, COUNT(o.id)
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.id IS NOT NULL
GROUP BY u.name;

With LEFT JOIN, putting conditions in WHERE vs ON changes the result. Conditions in ON are applied during the join. Conditions in WHERE are applied after the join. Understanding this difference is one of the most important skills for writing correct queries. Getting it wrong is a silent bug that produces wrong data without any error messages.

JOIN Performance Tips

JOINs can be slow if you do not index the right columns. Here are the rules:

  1. Always index JOIN columns - The columns in ON clauses should have indexes
  2. Index foreign keys - MySQL InnoDB creates a basic index on foreign keys, but you should add your own composite indexes for optimal JOIN performance
  3. Use EXPLAIN - Check that your JOINs are using indexes, not doing full table scans
  4. Join on primary keys when possible - Primary keys are always indexed
-- Add indexes for JOIN performance
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_products_id ON products(id);

Key Takeaways

  • INNER JOIN returns only matching rows; LEFT JOIN returns all rows from the left table plus matches
  • FULL OUTER JOIN returns all rows from both tables; in MySQL, simulate it with UNION of LEFT JOIN and RIGHT JOIN
  • Use ON for join conditions and WHERE for filtering after the join
  • Always specify the join type explicitly rather than relying on implicit syntax
  • Choose the right JOIN type based on whether you need all rows from one or both tables

FAQ

What is the most common JOIN type?

INNER JOIN and LEFT JOIN are the most common. Most queries use one of these two. RIGHT JOIN is rarely used. FULL OUTER JOIN requires a UNION emulation in MySQL.

When should I use RIGHT JOIN instead of LEFT JOIN?

Almost never. Rewrite it as a LEFT JOIN by swapping the table order. It is easier to read and reason about.

Can I JOIN more than two tables?

Yes. You can chain JOINs: FROM a JOIN b ON ... JOIN c ON ... JOIN d ON .... Just make sure each JOIN has an ON clause.

Why is my JOIN slow?

Usually because the JOIN columns are not indexed. Run EXPLAIN to check. If you see "Using join buffer (hash join)" with high row counts, add indexes on the JOIN columns.

How do I choose between INNER JOIN and LEFT JOIN?

Ask yourself: do I need rows from the left table even if there is no match? If yes, use LEFT JOIN. If you only want rows that have matches in both tables, use INNER JOIN. The wrong choice leads to either missing data or unexpected NULL values in your results.

A common mistake is using INNER JOIN when you want to count all users, including those with no orders. This silently drops users without orders from the result, giving you an incorrect count. LEFT JOIN with a WHERE condition checking for NULL on the right table is the correct pattern for finding non-matching rows. Run EXPLAIN on both versions to compare performance, but correctness should always take priority over speed when choosing between JOIN types.

M

Written by

MasterSQL

Related Tutorials