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.
Key article
Related terms
Further reading
Put it to work
- Average order value — Metric
- Ecommerce sales dashboard — Dashboard
- Ecommerce analytics — Overview