SQL INNER JOIN: Matching Rows, Keys and Duplicate Results

INNER JOIN returns combinations of input rows that satisfy the join condition, excluding input rows that have no qualifying match.

Lesson 61 of 102Intermediate10 min read

INNER JOIN returns combinations of input rows that satisfy the join condition, excluding input rows that have no qualifying match.

A sale stores a customer id rather than copying the customer’s name. INNER JOIN connects that reference to a customer record. The number of returned rows follows the relationship: a customer with two sales produces two joined combinations.

This reference adds detail to the main course. You can read it independently: the small database below supplies every table and row used in the examples. Start with the question each query answers, then compare the returned values with the data rather than memorising a line of syntax.

Set up the practice database

Run this setup once in a new practice database, before the examples. It uses four customers and six sales. Ana has two sales, Ben has one, Cara has two, and Dev has none.

Sale 106 has no customer link; sale 104 has an unknown amount. These deliberate test cases expose missing information and unmatched relationships.

SQL
CREATE TABLE customers (
  id INTEGER PRIMARY KEY,
  name VARCHAR(40) NOT NULL,
  city VARCHAR(40),
  manager_id INTEGER
);
CREATE TABLE sales (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER,
  amount DECIMAL(10,2),
  status VARCHAR(20) NOT NULL,
  created_on DATE NOT NULL
);
INSERT INTO customers (id,name,city,manager_id) VALUES
  (1,'Ana','Leeds',NULL),(2,'Ben','Leeds',1),
  (3,'Cara','Porto',1),(4,'Dev',NULL,2);
INSERT INTO sales (id,customer_id,amount,status,created_on) VALUES
  (101,1,10,'paid','2026-01-01'),
  (102,1,30,'pending','2026-01-02'),
  (103,2,20,'paid','2026-01-02'),
  (104,3,NULL,'pending','2026-02-01'),
  (105,3,20,'paid','2026-02-02'),
  (106,NULL,5,'paid','2026-02-03');

An amount of zero would be a known value. The NULL amount here means the value is unknown. A customer with no matching sale differs from a sale whose customer_id is NULL.

The primary keys identify individual records even when amounts or cities repeat. Dates use year-month-day format. The examples specify sorting whenever order matters; insertion order alone does not guarantee display order.

How INNER JOIN works

The ON condition defines which pairs match. customers AS c INNER JOIN sales AS s ON s.customer_id=c.id compares each sale’s reference with the customer identity. A correct one-to-many relationship yields one joined row per matching sale, not one per customer. Ana’s two sales create two output rows, while Dev creates none because he has no matching sale. The anonymous sale does not match a customer through ordinary equality.

Qualify shared column names with table aliases. Both customers and sales have id, so c.id and s.id identify different values. Select only the fields the consumer needs, with aliases that expose their roles clearly. SELECT * can produce duplicate output names and expose new fields unexpectedly after a schema change. It also makes it harder to see the intended grain of the result.

Repeated rows in one-to-many joins

Multiple matches are normal when the relationship permits them. They are not necessarily duplicates to remove. A customer summary should aggregate sale rows after the join or aggregate sales first, while a transaction detail should preserve each sale identity. Joining on a nonunique descriptive field, such as city, can create many combinations that are unrelated to the intended customer ownership. Compare the ON clause with the actual key definition rather than choosing columns merely because their values sometimes resemble one another.

WHERE filters the joined rows further. A paid-sale condition there is appropriate when the entire result should contain paid sales only. For an inner join, many simple filters can be written in ON or WHERE with the same logical result, but this should not be generalised to outer joins, where placement affects preserved unmatched rows. Indexes on relationship keys can help large joins, yet the optimiser chooses physical algorithms independently of the order you happened to type table names. Focus first on matching semantics and row counts.

MySQL, PostgreSQL, SQL Server and SQLite

Explicit INNER JOIN ... ON is supported by all four systems. USING is supported by PostgreSQL, MySQL and SQLite but not in the same form in SQL Server, so ON is the shared teaching syntax. NATURAL JOIN can silently change matching columns after schema changes and is unsuitable as a shortcut for a stable application relationship.

Check the engine-specific syntax and version requirements in PostgreSQL joined tables.

Practice task and worked solution

Return only paid sales with identified customer matches, showing the customer name and sale id.

Try the task before reading its worked solution. The supplied rows make the expected answer checkable by hand.

SQL
SELECT c.name,s.id AS sale_id FROM customers AS c
JOIN sales AS s ON s.customer_id=c.id
WHERE s.status='paid' ORDER BY s.id;
Output
name | sale_id
Ana | 101
Ben | 103
Cara | 105

The inner join removes the anonymous paid sale because it has no customer partner. WHERE then excludes pending matched sales. The three results are transactions, not a distinct customer list; a future customer with multiple paid sales would appear more than once.

Replace the correct relationship with c.id=s.id and predict no rows from the supplied ids. Restore s.customer_id=c.id and count five matches. Then filter to Ana and inspect her two different sale ids. Selecting DISTINCT c.name changes the output to a customer list; explain why that is a different result shape rather than a repair to the original detail query.

SQL INNER JOIN: Matching Rows, Keys and Duplicate Results Syntax

INNER JOIN

SELECT c.id AS customer_id,c.name,s.id AS sale_id,s.amount
FROM customers AS c INNER JOIN sales AS s ON s.customer_id=c.id
ORDER BY c.id,s.id;

SQL INNER JOIN: Matching Rows, Keys and Duplicate Results Summary

  • INNER JOIN ... ON returns combinations of rows that satisfy the join condition.
  • Rows without a match on either side disappear from the result.
  • In a one-to-many relationship, each "one" row repeats once per matching "many" row.
  • Join a foreign key to the key it references, such as s.customer_id = c.id, not columns that merely share a name.
  • Keep each detail row's id in the output so repeated names remain understandable.

SQL INNER JOIN: Matching Rows, Keys and Duplicate Results Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Sales with their customers' names

Five sales have an identified customer match. Cara’s missing amount does not prevent her sale from joining because the relationship compares ids, not amounts. Dev and the anonymous sale are excluded because neither has a qualifying partner.

SQL
SELECT c.id AS customer_id,c.name,s.id AS sale_id,s.amount
FROM customers AS c INNER JOIN sales AS s ON s.customer_id=c.id
ORDER BY c.id,s.id;
Output
customer_id | name | sale_id | amount
1 | Ana | 101 | 10
1 | Ana | 102 | 30
2 | Ben | 103 | 20
3 | Cara | 104 | NULL
3 | Cara | 105 | 20

Common SQL INNER JOIN: Matching Rows, Keys and Duplicate Results Mistakes

Joining on the wrong id column

Joining c.id=s.id compares a customer identity with a sale identity rather than its customer_id reference. It returns no matches for this sample even though real relationships exist. Inspect the foreign-key direction and use s.customer_id=c.id instead of matching similarly named fields blindly.

SQL INNER JOIN: Matching Rows, Keys and Duplicate Results in Real Projects

Customer names on transaction details

A transaction detail report can attach customer names to sales without storing repeated customer text in every sale. Preserve the sale id in the export so repeated customer names remain understandable as separate purchases. If anonymous sales must also appear, the required join shape changes to an outer join.

Retain transaction identity in a detail report so repeated customer labels are not mistaken for duplicate records. Compare the joined sale count with the eligible source count before calculating revenue. If the relationship changes from one-to-many to many-to-many, preaggregate or revise the output grain rather than applying DISTINCT indiscriminately to make the table shorter.

SQL INNER JOIN: Matching Rows, Keys and Duplicate Results FAQ

Does INNER JOIN keep customers with no sale?

No. A customer without any matching sale has no qualifying partner, so it's left out. Use a LEFT JOIN to keep them, or an anti-join with NOT EXISTS to list only them.

Why is Ana repeated?

Two different sale rows match her one customer row, so the join produces two output rows. That's correct for a list of sales; for one row per customer, aggregate with GROUP BY after the join.

Does a NULL amount prevent an id-based join?

No. Only the join condition determines the relationship match, and it compares ids, not amounts. A NULL in the join column itself, such as the anonymous sale's customer_id, does prevent a match, because NULL = 1 is unknown.

Is it faster to filter in ON or in WHERE for an INNER JOIN?

For an INNER JOIN, a condition in ON and the same condition in WHERE give the same result, and the optimiser treats them alike. Keep relationship conditions in ON and other filters in WHERE for readability. With a LEFT JOIN, the placement changes the result.

Can I join more than two tables?

Yes. Add one JOIN ... ON per table, each linking the new table to one already in the query: FROM sales s JOIN customers c ON s.customer_id = c.id JOIN products p ON s.product_id = p.id. Check the row count after each join to catch accidental multiplication.

SQL INNER JOIN: Matching Rows, Keys and Duplicate Results Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

How many rows appear in the full identified-customer INNER JOIN example?

SQL INNER JOIN: Matching Rows, Keys and Duplicate Results Quiz

2 questions to check what you learned.

  1. What happens to an input row without a qualifying partner?

    • It is always preserved with NULL partner fields
    • INNER JOIN excludes it from the result
    • It is removed permanently from the source table
  2. Which relationship connects these tables correctly?

    • sales.id = customers.id
    • sales.amount = customers.id
    • sales.customer_id = customers.id