SQL JOINs Explained: INNER, LEFT, RIGHT and FULL, Without the Confusion

A clear, practical guide to SQL JOINs: what INNER, LEFT, RIGHT and FULL actually return, and the mistakes that quietly break queries.

Share on Linkedin Share on WhatsApp

Estimated reading time: 7 minutes

Article image SQL JOINs Explained: INNER, LEFT, RIGHT and FULL, Without the Confusion

JOINs are where most people learning SQL get stuck. The syntax is short, the diagrams look simple, and yet the results are constantly surprising. The confusion almost always comes from the same place: thinking about JOINs as “combining tables” instead of asking the one question that actually matters — what happens to rows that have no match?

The setup

Imagine two tables. customers holds people; orders holds purchases, each linked back to a customer.

customers.idcustomers.name
1Ana
2Ben
3Carla
orders.idorders.customer_idorders.total
10150
11130
12220
13NULL90

Note the two awkward rows: Carla has never ordered anything, and order 13 belongs to no customer. Those two rows are what reveal the difference between JOIN types.

INNER JOIN — only the matches

SELECT c.name, o.total
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id;

Returns only rows that exist on both sides: Ana (50), Ana (30), Ben (20). Carla disappears because she has no order. Order 13 disappears because it has no customer.

Notice something important: Ana appears twice. A JOIN does not “merge” tables one-to-one — it produces one row for every matching pair. This is the single most common source of unexpected row counts.

LEFT JOIN — everything on the left, matched or not

SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;

Every customer is kept, whether or not they ordered. Carla appears with NULL in the total column. Order 13 is still excluded, because it has no customer to attach to.

LEFT JOIN is the workhorse of reporting. Any time you want “all X, plus their Y if it exists” — all customers with their order count, all products with their reviews, all employees with their last training date — LEFT JOIN is the answer.

RIGHT JOIN — the mirror image

RIGHT JOIN keeps everything from the second table instead. In our example it would keep order 13, showing NULL for the customer name, and drop Carla.

In practice RIGHT JOIN is rare. Most developers simply reverse the table order and use LEFT JOIN, because reading a query is easier when the “kept” table is always the one you started from.

FULL OUTER JOIN — keep everything

FULL OUTER JOIN keeps every row from both tables, filling in NULL wherever there is no match. Our result would include Carla (no order) and order 13 (no customer).

It is mostly used to find inconsistencies — orphaned records, data that failed to sync between systems, mismatches after a migration. Note that not every database engine supports it; MySQL, for example, does not, and requires a workaround using a UNION of a LEFT and a RIGHT JOIN.

Quick reference

Join typeUnmatched left rowsUnmatched right rows
INNER JOINDroppedDropped
LEFT JOINKept (NULLs on right)Dropped
RIGHT JOINDroppedKept (NULLs on left)
FULL OUTER JOINKeptKept
CROSS JOINEvery row paired with every row — no condition at all

The mistake that silently ruins LEFT JOINs

This is worth memorising. Suppose you want all customers, with their orders from 2026 only:

-- WRONG: silently behaves like an INNER JOIN
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.order_date >= '2026-01-01';

Carla has no orders, so her o.order_date is NULL. The WHERE clause runs after the join, and NULL >= '2026-01-01' is not true — so Carla is filtered right back out. The LEFT JOIN has been destroyed.

The fix is to move the condition into the ON clause, so it applies while matching rather than after:

-- RIGHT: filter during the join
SELECT c.name, o.total
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.id
  AND o.order_date >= '2026-01-01';

Rule of thumb: conditions on the outer (optional) table belong in ON. Conditions on the preserved table can safely stay in WHERE.

Self joins and multiple joins

Two extensions come up constantly in real work, and both are less exotic than they sound.

A self join is a table joined to itself, which is how you model hierarchies. If an employees table has a manager_id column pointing at another row in the same table, you can list every employee alongside their manager by joining employees to a second alias of employees. A LEFT JOIN is usually the right choice here, so that the person at the top of the hierarchy — who has no manager — still appears in the results.

Chaining several joins in one query is equally routine: customers to orders, orders to order items, order items to products. The important thing is that joins are evaluated in sequence, each one operating on the result of the previous step. If an INNER JOIN appears anywhere in that chain, it can quietly discard rows that an earlier LEFT JOIN had carefully preserved. When a query with many joins returns fewer rows than expected, this is almost always the reason — read the chain from top to bottom and find the first join that drops rows.

Other habits worth building

  • Always alias your tables (customers c) and prefix every column. Ambiguity errors vanish and queries become readable.
  • Check your row count. If a join unexpectedly multiplies rows, you are joining on a non-unique column somewhere.
  • Index your join keys. Foreign key columns without an index are a classic cause of slow queries.
  • Use COUNT(o.id), not COUNT(*), when counting after a LEFT JOIN — otherwise customers with no orders will count as 1 instead of 0.

Conclusion

Forget the overlapping circles for a moment and ask a single question every time you write a join: which rows do I want to survive even when they have no match? Answer that, and the correct JOIN type picks itself.

To go deeper into query writing, indexing and design, the free databases courses on Cursa are a solid next step.

SQL JOINs Explained: INNER, LEFT, RIGHT and FULL, Without the Confusion

A clear, practical guide to SQL JOINs: what INNER, LEFT, RIGHT and FULL actually return, and the mistakes that quietly break queries.

Essential Excel Functions Every Beginner Should Learn

Master the most useful Excel functions for beginners, from SUM and AVERAGE to IF and VLOOKUP, with clear examples to speed up your everyday work.

Getting Started with Drones: A Beginner’s Guide to Flight Basics

A practical introduction to flying your first drone: controls, safety rules, and beginner tips.

From Script to System: How to Pick the Right Language Features in Python, Ruby, Java, and C

Learn how to choose the right language features in Python, Ruby, Java, and C for scripting, APIs, performance, and maintainable systems.

Build a Strong Programming Foundation: Data Structures and Algorithms in Python, Ruby, Java, and C

Learn Data Structures and Algorithms in Python, Ruby, Java, and C to build transferable programming skills beyond syntax.

Beyond Syntax: Mastering Debugging Workflows in Python, Ruby, Java, and C

Master debugging workflows in Python, Ruby, Java, and C with practical techniques for tracing bugs, reading stack traces, and preventing regressions.

APIs in Four Languages: Build, Consume, and Test Web Services with Python, Ruby, Java, and C

Learn API fundamentals across Python, Ruby, Java, and C by building, consuming, and testing web services with reliable patterns.

Preventative Maintenance Checklists for Computers & Notebooks: A Technician’s Routine That Scales

Prevent PC and notebook failures with practical maintenance checklists, improving performance, reliability, and long-term system health.