Data and Business Intelligence Glossary Terms

What is an inner join?

An inner join is a join that only keeps rows where the keys from the two input tables matched. It returns the intersection: rows that exist on both sides and nothing else.

A worked example

Say you have an orders table with four rows, and a products table. One of those orders references a product that was deleted from the catalog, so its product_id has no match:

SELECT
  orders.id AS order_id,
  products.title,
  orders.total
FROM orders
INNER JOIN products ON orders.product_id = products.id

Before: orders has 4 rows, one of which points at a missing product.

After: the result has 3 rows. The orphaned order is gone — not blanked out, gone. There is no row for it in the output at all, and its total won’t appear in any sum you calculate downstream.

That’s the defining behavior. In every other join type, unmatched rows survive with NULLs. In an inner join they’re dropped.

An inner join is also a filter

This is the part that trips people up. Because unmatched rows disappear, an inner join quietly filters your data, and it does so without appearing anywhere in your WHERE clause. If you join orders to customers and 2% of orders carry a stale customer_id, your revenue total silently drops by 2%.

A quick sanity check: compare the row count before and after the join. If the count fell and you didn’t expect it to, you have referential integrity problems in the source data — and an inner join is hiding them rather than showing them. Switching to a left outer join and looking for NULLs on the right-hand side is how you find the culprits.

Syntax notes

INNER JOIN and plain JOIN are the same thing in SQL — the INNER keyword is optional, though writing it out makes your intent obvious to the next reader. Because the join is symmetric, A INNER JOIN B and B INNER JOIN A return the same rows (only the column order differs), which is not true of the outer joins.

When to reach for it

Use an inner join when a missing match means the row isn’t relevant to the question. Counting revenue per product category, measuring average order value for orders you can attribute, listing users who have an active subscription — in all of these, a row without a match is noise.

Reach for an outer join instead the moment the absence of a match is itself interesting: customers who never ordered, products that never sold.

Inner joins in Metabase

If you write a native query with a bare JOIN, you get an inner join. The query builder defaults to a left outer join instead, so you pick the inner join explicitly from the join-type picker when you want it.

Was this helpful?

Thanks for your feedback!