Learn why relational databases split data across tables, how primary and foreign keys connect, how the ON clause works, and why unmatched rows disappear from the result.
In real-world applications, data is almost never dumped into a single gargantuan spreadsheet table. Consider an e-commerce platform:
CUSTOMERS table (id, name, email, city)
+
ORDERS table (id, customer_id, product, amount, order_date)
↓
Fundamental Question: "Which customer placed which order?"Why do relational databases keep related information in separate tables instead of cramming everything into one?
customers table instantly reflects across all their past and future orders.A JOIN is an operation that combines rows from two (or more) tables based on a related column between them.
Let us look at two minimal sample tables:
| id (PK) | name |
|---|---|
| 1 | Rahul |
| 2 | Priya |
| id | customer_id (FK) | amount |
|---|---|---|
| 101 | 1 | ₹500 |
| 102 | 2 | ₹300 |
Notice the direct relationship between customers.id and orders.customer_id:
customers.id (Primary Key) <═════ Relational Link ═════> orders.customer_id (Foreign Key)
The value in orders.customer_id points directly to the corresponding row in customers.
INNER JOIN is the most common type of join in SQL. It compares each row of the first table with each row of the second table and returns only the rows where the matching condition evaluates to TRUE in both tables.
SELECT customers.name, orders.amount
FROM customers
INNER JOIN orders
ON customers.id = orders.customer_id;Plain English Translation: "Look at the customers table and the orders table. Match each customer with their orders where customers.id equals orders.customer_id. Give me only the rows that successfully match, showing the customer name and order amount."
The ON clause is the bridge that tells SQL how the two tables connect:
ON customers.id = orders.customer_id
Without the ON clause, SQL has no idea which customer belongs to which order. The ON condition acts as the matching rule:
SQL iterates over the rows: for every row in customers, it searches orders where orders.customer_id matches customers.id.
Let us trace the exact evaluation process with a small dataset containing both matching and unmatched rows:
| id | name |
|---|---|
| 1 | Rahul |
| 2 | Priya |
| 3 | Aman (No order) |
| id | customer_id | amount |
|---|---|---|
| 101 | 1 | ₹500 |
| 102 | 1 | ₹800 |
| 103 | 2 | ₹300 |
| 104 | 5 | ₹900 (No cust #5) |
customer_id = 1) and Order 102 (customer_id = 1). → Produces 2 result rows.customer_id = 2). → Produces 1 result row.orders table for customer_id = 3. Found: 0 matches. → Excluded completely!customers table for id = 5. Found: 0 matches. → Excluded completely!| name | amount |
|---|---|
| Rahul | ₹500 |
| Rahul | ₹800 |
| Priya | ₹300 |
It is crucial to understand that INNER JOIN is not simply placing two tables side by side horizontally. It is a filter that extracts the mutual intersection of matching rows.
When querying across multiple tables, always specify which table each selected column comes from using dot notation (tablename.columnname):
SELECT
customers.name,
customers.city,
orders.id AS order_id,
orders.amount
FROM customers
INNER JOIN orders
ON customers.id = orders.customer_id;customers and orders have a column named id. If you write:SELECT id, name, amount ...Typing full table names repeatedly like customers.name and customers.id gets tedious. SQL allows you to assign short Table Aliases using the AS keyword (or just a space):
SELECT
c.name,
c.city,
o.product,
o.amount
FROM customers AS c
INNER JOIN orders AS o
ON c.id = o.customer_id;Here:
c is a shorthand alias for customerso is a shorthand alias for ordersFROM or JOIN clause, you must use that alias everywhere in the query (including SELECT, ON, and WHERE).You can combine INNER JOIN with a WHERE clause to filter the joined dataset:
SELECT
c.name,
o.product,
o.amount
FROM customers AS c
INNER JOIN orders AS o
ON c.id = o.customer_id
WHERE o.amount > 5000;| Clause | Core Responsibility | Example in Action |
|---|---|---|
| INNER JOIN ... ON | Combines corresponding rows across tables based on relationship | ON c.id = o.customer_id |
| WHERE | Filters down the resulting joined rows based on business conditions | WHERE o.amount > 5000 |
The ON clause can contain more than one condition connected by logical operators like AND:
SELECT c.name, o.product, o.status
FROM customers AS c
INNER JOIN orders AS o
ON c.id = o.customer_id
AND o.status = 'completed';In this query, a row from orders only joins with customers if both:
c.id = o.customer_id evaluates to TRUE, ANDo.status = 'completed' evaluates to TRUE.Beginners are often surprised when an INNER JOIN produces more rows than existed in the left table. This happens because of One-to-Many (1:N) relationships:
Customer 1 (Rahul) has 3 orders: - Order #101 (Laptop) - Order #102 (Mouse) - Order #103 (Keyboard)
When joined, SQL pairs Customer #1 with Order #101, Customer #1 with Order #102, and Customer #1 with Order #103.
It is essential not to confuse what JOIN does with what DISTINCT does:
| Feature | SQL INNER JOIN | SQL DISTINCT |
|---|---|---|
| Primary Goal | Combines related data from two or more tables | Eliminates duplicate output combinations from the final result |
| Row Count Effect | Can increase or decrease rows based on key matches | Always reduces or preserves row count |
| Typical Combination | SELECT DISTINCT c.name FROM customers c INNER JOIN orders o ON c.id = o.customer_id;(Returns the unique list of customers who have placed at least one order without repeating names.) | |
ON c.id = o.id compares Customer Primary Key (1, 2, 3) with Order Primary Key (101, 102, 103). Because they never match, 0 rows are returned! Always join Primary Key with Foreign Key: ON c.id = o.customer_id.SELECT id, amount FROM customers c INNER JOIN orders o ... without prefixing c.id or o.id causes ambiguous column errors.Test your query construction skills against these progressive business scenarios:
Goal: Display employee full names and their department names from employees (id, name, department_id) and departments (id, department_name).
SELECT
e.name AS employee_name,
d.department_name
FROM employees AS e
INNER JOIN departments AS d
ON e.department_id = d.id;Goal: Find the names of all students and their course titles who scored 80 or above from students and enrollments.
SELECT
s.name AS student_name,
e.course_title,
e.score
FROM students AS s
INNER JOIN enrollments AS e
ON s.id = e.student_id
WHERE e.score >= 80;Goal: Join active doctor appointments where the appointment status is 'confirmed'.
SELECT
p.patient_name,
a.appointment_date,
a.doctor_name
FROM patients AS p
INNER JOIN appointments AS a
ON p.id = a.patient_id
AND a.status = 'confirmed';c for customers, oi for order_items) to keep queries compact and readable.c.name, o.amount) to avoid ambiguous column bugs when tables evolve.ON for key relationships (c.id = o.customer_id) and WHERE for row filtering (WHERE o.amount > 1000).INT or UUID) for optimal join performance.FROM customers AS c) make multi-table SQL queries concise and readable.c.id) prevents "ambiguous column" database errors.| id | name | city | Status |
|---|---|---|---|
| 1 | Rahul Sharma | Mumbai | Matched |
| 2 | Priya Patel | Delhi | Matched |
| 3 | Aman Verma | Bengaluru | No Order |
| 4 | Neha Gupta | Pune | Matched |
| id | customer_id | product | amount |
|---|---|---|---|
| 101 | 1 | Laptop Pro 16" | ₹85,000 |
| 102 | 1 | Wireless Mouse | ₹1,200 |
| 103 | 2 | Mechanical Keyboard | ₹4,500 |
| 104 | 4 | 4K IPS Monitor | ₹28,000 |
| 105 | 9 | USB-C Hub Multiport | ₹1,800 |
SELECT
c.name AS customer_name,
c.city,
o.id AS order_id,
o.product,
o.amount
FROM customers AS c
INNER JOIN orders AS o
ON c.id = o.customer_id;| customer_name | city | order_id | product | amount |
|---|---|---|---|---|
| Rahul Sharma | Mumbai | 101 | Laptop Pro 16" | ₹85,000 |
| Rahul Sharma | Mumbai | 102 | Wireless Mouse | ₹1,200 |
| Priya Patel | Delhi | 103 | Mechanical Keyboard | ₹4,500 |
| Neha Gupta | Pune | 104 | 4K IPS Monitor | ₹28,000 |
SELECT c.name, o.product FROM customers c INNER JOIN orders o ON c.id = o.customer_id;?ON c.id = o.id and got 0 results. How should the query be fixed?