Outer joins · preservation guide

LEFT JOIN SQL: NULLs, Filters, Duplicates, and Fixes

Preserve every required left-side entity, distinguish unmatched rows from missing values, and prevent filters or one-to-many relationships from changing the answer silently.

Updated July 22, 202621 min readInfiniSynapse Editorial Team
A complete left table preserves matching and unmatched rows through a left join while unsafe post-join filtering removes empty matches and one-to-many duplication is corrected before complexity inspection
On this page

What does LEFT JOIN do in SQL?

LEFT JOIN returns every row from the left table expression and every right-side row satisfying the join condition. If a left row has no match, the database still returns it and supplies NULL for columns from the right side. If it has several matches, the left row appears once for each match.

Preserve every customer
SELECT
  c.customer_id,
  c.customer_name,
  o.order_id,
  o.order_date
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.customer_id;

This answers “show all customers and any orders they have.” It does not promise one row per customer. A customer with five orders returns five rows; a customer with none returns one null-extended row. State the intended grain before choosing columns or aggregates.

Think in preserved rows and matching pairs

A reliable mental model has two phases. First, evaluate the join condition and return every matching pair, just like an inner join. Second, add one null-extended row for each left row that found no match. Conditions in ON participate in the first phase; conditions in WHERE filter the joined result afterward.

Left population

Choose the table or stage representing every entity that must remain in the answer.

Matching rule

Include identity, tenant, effective-date, and validity conditions required for one right row to be related.

Match cardinality

Know whether each left row expects zero-or-one or zero-to-many right rows.

Null meaning

Separate “no related row” from “a related row exists but this attribute is missing.”

Place right-side filters in ON or WHERE intentionally

Filter placement is the most common LEFT JOIN bug. If a right-side eligibility rule belongs to what counts as a match, place it in ON. If the final answer should contain only joined rows satisfying a condition, put it in WHERE and recognize that unmatched rows will usually disappear.

Two different questions
-- All customers, plus completed orders when available
SELECT c.customer_id, o.order_id
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.customer_id
 AND o.status = 'completed';

-- Only customers that have a completed order
SELECT c.customer_id, o.order_id
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.customer_id
WHERE o.status = 'completed';

The second query rejects null-extended rows because NULL = 'completed' is not true. It may be better written as an inner join to make intent obvious. Adding OR o.status IS NULL is not always equivalent to moving the condition: a customer with only pending orders has matched rows, so no null-extended row exists to preserve.

The official PostgreSQL joined-table documentation demonstrates how a restriction in ON versus WHERE changes an outer join's result.

Distinguish unmatched rows from real NULL values

After a left join, a nullable right-side attribute cannot prove whether a match exists. If orders.discount_code is naturally nullable, discount_code IS NULL includes both unmatched customers and matched orders without a discount. Test a right-side primary key or another column guaranteed non-null for real rows.

ObservationPossible meaningSafer evidence
right.attribute IS NULLNo match or matched missing attributeCheck non-nullable right primary key
right.id IS NULLNo matched right row if id is guaranteed non-nullVerify schema constraint
COUNT(right.id)=0No non-null matched ids in the groupAudit duplicated left rows and grouping grain

COALESCE is useful for display but can erase the distinction between missing and zero. A customer with no invoice is not necessarily a customer with a zero-value invoice. Preserve a match indicator when downstream decisions need that distinction.

Prevent one-to-many LEFT JOIN multiplication

LEFT JOIN preserves left rows, but it does not preserve left row count. One customer with four addresses produces four result rows. Joining another one-to-many table can multiply combinations. This is correct relational behavior and a common source of inflated counts and sums.

Select one deterministic latest address
WITH ranked_addresses AS (
  SELECT
    a.*,
    ROW_NUMBER() OVER (
      PARTITION BY customer_id
      ORDER BY effective_from DESC, address_id DESC
    ) AS rn
  FROM customer_addresses a
)
SELECT c.customer_id, a.city
FROM customers c
LEFT JOIN ranked_addresses a
  ON a.customer_id = c.customer_id
 AND a.rn = 1;

This restores zero-or-one address per customer only if “latest” is the correct rule. Other needs may require pre-aggregation, returning a collection, or retaining every child row at child grain. Do not use DISTINCT as a reflex; it can hide duplicated projections without repairing semantics.

Use LEFT JOIN as an anti-join carefully

To find customers with no orders, left join orders and keep rows where a non-nullable order key is null. NOT EXISTS often communicates the existence question more directly and cannot multiply output rows.

Two anti-join forms
SELECT c.customer_id
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;

SELECT c.customer_id
FROM customers c
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.customer_id
);

When the question is “no completed orders,” the status condition belongs inside the join or EXISTS subquery. Putting it in the outer WHERE and also testing null creates contradictory logic. Test customers with no orders, only pending orders, and at least one completed order.

Keep preservation clear across several LEFT JOINs

A chain of left joins can create a false sense that the first table is always fully preserved. A later inner join to a nullable child can remove rows. A WHERE predicate on any later table can reject null-extended records. Two one-to-many branches can multiply each other. Draw the relationship graph and label expected cardinality for every edge.

  1. Choose the preserved population. State why every row from the first stage belongs.
  2. Add one relationship. Measure row count, distinct preserved keys, and unmatched rate.
  3. Validate right-side uniqueness. Enforce or profile the key expected to be zero-or-one.
  4. Reduce child detail. Aggregate or rank before joining when final output stays at parent grain.
  5. Inspect downstream filters. Mark predicates that can reject null-extended rows.
  6. Reconcile metrics. Compare totals before and after each relationship.

Review LEFT JOIN performance without changing meaning

Optimizers can choose hash, merge, or nested-loop algorithms and may reorder operations where semantics allow. Outer-join preservation restricts some transformations. Pushing a predicate into the right input may be safe when it defines eligible matches; pushing a null-rejecting predicate can effectively change the join. Validate results before celebrating a faster plan.

Plan signalPossible issueCheck
Actual rows exceed estimateUnexpected right-side duplicates or skewKey frequency and statistics
Large hash or spillWide or excessive right inputEarly safe filter and projection
Unmatched rate near zeroLEFT may be unnecessary or a data-quality assumptionBusiness requirement and enforced foreign key
Repeated child scansCorrelated or nested access pathActual loops, indexes, and set-based rewrite

Validate LEFT JOIN results before production

LeftAll required keys remain
MatchRelationship rule is complete
RowsMultiplication is explained
NULLMissing and zero stay distinct
  • Assert every required left key appears at least once.
  • Test no match, one match, several matches, null keys, and malformed keys.
  • Compare ON and WHERE placement against a written business question.
  • Measure total rows, distinct left keys, and match counts after each join.
  • Use a guaranteed non-null right key when detecting unmatched rows.
  • Reconcile sums and counts before and after one-to-many relationships.
  • Inspect actual plans with representative skew and unmatched rates.

Review LEFT JOIN behavior with adversarial data

A production-safe review starts by naming the preserved population in plain language. “Every active customer at month end” is testable; “customers joined to orders” is not. Capture a stable control count for that population before the join. After each relationship, compare distinct preserved keys, total rows, matched keys, unmatched keys, and maximum right-side matches. These measurements separate legitimate child detail from accidental loss or multiplication.

Create fixtures that force the edge cases: a left row with no partner, one partner, and several partners; a right row whose descriptive attribute is genuinely null; duplicate right keys; null join keys; status changes at the reporting boundary; and a child row outside the eligible date range. Run the intended query and an independently written control query. If both are derived from the same flawed join, agreement proves little, so use direct counts or source-system invariants where possible.

Observed symptomLikely causeDecisive test
Left count fallsNull-rejecting WHERE or later INNER JOINList missing left keys at each stage
Measures increaseOne-to-many or many-to-many expansionFrequency profile on both join inputs
“Missing” category looks too largeReal nulls confused with unmatched rowsTest a guaranteed non-null right key

Performance review comes after semantic proof. Inspect whether the right input is wider or larger than expected, whether estimates understand key skew, and whether a safe eligibility filter can reduce it before hashing or sorting. Preserve a reconciliation query and the edge-case fixture in automated tests. A faster left join is not an improvement if it changes who remains, what null means, or how many times a measure is counted.

Record the unmatched rate as a monitored contract rather than a one-time observation. A sudden rise can indicate delayed ingestion, changed identifiers, a broken effective-date condition, or a new source population. Alert on both loss and unexpected improvement: a match rate that becomes exactly 100 percent may mean a downstream filter has silently removed the very records the outer join was intended to preserve.

Inspect the complete LEFT JOIN query

Paste a sanitized complete statement into the InfiniSynapse SQL Complexity Checker so every join, filter, CTE, subquery, window, and aggregate layer can be reviewed together. Then validate unmatched cases, cardinality, and execution evidence in your database.

Open SQL Complexity CheckerRemove credentials, secrets, personal data, and sensitive literals.

LEFT JOIN SQL frequently asked questions

What does LEFT JOIN do?

It returns every left row and every matching right row, using NULL for right columns when no match exists.

How is it different from INNER JOIN?

INNER JOIN keeps only matching pairs; LEFT JOIN also preserves unmatched left rows.

Why can WHERE remove unmatched rows?

A right-side predicate rejects the NULL-extended row after joining unless it explicitly accepts NULL.

Why are rows duplicated?

One left row is returned once per right-side match. Non-unique keys can create one-to-many or many-to-many expansion.

How do I find rows with no match?

Test a non-nullable right key with IS NULL after a left join, or use NOT EXISTS.

Official LEFT JOIN documentation