SQLab Hub
Lesson 13 · INNER JOIN
Lesson 13 · Querying

SQL INNER JOIN: Syntax, Examples & Practice

SQL INNER JOIN returns only the rows that have a match in both tables. It pairs each row of one table with the rows of another that satisfy the ON condition, and leaves out anything without a partner. Plain JOIN means the same thing.

⋈ Matching rows only ▶ Runnable Playground examples 🔗 Join 3 and 4 tables 🧪 8 practice exercises 📝 10-question quiz 🎤 12 interview questions

What Is an INNER JOIN in SQL?

In the Playground database, orders stores who placed each order as a number, user_id. The customer's name lives in users. An INNER JOIN puts the two together by matching orders.user_id with users.id:

SQLite · runs in the Playground
SELECT o.id AS order_id, o.order_date,
       u.first_name, u.last_name, o.total
FROM orders o
INNER JOIN users u
  ON u.id = o.user_id
ORDER BY o.id
LIMIT 5;

▶ Run this query in the SQL Playground

The first five orders with the customer who placed them
order_idorder_datefirst_namelast_nametotal
12024-04-24AmandaMitchell2638.11
22024-03-15TimothyCampbell2947
32023-02-15SarahHernandez683.24
42022-01-10LindaAllen3920.44
52023-06-16RobertLewis1120.99
✓ 5 rows · real output from the Playground database

Each result row is built from two tables: the order columns come from orders and the names from users. The ON clause says which rows belong together. o and u are table aliases, short names that show which table each column comes from.

"Inner" means only matches are returned. A customer with no orders does not appear, and neither would an order whose customer did not exist. To keep the unmatched rows, use a LEFT JOIN.

INNER JOIN Syntax

SQL · syntax
SELECT columns
FROM table_a
INNER JOIN table_b
  ON table_a.key = table_b.key;
PartWhat it does
FROM table_aThe first table. For an INNER JOIN the order of the two tables does not change the result.
INNER JOIN table_bThe table to combine with it.
ON …The matching rule, usually a foreign key compared with the primary key it points to.
JOINThe same join. INNER is the default and may be left out.

INNER JOIN vs JOIN vs a comma in FROM

There are three ways to write the same inner join. All three return the same rows:

SQLite · runs in the Playground
SELECT 'INNER JOIN … ON' AS syntax, COUNT(*) AS rows_returned
FROM orders o INNER JOIN users u ON u.id = o.user_id
UNION ALL
SELECT 'JOIN … ON', COUNT(*)
FROM orders o JOIN users u ON u.id = o.user_id
UNION ALL
SELECT 'FROM a, b WHERE', COUNT(*)
FROM orders o, users u WHERE u.id = o.user_id;

▶ Run this query in the SQL Playground

Three spellings of the same inner join
syntaxrows_returned
INNER JOIN … ON200
JOIN … ON200
FROM a, b WHERE200
✓ 3 rows · real output from the Playground database
FormNotes
INNER JOIN … ONExplicit and the clearest. Recommended.
JOIN … ONIdentical; INNER is implied.
FROM a, b WHERE a.id = b.a_idThe older "implicit" join. It works, but the join rule is mixed in with the filters, and forgetting it silently pairs every row with every row.
Same in every database. INNER JOIN is standard SQL and is written identically in MySQL, PostgreSQL, SQL Server, Oracle and SQLite.

How INNER JOIN Matches Rows

For each row of one table, the database finds the rows of the other table for which ON is true and outputs one combined row per pair:

A row has…Rows in the result
One match1
Several matchesOne per match
No match0 — the row is left out

Joining customers to orders shows all three cases. Look at who is missing and who repeats:

SQLite · runs in the Playground
SELECT u.id, u.first_name, o.id AS order_id, o.status
FROM users u
INNER JOIN orders o
  ON o.user_id = u.id
WHERE u.id <= 5
ORDER BY u.id, o.id;

▶ Run this query in the SQL Playground

Orders of the first five customers
idfirst_nameorder_idstatus
1Jennifer109delivered
2Donna13delivered
4Michael11processing
4Michael87delivered
4Michael181shipped
5Robert127delivered
✓ 6 rows · real output from the Playground database

Customer 3 has no orders and is absent. Customer 4 has three orders and appears three times. Across the whole tables:

SQLite · runs in the Playground
SELECT (SELECT COUNT(*) FROM users)  AS customers,
       (SELECT COUNT(*) FROM orders) AS orders,
       COUNT(*)             AS joined_rows,
       COUNT(DISTINCT u.id) AS customers_in_result
FROM users u
INNER JOIN orders o
  ON o.user_id = u.id;

▶ Run this query in the SQL Playground

Row counts before and after the inner join
customersordersjoined_rowscustomers_in_result
150200200114
✓ 1 row · real output from the Playground database

Every one of the 200 orders finds its customer, so the join returns 200 rows. Only 114 of the 150 customers are in it; the rest have never ordered.

INNER JOIN With Three or More Tables

Add one INNER JOIN … ON for each extra table. Every join needs its own condition linking the new table to one already in the query. This lists the items of order 1 with the customer and the product name, using four tables:

SQLite · runs in the Playground
SELECT oi.id AS item_id, u.first_name AS customer,
       o.order_date, p.name AS product,
       oi.quantity, oi.subtotal
FROM order_items oi
INNER JOIN orders o
  ON o.id = oi.order_id
INNER JOIN users u
  ON u.id = o.user_id
INNER JOIN products p
  ON p.id = oi.product_id
WHERE o.id = 1
ORDER BY oi.id;

▶ Run this query in the SQL Playground

Items of order 1 with customer and product details
item_idcustomerorder_dateproductquantitysubtotal
1Amanda2024-04-24Giant Stuffed Teddy Bear5961.05
2Amanda2024-04-24Marvel Action Figures 5pk3321.3
3Amanda2024-04-24Waterproof Hiking Boots15.39
✓ 3 rows · real output from the Playground database
JoinLinks
order_items → ordersEach item to the order it belongs to
orders → usersThe order to the customer who placed it
order_items → productsEach item to the product that was bought
Rule of thumb: joining n tables needs at least n − 1 join conditions. If one is missing, the unlinked table is multiplied against everything else.

INNER JOIN With WHERE, GROUP BY and Aggregates

A join produces a normal set of rows, so everything else works on top of it: WHERE filters the joined rows, GROUP BY groups them and aggregate functions summarise them. Revenue per product category from delivered orders:

SQLite · runs in the Playground
SELECT p.category,
       COUNT(DISTINCT o.id) AS orders,
       SUM(oi.quantity)     AS units,
       ROUND(SUM(oi.subtotal), 2) AS revenue
FROM order_items oi
INNER JOIN orders o
  ON o.id = oi.order_id
INNER JOIN products p
  ON p.id = oi.product_id
WHERE o.status = 'delivered'
GROUP BY p.category
ORDER BY revenue DESC;

▶ Run this query in the SQL Playground

Delivered orders, units and revenue per product category
categoryordersunitsrevenue
Office Supplies259222858.48
Food & Beverage246919930.66
Books238419514.61
Automotive176818032.88
Health & Beauty196013963.53
✓ 10 rows · first 5 shown · real output from the Playground database

The category comes from products, the status from orders and the amounts from order_items. No single table could answer the question. Adding HAVING filters the groups, here customers with at least four orders:

SQLite · runs in the Playground
SELECT u.id, u.first_name, u.last_name,
       COUNT(*) AS orders,
       ROUND(SUM(o.total), 2) AS total_spent
FROM users u
INNER JOIN orders o
  ON o.user_id = u.id
GROUP BY u.id, u.first_name, u.last_name
HAVING COUNT(*) >= 4
ORDER BY total_spent DESC;

▶ Run this query in the SQL Playground

Customers with four or more orders and what they spent
idfirst_namelast_nameorderstotal_spent
118PaulYoung415384.26
77JosephKing514593.17
124MichaelRodriguez412059.32
54LauraMartin411780.53
83StevenRivera49700.36
37AndrewWright56432.94
✓ 6 rows · real output from the Playground database

ON or WHERE for a filter?

With an INNER JOIN it makes no difference to the result. A condition can sit in either clause:

SQLite · runs in the Playground
SELECT 'condition in ON' AS version, COUNT(*) AS rows_returned
FROM users u
INNER JOIN orders o ON o.user_id = u.id AND o.status = 'delivered'
UNION ALL
SELECT 'condition in WHERE', COUNT(*)
FROM users u
INNER JOIN orders o ON o.user_id = u.id
WHERE o.status = 'delivered';

▶ Run this query in the SQL Playground

A filter in ON and in WHERE returns the same rows for an inner join
versionrows_returned
condition in ON82
condition in WHERE82
✓ 2 rows · real output from the Playground database

The convention is to keep the matching rule in ON and filters in WHERE, which is easier to read. The placement only changes the result for outer joins; see ON vs WHERE in a LEFT JOIN.

Joining on More Than One Condition

ON accepts any condition, including several joined with AND. This finds employees who live in the city where their department is based: the department must match and the two cities must be equal.

SQLite · runs in the Playground
SELECT e.first_name, e.last_name, e.title,
       d.name AS department, d.location
FROM employees e
INNER JOIN departments d
  ON d.id = e.department_id
 AND d.location = e.city
ORDER BY e.id;

▶ Run this query in the SQL Playground

Employees who live in their department's city
first_namelast_nametitledepartmentlocation
MelissaSmithVP EngineeringEngineeringFort Worth
StevenAdamsGroup PMProductCharlotte
JeffreyLewisMarketing ManagerMarketingChicago
RichardBrownDirector of PeopleHRIndianapolis
DonaldBrownDirector of ProductProductCharlotte
✓ 9 rows · first 5 shown · real output from the Playground database

9 employees qualify. Two other forms are worth recognising:

SQL
-- USING: shorthand when the column has the same name in both tables
SELECT *
FROM order_items
INNER JOIN payments USING (order_id);

-- a join does not have to use =
SELECT e.first_name, b.band
FROM employees e
INNER JOIN salary_bands b
  ON e.salary BETWEEN b.min_salary AND b.max_salary;

USING (column) is supported by MySQL, PostgreSQL, Oracle and SQLite, but not SQL Server. NATURAL JOIN goes further and joins on every column name the tables share; avoid it, because adding a column later can silently change the join.

INNER JOIN vs LEFT JOIN

SQLite · runs in the Playground
SELECT 'INNER JOIN' AS join_type, COUNT(*) AS rows_returned,
       COUNT(DISTINCT u.id) AS customers
FROM users u
INNER JOIN orders o ON o.user_id = u.id
UNION ALL
SELECT 'LEFT JOIN', COUNT(*), COUNT(DISTINCT u.id)
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

▶ Run this query in the SQL Playground

Rows and customers returned by INNER JOIN and LEFT JOIN
join_typerows_returnedcustomers
INNER JOIN200114
LEFT JOIN236150
✓ 2 rows · real output from the Playground database

The INNER JOIN returns 114 customers; the LEFT JOIN returns all 150, adding one NULL row for each customer without orders.

INNER JOINLEFT JOINFULL OUTER JOIN
Matching rowsReturnedReturnedReturned
Unmatched rows of the first tableDroppedKept, NULLs on the rightKept
Unmatched rows of the second tableDroppedDroppedKept
Typical question"Orders with their customer""All customers, with orders if any""Everything from both, matched where possible"

Choose INNER JOIN when a row without a match is of no interest, and LEFT JOIN when the first table must stay complete. Counting "customers" from an inner join to orders really counts customers who have ordered. The SQL JOINs overview compares every join type.

Why an INNER JOIN Loses or Repeats Rows

Two things surprise people: rows that vanish, and totals that come out wrong. Both show up when orders are joined to payments. Some orders have no payment; others have several.

SQLite · runs in the Playground
SELECT 'orders alone' AS source,
       COUNT(*) AS row_count, ROUND(SUM(total), 2) AS order_total
FROM orders
UNION ALL
SELECT 'orders joined to payments',
       COUNT(*), ROUND(SUM(o.total), 2)
FROM orders o
INNER JOIN payments p
  ON p.order_id = o.id;

▶ Run this query in the SQL Playground

Order totals before and after joining to payments
sourcerow_countorder_total
orders alone200510970.71
orders joined to payments200488892.6
✓ 2 rows · real output from the Playground database

The row count happens to be the same, but the total is different. Orders with no payment were dropped, and orders with two or more payments were counted once per payment. Neither the count nor the sum describes the orders any more.

SymptomCauseFix
Rows are missingNo matching row in the other table, or the join column is NULLUse a LEFT JOIN if those rows should stay
Rows are repeatedOne-to-many: a row matches several rows on the other sideExpected. Group the result, or aggregate the "many" table in a subquery before joining
Totals are too highA value from the "one" side is summed once per matchSum it before the join, or use COUNT(DISTINCT …) for counts

NULL never matches

A join column that is NULL equals nothing, so the row cannot match. Ten employees have no manager (manager_id is NULL) and disappear from an inner join of the table to itself:

SQLite · runs in the Playground
SELECT (SELECT COUNT(*) FROM employees) AS employees,
       COUNT(*) AS rows_from_inner_join
FROM employees e
INNER JOIN employees m
  ON m.id = e.manager_id;

▶ Run this query in the SQL Playground

Employees and the rows left after joining each to a manager
employeesrows_from_inner_join
120110
✓ 1 row · real output from the Playground database

Joining a table to itself like this is a self join; the two aliases make it behave as two tables.

Where INNER JOIN Is Used

QuestionTables joined
Orders with the customer's name and emailorders + customers
Revenue by product categoryorder_items + products
Employees with their department and officeemployees + departments
Sales by region and sales repsales + reps + regions
Students and the courses they are enrolled instudents + enrollments + courses
Invoice lines with product descriptionsinvoice_lines + products
Transactions with account holder detailstransactions + accounts
Appointments with patient and doctor namesappointments + patients + doctors

In BI tools the same join is the default "Inner" option when two tables are merged or related.

Common INNER JOIN Mistakes

1. Leaving out the join condition

❌ Every order paired with every customer
SQL
SELECT o.id, u.first_name
FROM orders o, users u;
✅ Rows matched on the key
SQL
SELECT o.id, u.first_name
FROM orders o
INNER JOIN users u
  ON u.id = o.user_id;

Without a condition the result is 200 × 150 = 30,000 rows. Writing INNER JOIN … ON makes the condition hard to forget.

2. Joining on the wrong columns

❌ Compares two unrelated ids
SQL
SELECT o.id, u.first_name
FROM orders o
INNER JOIN users u
  ON u.id = o.id;
✅ Foreign key to primary key
SQL
SELECT o.id, u.first_name
FROM orders o
INNER JOIN users u
  ON u.id = o.user_id;

Both columns are numbers, so the wrong version runs and returns plausible rows, each order paired with an unrelated customer. Join the foreign key (orders.user_id) to the key it references.

3. Ambiguous column names

❌ Which id?
SQL
SELECT id, total
FROM orders o
INNER JOIN users u
  ON u.id = o.user_id;
✅ Prefix with the alias
SQL
SELECT o.id, o.total
FROM orders o
INNER JOIN users u
  ON u.id = o.user_id;

Both tables have an id column, so the database reports an "ambiguous column" error. Qualify every column in a join.

4. Using INNER JOIN when rows must be kept

❌ Customers without orders vanish
SQL
SELECT u.id, COUNT(o.id) AS orders
FROM users u
INNER JOIN orders o
  ON o.user_id = u.id
GROUP BY u.id;
✅ LEFT JOIN keeps them
SQL
SELECT u.id, COUNT(o.id) AS orders
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
GROUP BY u.id;

An inner join can never show a zero count, because rows with no match are not there to count.

5. Summing across a one-to-many join

❌ Order totals counted once per payment
SQL
SELECT SUM(o.total)
FROM orders o
INNER JOIN payments p
  ON p.order_id = o.id;
✅ Sum the orders on their own
SQL
SELECT SUM(total)
FROM orders;

Each order row is repeated for every payment it has. Aggregate a table before joining it to a table with several rows per key.

INNER JOIN Performance

TipWhy it helps
Index the join columnsPrimary keys are indexed already. Foreign keys such as orders.user_id usually need an index so matches can be looked up instead of scanned for.
Filter earlyA WHERE condition that removes rows before or during the join leaves less to match.
Select only the columns you needSELECT * across several tables returns every column of all of them.
Join on columns of the same typeComparing a number with text forces a conversion on every row and can stop an index being used.
Do not worry about table orderFor inner joins the optimiser picks the order itself; write the query in the order that reads best.

Use EXPLAIN (EXPLAIN QUERY PLAN in SQLite) to see whether an index is used. On the small Playground tables every join is instant.

INNER JOIN Cheat Sheet

GoalPattern
Match rows from two tablesFROM a INNER JOIN b ON b.a_id = a.id
Same thing, shorterFROM a JOIN b ON b.a_id = a.id
Three tables… JOIN b ON … JOIN c ON …
Two matching conditionsON b.a_id = a.id AND b.x = a.x
Filter the joined rowsWHERE after the joins
Summarise per groupGROUP BY with COUNT, SUM, AVG
Keep unmatched rows tooUse LEFT JOIN instead

Practice INNER JOIN Queries

8 exercises on the real Playground tables.

Write each query yourself first. Every solution has been run against the Playground database.

  1. 1Easy List each employee's first name, last name and title with the name and location of their department. Show the first ten by employee id. Table: employees, departments

    Show solution
    SQLite · runs in the Playground
    SELECT e.first_name, e.last_name, e.title,
           d.name AS department, d.location
    FROM employees e
    INNER JOIN departments d
      ON d.id = e.department_id
    ORDER BY e.id
    LIMIT 10;

    ▶ Run this query in the SQL Playground

    employees.department_id is the foreign key and departments.id the primary key it points to.

  2. 2Easy Show the five most helpful reviews: product name, rating and helpful votes. Table: reviews, products

    Show solution
    SQLite · runs in the Playground
    SELECT p.name AS product, r.rating, r.helpful_votes
    FROM reviews r
    INNER JOIN products p
      ON p.id = r.product_id
    ORDER BY r.helpful_votes DESC, r.id
    LIMIT 5;

    ▶ Run this query in the SQL Playground

    Each review is matched to its product; sorting and LIMIT are applied to the joined rows.

  3. 3Medium List the orders placed by customers in Canada during 2024: order id, date, customer name and total, newest first. Table: orders, users

    Show solution
    SQLite · runs in the Playground
    SELECT o.id, o.order_date,
           u.first_name, u.last_name, o.total
    FROM orders o
    INNER JOIN users u
      ON u.id = o.user_id
    WHERE u.country = 'Canada'
      AND o.order_date >= '2024-01-01'
      AND o.order_date <  '2025-01-01'
    ORDER BY o.order_date DESC, o.id;

    ▶ Run this query in the SQL Playground

    WHERE can filter on columns from either table once they are joined. 7 orders match.

  4. 4Medium For each department, show the number of employees and the average salary, highest average first. Table: departments, employees

    Show solution
    SQLite · runs in the Playground
    SELECT d.name AS department,
           COUNT(*) AS employees,
           ROUND(AVG(e.salary)) AS avg_salary
    FROM departments d
    INNER JOIN employees e
      ON e.department_id = d.id
    GROUP BY d.id, d.name
    ORDER BY avg_salary DESC;

    ▶ Run this query in the SQL Playground

    Every department has employees, so the inner join returns all 10. If one were empty it would be missing, and a LEFT JOIN would be needed.

  5. 5Medium Find the ten best-selling products by units sold. Show the product id, name and total units. Table: order_items, products

    Show solution
    SQLite · runs in the Playground
    SELECT p.id, p.name,
           SUM(oi.quantity) AS units_sold
    FROM order_items oi
    INNER JOIN products p
      ON p.id = oi.product_id
    GROUP BY p.id, p.name
    ORDER BY units_sold DESC, p.id
    LIMIT 10;

    ▶ Run this query in the SQL Playground

    Grouping by the product id as well as the name keeps products that share a name apart.

  6. 6Medium List each employee with their manager's name and title, for the first ten employees who have a manager. Table: employees

    Show solution
    SQLite · runs in the Playground
    SELECT e.first_name AS employee, e.title,
           m.first_name AS manager, m.title AS manager_title
    FROM employees e
    INNER JOIN employees m
      ON m.id = e.manager_id
    ORDER BY e.id
    LIMIT 10;

    ▶ Run this query in the SQL Playground

    A self join: the same table under two aliases. Employees with no manager are left out because their manager_id is NULL.

  7. 7Challenging Show total revenue per customer country and product category for delivered orders. Return the ten largest combinations. Table: order_items, orders, users, products

    Show solution
    SQLite · runs in the Playground
    SELECT u.country, p.category,
           ROUND(SUM(oi.subtotal), 2) AS revenue
    FROM order_items oi
    INNER JOIN orders o
      ON o.id = oi.order_id
    INNER JOIN users u
      ON u.id = o.user_id
    INNER JOIN products p
      ON p.id = oi.product_id
    WHERE o.status = 'delivered'
    GROUP BY u.country, p.category
    ORDER BY revenue DESC
    LIMIT 10;

    ▶ Run this query in the SQL Playground

    Four tables, three join conditions. The amounts are summed from order_items, the "many" table, so nothing is double counted.

  8. 8Challenging Find reviews written by a customer who actually ordered that product. Show the review id, user id, product id and rating, each review once. Table: reviews, orders, order_items

    Show solution
    SQLite · runs in the Playground
    SELECT DISTINCT r.id AS review_id, r.user_id, r.product_id, r.rating
    FROM reviews r
    INNER JOIN orders o
      ON o.user_id = r.user_id
    INNER JOIN order_items oi
      ON oi.order_id = o.id
     AND oi.product_id = r.product_id
    ORDER BY r.id;

    ▶ Run this query in the SQL Playground

    The second join has two conditions: the item must be in one of the reviewer's orders and be the reviewed product. Only 5 reviews pass.

📝 Quiz — INNER JOIN

Test your understanding · 0/10 answered

Question 1 of 10

What does an INNER JOIN return?

Question 2 of 10

What is the difference between JOIN and INNER JOIN?

Question 3 of 10

A customer has no orders. What does users INNER JOIN orders show for that customer?

Question 4 of 10

A customer has three orders. How many rows does users INNER JOIN orders return for that customer?

Question 5 of 10

What happens if you list two tables in FROM with a comma and no condition?

Question 6 of 10

How many join conditions are needed to join four tables properly?

Question 7 of 10

For an INNER JOIN, does moving a filter from WHERE into ON change the result?

Question 8 of 10

A row's join column is NULL. What does an INNER JOIN do with it?

Question 9 of 10

You need every department with its employee count, including departments with none. Which join?

Question 10 of 10

Why can SUM(o.total) be too high after orders INNER JOIN payments?

INNER JOIN Interview Questions

Q1. What is an INNER JOIN?

A join that returns only the rows for which the ON condition is true in both tables. Rows of either table with no match are not returned. JOIN on its own means INNER JOIN.

Q2. What is the difference between INNER JOIN and LEFT JOIN?

INNER JOIN returns matches only. LEFT JOIN also returns the rows of the first table that have no match, with NULL in the second table's columns. They give the same result when every row of the first table has a match.

Q3. Is there a difference between a condition in ON and in WHERE for an INNER JOIN?

Not in the result. For inner joins a predicate filters the same rows in either place. By convention ON holds the matching rule and WHERE holds filters. For outer joins the placement does change the result.

Q4. Can an INNER JOIN return more rows than either table has?

Yes. In a one-to-many or many-to-many relationship a row is returned once per match, so the result can be larger than both inputs.

Q5. What happens to NULL values in the join column?

They never match, because NULL = NULL is not true. Rows with a NULL key are dropped by an inner join.

Q6. How do you join three tables?

Chain the joins: FROM a JOIN b ON … JOIN c ON …. Each new table needs a condition that links it to a table already in the query.

Q7. What is the difference between an INNER JOIN and a comma join with WHERE?

They return the same rows. The explicit form separates the join rule from the filters and makes a missing condition obvious; the comma form silently becomes a cross join if the condition is forgotten.

Q8. What is the difference between INNER JOIN and CROSS JOIN?

CROSS JOIN pairs every row with every row and has no condition. INNER JOIN keeps only the pairs that satisfy ON.

Q9. Does the order of tables matter in an INNER JOIN?

Not for the result, and usually not for speed, because the optimiser chooses the join order. It does matter for LEFT and RIGHT joins.

Q10. What is USING, and how does it differ from ON?

USING (col) is shorthand for an equality join on a column that has the same name in both tables, and returns that column once. ON accepts any condition. SQL Server does not support USING.

Q11. Why did my totals change after adding a join?

The new table has several rows per key, so each row of the original table is repeated and its values are summed more than once; or rows without a match were dropped. Aggregate the detail table first, or use a LEFT JOIN if rows must be kept.

Q12. How do you make an INNER JOIN fast?

Index the join columns, especially foreign keys; filter rows early; join columns of the same data type; and return only the columns needed. Check the plan with EXPLAIN.

Frequently Asked Questions

What is INNER JOIN in SQL?

INNER JOIN combines rows from two tables and returns only the pairs that satisfy the ON condition. Rows that have no match in the other table are not returned.

Is JOIN the same as INNER JOIN?

Yes. INNER is the default join type, so JOIN and INNER JOIN are identical in every major database.

What is the difference between INNER JOIN and LEFT JOIN?

INNER JOIN returns only matching rows. LEFT JOIN returns the matching rows and also every unmatched row of the first table, with NULL in the columns of the second table.

How do I INNER JOIN three tables?

Write one INNER JOIN … ON clause per additional table, each with a condition that links the new table to one already in the query. Three tables need two join conditions.

Why is my INNER JOIN returning duplicate rows?

A row is returned once for each row it matches. If a customer has three orders, the customer appears three times. This is expected for one-to-many relationships; group the result if you need one row per customer.

Why is my INNER JOIN missing rows?

Rows with no match in the other table, and rows whose join column is NULL, are left out. Use a LEFT JOIN if those rows should stay in the result.

Can I use WHERE with INNER JOIN?

Yes. WHERE filters the joined rows and can use columns from any of the joined tables. For an inner join, a filter gives the same result in WHERE or in ON.

Does INNER JOIN work the same in MySQL, PostgreSQL and SQL Server?

Yes. INNER JOIN … ON is standard SQL and behaves the same in MySQL, PostgreSQL, SQL Server, Oracle and SQLite.