- SQL
- DBMS
- Interview Preparation
- Placement Preparation
- Databases
SQL Joins Explained with Examples: INNER, LEFT, RIGHT, FULL, CROSS and SELF JOIN
A SQL join combines rows from two tables using a condition, usually a shared key. INNER JOIN keeps only matching rows; LEFT and RIGHT JOIN keep every row from one side and fill the other with NULL; FULL OUTER JOIN keeps everything from both; CROSS JOIN pairs every row with every row; a SELF JOIN joins a table to itself. The rest of this guide shows each one on the same two tables so you can see exactly which rows survive.
The two tables used in every example
Every example below uses the same data, so you can compare the output row by row. One employee has no department, and one department has no employees, because those are the cases that make the join types different.
employees
| id | name | dept_id |
|---|---|---|
| 1 | Asha | 10 |
| 2 | Ravi | 20 |
| 3 | Meera | NULL |
| 4 | Karan | 40 |
departments
| id | name |
|---|---|
| 10 | Engineering |
| 20 | Sales |
| 30 | HR |
Notice: Meera has no department at all, Karan points at a department (40) that does not exist, and HR (30) has nobody in it. Only Asha and Ravi match on both sides.
CREATE TABLE departments (id INT PRIMARY KEY, name VARCHAR(50));
CREATE TABLE employees (id INT PRIMARY KEY, name VARCHAR(50), dept_id INT);
INSERT INTO departments VALUES (10, 'Engineering'), (20, 'Sales'), (30, 'HR');
INSERT INTO employees VALUES (1, 'Asha', 10), (2, 'Ravi', 20), (3, 'Meera', NULL), (4, 'Karan', 40);
What is an INNER JOIN?
An INNER JOIN returns a row only when the ON condition is true for a pair of rows. Unmatched rows on either side are dropped.
SELECT e.name, d.name AS department
FROM employees e
INNER JOIN departments d ON d.id = e.dept_id;
| name | department |
|---|---|
| Asha | Engineering |
| Ravi | Sales |
Meera (NULL department), Karan (department 40 does not exist) and HR (no employees) all disappear. JOIN on its own means INNER JOIN; the word INNER is optional.
Use it when a row only makes sense if both sides exist: an order line and its product, a student and the course they enrolled in.
What is a LEFT JOIN?
A LEFT JOIN (also written LEFT OUTER JOIN) keeps every row from the left table, the one named before the JOIN keyword. When the right table has no match, its columns come back as NULL.
SELECT e.name, d.name AS department
FROM employees e
LEFT JOIN departments d ON d.id = e.dept_id;
| name | department |
|---|---|
| Asha | Engineering |
| Ravi | Sales |
| Meera | NULL |
| Karan | NULL |
All four employees survive. Meera and Karan get NULL because nothing matched. HR is still missing because it is on the right side and nobody referenced it.
This is the join you reach for in most reporting queries: "all customers, with their latest order if any", "all products, with their total sales even if zero".
What is a RIGHT JOIN?
A RIGHT JOIN keeps every row from the right table and fills the left with NULL. It is the exact mirror of LEFT JOIN.
SELECT e.name, d.name AS department
FROM employees e
RIGHT JOIN departments d ON d.id = e.dept_id;
| name | department |
|---|---|
| Asha | Engineering |
| Ravi | Sales |
| NULL | HR |
Now HR appears with no employee, while Meera and Karan are gone. The same result comes from swapping the table order and using LEFT JOIN, which is why many teams never write RIGHT JOIN at all: it is easier to read a query when the "keep all of these" table is always first.
SELECT e.name, d.name AS department
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.id; -- identical output
What is a FULL OUTER JOIN?
A FULL OUTER JOIN keeps every row from both tables. Matching rows are merged; unmatched rows from either side are padded with NULL.
SELECT e.name, d.name AS department
FROM employees e
FULL OUTER JOIN departments d ON d.id = e.dept_id;
| name | department |
|---|---|
| Asha | Engineering |
| Ravi | Sales |
| Meera | NULL |
| Karan | NULL |
| NULL | HR |
PostgreSQL, SQL Server, Oracle and SQLite (3.39+) support it. MySQL does not. In MySQL you emulate it with a UNION of the LEFT and RIGHT joins; UNION removes the duplicate matched rows:
SELECT e.name, d.name AS department
FROM employees e LEFT JOIN departments d ON d.id = e.dept_id
UNION
SELECT e.name, d.name
FROM employees e RIGHT JOIN departments d ON d.id = e.dept_id;
FULL OUTER JOIN is mostly used for reconciliation: comparing two lists and finding what is only in one of them.
What is a CROSS JOIN?
A CROSS JOIN has no ON condition. It pairs every row of the first table with every row of the second: the Cartesian product. 4 employees × 3 departments = 12 rows.
SELECT e.name, d.name AS department
FROM employees e
CROSS JOIN departments d;
| name | department |
|---|---|
| Asha | Engineering |
| Asha | Sales |
| Asha | HR |
| Ravi | Engineering |
| … | … |
It is useful on purpose for generating combinations (every size of every T-shirt, every day of a date range against every store). It happens by accident when you list two tables in FROM with a comma and forget the WHERE clause, which on a million-row table is a query that never finishes.
What is a SELF JOIN?
A self join is not a separate keyword. It is any join where both sides are the same table, under two different aliases. The classic case is a hierarchy stored in one table.
CREATE TABLE staff (id INT PRIMARY KEY, name VARCHAR(50), manager_id INT);
INSERT INTO staff VALUES (1, 'Priya', NULL), (2, 'Asha', 1), (3, 'Ravi', 1), (4, 'Karan', 2);
SELECT s.name AS employee, m.name AS manager
FROM staff s
LEFT JOIN staff m ON m.id = s.manager_id;
| employee | manager |
|---|---|
| Priya | NULL |
| Asha | Priya |
| Ravi | Priya |
| Karan | Asha |
A LEFT JOIN is used so the top of the tree (Priya, who has no manager) is not dropped. Other self-join uses: finding duplicate rows, pairing each row with the previous one by date, or listing employees who share a department with a given person.
The ON vs WHERE trap
This is the single most common join bug, and a favourite interview question. Suppose you want all employees, and the department name only when the department is in a given building:
-- WRONG: silently becomes an inner join
SELECT e.name, d.name
FROM employees e
LEFT JOIN departments d ON d.id = e.dept_id
WHERE d.name <> 'HR';
The LEFT JOIN first produces NULL department rows for Meera and Karan. Then the WHERE clause evaluates NULL <> 'HR', which is not true, so those rows are thrown away. The result has only Asha and Ravi, exactly as if you had written INNER JOIN.
-- RIGHT: extra conditions on the outer table go in ON
SELECT e.name, d.name
FROM employees e
LEFT JOIN departments d ON d.id = e.dept_id AND d.name <> 'HR';
Rule of thumb: in an outer join, conditions that filter the preserved (left) table can go in WHERE; conditions on the nullable (right) table belong in ON.
How many rows will a join return?
Interviewers like to ask this because it shows whether you understand matching, not just syntax.
| Join | Rows returned |
|---|---|
| INNER | One row per matching pair. Can be fewer than either table, or more if keys repeat on both sides |
| LEFT | At least every left row. Left rows with several matches are repeated once per match |
| RIGHT | At least every right row, same repetition rule |
| FULL | Matched pairs + unmatched left + unmatched right |
| CROSS | left rows × right rows |
Two things to remember:
- NULL never matches NULL.
NULL = NULLis unknown, not true, so a row with a NULL key is never joined by an equality condition. That is why Meera is unmatched even if a department also had a NULL id. - Duplicates multiply. If an employee appears twice in a table and their department appears twice in the other, the join produces four rows for that pair. Unexpected row counts after a join almost always mean a key you thought was unique is not.
Anti joins and semi joins
Two patterns built on joins show up constantly in real queries.
Anti join: rows with no match. Employees who are not in any existing department:
SELECT e.*
FROM employees e
LEFT JOIN departments d ON d.id = e.dept_id
WHERE d.id IS NULL; -- Meera, Karan
The same thing with NOT EXISTS, which many people find clearer and which the optimiser handles well:
SELECT e.*
FROM employees e
WHERE NOT EXISTS (SELECT 1 FROM departments d WHERE d.id = e.dept_id);
Avoid NOT IN (SELECT dept_id ...) when the subquery can contain NULL: a single NULL in the list makes NOT IN return no rows at all.
Semi join: rows that have at least one match, without duplicating them. Departments that have at least one employee:
SELECT d.*
FROM departments d
WHERE EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.id); -- Engineering, Sales
An INNER JOIN would give the same departments but repeated once per employee; EXISTS returns each department once.
USING, NATURAL JOIN and joining more than two tables
JOIN departments USING (dept_id)works when both columns have the same name and merges them into one output column. Fine for quick queries.NATURAL JOINjoins on every column with a matching name. Avoid it: adding a column callednameorcreated_atto either table silently changes the join.- Joins chain left to right. Each JOIN clause joins the result so far with the next table, so with three tables you can mix types:
orders LEFT JOIN customers ... INNER JOIN products .... Read it one step at a time and ask what rows exist after each step.
Joins and performance
The optimiser decides how to execute a join (nested loop, hash join, merge join); you decide whether it has what it needs.
- Index the join columns. Foreign keys such as
employees.dept_idshould be indexed; the primary key side already is. - Filter early. Put selective WHERE conditions on the driving table so fewer rows reach the join.
- Select only needed columns.
SELECT *across four tables drags wide rows through every step. - Run EXPLAIN. If you see a full table scan on the big table in a join, you are missing an index or a usable condition.
SQL join interview questions
- INNER vs LEFT JOIN? INNER keeps matches only; LEFT keeps all left rows with NULLs for missing right rows.
- Can a LEFT JOIN return fewer rows than the left table? No. It returns at least one row per left row, and more when a left row matches several right rows.
- Why is my LEFT JOIN behaving like an INNER JOIN? A WHERE condition on the right table drops the NULL-padded rows. Move it into ON.
- How do you get rows with no match? LEFT JOIN with
WHERE right.key IS NULL, or NOT EXISTS. - What is the output size of a CROSS JOIN? Rows in A × rows in B.
- How do you write FULL OUTER JOIN in MySQL? UNION of a LEFT JOIN and a RIGHT JOIN.
- Do NULL keys match in a join? Never with
=. UseIS NOT DISTINCT FROM(PostgreSQL) or COALESCE if you really need NULL-safe matching. - JOIN vs subquery? Joins return columns from both tables and are usually what the optimiser prefers; EXISTS subqueries are better for "does a match exist" checks because they do not duplicate rows.
Key takeaway
Pick the join by asking one question: which table's rows must survive even without a match? Both sides must match: INNER. Keep the left side: LEFT. Keep both: FULL. Every combination: CROSS. Then put filters on the nullable table in ON, not WHERE, and check row counts against the key rules above. For the rest of DBMS (normalisation, indexes, transactions, the questions that follow joins in an interview), download the free SQL handwritten notes and follow the placement preparation roadmap.
Frequently asked questions
What is the difference between INNER JOIN and LEFT JOIN?
INNER JOIN returns only the rows where the join condition matches in both tables. LEFT JOIN returns every row from the left table, and fills the right table's columns with NULL when there is no match. Use INNER JOIN when both sides must exist, and LEFT JOIN when you want to keep the left rows that have nothing on the other side, such as customers with no orders.
What is the difference between a LEFT JOIN and a RIGHT JOIN?
They are mirror images. LEFT JOIN keeps all rows from the table written before the JOIN keyword; RIGHT JOIN keeps all rows from the table written after it. Any RIGHT JOIN can be rewritten as a LEFT JOIN by swapping the table order, which is why most codebases use LEFT JOIN only.
What does a FULL OUTER JOIN return?
All rows from both tables. Rows that match on the join condition are combined into one row; rows with no match on the other side are kept with NULLs in the missing columns. MySQL does not support FULL OUTER JOIN, so you emulate it with a UNION of a LEFT JOIN and a RIGHT JOIN.
What is a CROSS JOIN?
A CROSS JOIN pairs every row of the first table with every row of the second, producing the Cartesian product. With 4 rows and 3 rows you get 12 rows. It has no ON clause and is useful for generating combinations, such as every product in every size, but accidentally writing one on large tables is a classic performance bug.
What is a SELF JOIN in SQL?
A self join joins a table to itself using two aliases, usually to compare rows within the same table. The standard example is an employees table with a manager_id column: joining employees e to employees m ON e.manager_id = m.id lists each employee next to their manager's name.
Why does my LEFT JOIN return the same rows as an INNER JOIN?
Almost always because a condition on the right table was put in the WHERE clause instead of the ON clause. After the LEFT JOIN fills unmatched rows with NULL, a WHERE filter like right_table.column = 'x' throws those NULL rows away, which turns the join into an inner join. Move the condition into the ON clause to keep the unmatched left rows.
How do you find rows in one table that have no match in another?
Use an anti join: LEFT JOIN the second table and keep only the rows where the second table's key IS NULL. For example, SELECT c.* FROM customers c LEFT JOIN orders o ON o.customer_id = c.id WHERE o.id IS NULL returns customers who never ordered. NOT EXISTS with a correlated subquery gives the same result and is often clearer.