LEARN VISUALLY · Data foundations

SQL joins: explain every row in the resultMatch records without hiding missing or repeated relationships

Use a small customers-and-orders dataset to compare INNER JOIN and LEFT JOIN, inspect NULL values and explain why one customer can produce several rows.

Your report says there are three customers, but joining orders produces four lines. Another customer disappears completely. Neither result is necessarily a database bug: the query may be answering a different question from the report.

Jump to a section
Customers: 1 Asha · 2 Bo · 3 Cy. Orders: 101→1 · 102→1 · 103→2. Inner result: Three matching pairs. Left result: Three pairs plus Cy with NULL. Tables compare inputs with their joined result. Asha legitimately matches two orders.
Tables compare inputs with their joined result. Asha legitimately matches two orders.

The diagram’s relationships are explained in the readable cards below. Open the full-size SVG to zoom.

Download branded diagram (SVG)

Read the diagram

Tables compare inputs with their joined result. Asha legitimately matches two orders.

01 · Diagram relationship

Customers

1 Asha · 2 Bo · 3 Cy

02 · Diagram relationship

Orders

101→1 · 102→1 · 103→2

03 · Diagram relationship

Inner result

Three matching pairs

04 · Diagram relationship

Left result

Three pairs plus Cy with NULL

Start with the relationship, not a Venn diagram

Our synthetic customers table contains Asha with id 1, Bo with id 2 and Cy with id 3. Orders 101 and 102 belong to customer 1; order 103 belongs to customer 2. This is a one-to-many relationship. The shared customer identifier connects the tables. Names are display values; matching on a name would create ambiguity if two people had the same name or one spelling changed.

Before writing the query, state the question. “Show orders with customer names” is different from “show all customers, including those with no orders.” Choose output columns deliberately so duplicate column names do not hide which table supplied a value. A table alias such as c or o makes the relationship easier to read.

An inner join keeps matching pairs

Join customers c to orders o on c.id = o.customer_id. The inner result contains Asha with order 101, Asha with order 102 and Bo with order 103. Cy has no matching order, so no Cy row is returned. Asha’s repeated name is expected: each output row represents a matching customer-order pair, not a unique customer.

If a report counts those three rows as three distinct buyers, its interpretation is wrong. Decide whether you need orders, customers or an aggregate per customer. DISTINCT can hide repetitions in selected columns, but it is not a substitute for understanding why rows multiplied. A join to another one-to-many table can multiply them again.

A left join preserves the left side

Using the same match condition with LEFT JOIN keeps Cy. The order columns for Cy are NULL because no matching order supplied them. NULL is not the string “NULL,” an empty order object or an order with id zero. It represents the missing right-side values in this result.

The left-join result now has four rows: two for Asha, one for Bo and one unmatched Cy row. Preserving all customers does not mean exactly one output row per customer. If you want that, explicitly group or construct the intended summary. The number of customer records and the number of joined rows still answer different questions.

A filter can undo the preservation

Imagine adding WHERE o.id = 101 after the left join. Cy’s unmatched row cannot satisfy that condition, so it disappears. The join still says LEFT JOIN, but the later filter removes the very rows you hoped to preserve. A right-side condition inside ON controls which rows match; a condition in WHERE filters the already formed result.

For “all customers, with order 101 if present,” put that order condition in the match. For “only customers who have order 101,” an inner join or equivalent filtering may be appropriate. Explain the business question first, then select the query shape. There is no universal rule that one placement is always correct.

Execute a deliberately small dataset

The companion example creates the tables in an in-memory SQLite database and executes both joins with explicit ordering. It expects inner pairs 1:101, 1:102, 2:103 and left pairs with 3:NULL added. PostgreSQL documentation supplies the general relational explanation; this execution is SQLite evidence, not a claim that PostgreSQL was installed or benchmarked.

Try the WHERE condition and then the ON condition above. Predict whether Cy survives before inspecting the answer. This boundary case tests understanding more effectively than a large dataset whose output cannot be checked by eye. No private customer data is involved.

Avoid accidental cartesian multiplication

Omitting the intended relationship can produce combinations you did not mean. Three customers crossed with three orders produce nine pairs. That is valid for some questions, but it is not our customer-order report. If the count grows unexpectedly, inspect keys, uniqueness assumptions and each join stage before adding DISTINCT or changing data.

What this lesson leaves out

We focus on equality-based inner and left joins, NULLs and one-to-many relationships. Full outer joins, window functions, execution algorithms and database-specific optimization need separate treatment. Correct result semantics come before performance tuning. The fixture verifies exact rows, not production schema design or the suitability of these fields for a real customer system.

Work through the checked example

Input

Customers: 1 Asha, 2 Bo, 3 Cy
Orders: 101→1, 102→1, 103→2

Expected result

INNER: (1,101), (1,102), (2,103)
LEFT: same pairs plus (3,NULL)
WHERE o.id=101: 1 row
ON includes o.id=101: 3 rows
CROSS: 9 rows
What if it fails?

Executed in SQLite, not PostgreSQL. Rows count pairs, not distinct customers.

Original synthetic teaching fixture. No remote connection or real account is used.

References

Sources used to check the explanations and examples are listed below.

  1. PostgreSQL: joins between tables
  2. PostgreSQL: joined tables and ON/WHERE semantics

Found a mistake? Send a correction with this lesson’s title, URL and disputed detail. No login needed.

Continue learning

Database indexes: faster lookups have a cost →

Pagination: why an item can appear twice—or vanish →

All visual lessons and practical guides →