By WebNest Studio Engineering Team
Database and SQL Tutorial
SQL Joins: INNER and LEFT JOIN
A join combines rows from two or more tables based on a related column between them, most often a foreign key matching a primary key. Joins are what let a normalized database — split into small, non-redundant tables — be reassembled back into the combined view an application or report actually needs.
Why it matters
Normalised data is spread across tables, and joins are how it is put back together: orders with their customers, students with their courses. Joins are the heart of SQL and the topic interviewers test most. The classic mistake is an inner join that silently drops rows that have no match.
INNER JOIN
INNER JOIN returns only the rows that have a match in both tables. If a row in the left table has no corresponding row in the right table (based on the join condition), that row is excluded entirely from the result. INNER JOIN is the default and most commonly used join type when you only care about complete, matched pairs.
LEFT JOIN (LEFT OUTER JOIN)
LEFT JOIN returns every row from the left table, whether or not it has a match in the right table. When there is no match, the columns from the right table are filled with NULL for that row. This makes LEFT JOIN the right tool whenever you need to include "rows with nothing related yet" — for example, listing every customer including those who have never placed an order.
The Practical Difference in Results
Given a customers table and an orders table, an INNER JOIN between them returns only customers who have placed at least one order — a customer with zero orders simply disappears from the result. A LEFT JOIN from customers to orders returns every customer, and for any customer with no orders, the order columns appear as NULL instead of the customer row being dropped. Choosing the wrong join type is a very common source of "missing rows" bugs in reports.
Examples
INNER JOIN drops customers with no orders
SELECT c.name, o.id AS order_id
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id;
Returns 2 rows: ('Asha Rao', 101), ('Rohit Sharma', 102) — a customer named 'Meera Iyer' with zero orders does not appear.
LEFT JOIN keeps every customer, using NULL for unmatched orders
SELECT c.name, o.id AS order_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id;
Returns 3 rows: ('Asha Rao', 101), ('Rohit Sharma', 102), ('Meera Iyer', NULL) — Meera now appears with a NULL order_id.
Common Mistakes
- Using INNER JOIN when the goal is "show everything, including things with no related rows" — this silently drops unmatched rows instead of showing them.
- Forgetting the ON condition (or writing an incorrect one), which can produce a cross join-like explosion of unintended row combinations.
- Filtering on a right-table column in the WHERE clause after a LEFT JOIN (e.g., "WHERE o.status = 'shipped'") without accounting for NULLs, which can inadvertently turn the LEFT JOIN back into behaving like an INNER JOIN.
- Assuming JOIN always means INNER JOIN in every database — always specify the join type explicitly for clarity, since relying on defaults reduces readability.
Exercise
List every customer with the products they have ordered. A customer with no orders must still appear, with NULL as the product. Order the result by customer name and then by product.
Expected output: name | product, Asha | book, Asha | pen, Ravi | NULL (one per line)
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER REFERENCES customers(id), product TEXT);
INSERT INTO customers (id, name) VALUES (1, 'Asha'), (2, 'Ravi');
INSERT INTO orders (id, customer_id, product) VALUES (1, 1, 'pen'), (2, 1, 'book');
-- TODO: every customer with their products, including customers with no orders
Show hints
- An inner join would leave Ravi out, because he has no matching order.
LEFT JOINkeeps every row of the left table and fills the right-hand columns withNULLwhere there is no match.
Show solution
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER REFERENCES customers(id), product TEXT);
INSERT INTO customers (id, name) VALUES (1, 'Asha'), (2, 'Ravi');
INSERT INTO orders (id, customer_id, product) VALUES (1, 1, 'pen'), (2, 1, 'book');
SELECT c.name, o.product
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.name, o.product;
Quick quiz
1. Which rows does an INNER JOIN return?
- A. All rows from both tables
- B. All rows from the left table
- C. Only the rows that have a match in both tables
- D. Only the rows without a match
Show answer
C. Rows on either side with no partner are left out.
2. How do you find customers who have never placed an order?
- A. LEFT JOIN orders and keep the rows where the order id IS NULL
- B. INNER JOIN orders
- C. SELECT DISTINCT customers
- D. CROSS JOIN orders
Show answer
A. The unmatched customers are exactly those with NULL on the orders side.
3. What happens when a join is written without any join condition?
- A. An error is raised
- B. Only matching rows are returned
- C. No rows are returned
- D. Every row of one table is paired with every row of the other
Show answer
D. This is a Cartesian product, or cross join: 1,000 rows by 1,000 rows gives a million.
Interview questions
Explain the different types of join.
Show answer
INNER JOIN returns only rows with a match in both tables. LEFT JOIN returns every row of the left table, with NULL in the right-hand columns where there is no match. RIGHT JOIN is the mirror image. FULL OUTER JOIN returns all rows from both sides, matched where possible. CROSS JOIN pairs every row with every row. A self join joins a table to itself, for example employees to their managers.
In a LEFT JOIN, what is the difference between a condition in ON and one in WHERE?
Show answer
A condition in ON decides which right-hand rows match; left-hand rows with no match are still returned, with nulls. A condition in WHERE is applied after the join to the combined rows, so a filter there on a right-hand column removes the rows that have nulls, which turns the left join into an inner join in effect. To keep unmatched rows, conditions on the right-hand table belong in ON.
Key Points to Remember
- INNER JOIN returns only rows with matches in both tables; unmatched rows are excluded.
- LEFT JOIN returns all rows from the left table, filling unmatched right-table columns with NULL.
- The join condition (ON clause) defines which columns must match between the two tables.
- Choosing INNER vs. LEFT JOIN changes which rows appear in the result, not just how they are formatted.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering