🗄️ SQL Deep Dive · August 2026 · 18 min read

SQL Joins Explained
INNER, LEFT, RIGHT, FULL, SELF & CROSS

Every join type — with plain-English meaning, Indian business examples, actual SQL queries, and the interview questions asked at Genpact, Wipro and Infosys.

By Prashant Shukla · SQL Trainer, EVIKA Academy Noida Sector 51

SQL Course Noida →🟢 Free Demo

What is a SQL JOIN — and Why Does It Matter?

A database never stores everything in one table. A company's data lives in dozens of separate tables — one for customers, one for orders, one for products, one for employees, one for departments. Each table stores one kind of thing, and they are linked together through matching ID columns.

A JOIN is how you ask SQL to combine rows from two or more of these tables into one result set. Without JOINs, you can only ever see one table at a time — which is almost never enough for real business analysis.

As a data analyst, you will use JOINs in almost every non-trivial query you write. They are asked in nearly every SQL interview in India. Understanding them deeply — not just the syntax, but when to use each type and what goes wrong — is what separates analysts who can do real work from those who have just read a cheat sheet.

🔑 The mental model — one sentence per join type:
INNER JOINOnly rows that exist in BOTH tables.
LEFT JOINAll rows from the LEFT table, matched from right.
RIGHT JOINAll rows from the RIGHT table, matched from left.
FULL OUTER JOINAll rows from BOTH tables, NULLs where no match.
SELF JOINA table joined to itself to find internal relationships.
CROSS JOINEvery possible combination of rows from both tables.

INNER JOIN

In plain English:
Give me ONLY the rows that exist in BOTH tables.
Imagine a list of orders and a list of customers. INNER JOIN gives you only the orders that have a valid customer attached — nothing else. If an order has a customer ID that does not exist in the customer table, that order disappears from your result.
Basic Syntax
SELECT o.order_id, c.customer_name, o.order_total
FROM orders o
INNER JOIN customers c
  ON o.customer_id = c.customer_id;
📌 Real Example — India Context
You work at a Delhi NCR e-commerce company. You have an orders table and a products table. You want a report of all orders that have valid product details — to calculate revenue per product.
SELECT p.product_name,
       COUNT(o.order_id)  AS total_orders,
       SUM(o.order_total) AS revenue
FROM orders o
INNER JOIN products p
  ON o.product_id = p.product_id
GROUP BY p.product_name
ORDER BY revenue DESC;
✓ Result: Only orders with a matched product in the products table appear. Orphan orders (deleted products) are excluded.
✦ When to use INNER JOIN: When you only care about rows that have a match in both tables. The most common join for business reporting — product sales, employee attendance, customer transactions.
🎯 Interview Question: Write a query to find all employees who have been assigned to a project.
SELECT e.employee_name, p.project_name
FROM employees e
INNER JOIN projects p ON e.project_id = p.project_id;
⚠️ Common mistake: Using INNER JOIN when you actually need LEFT JOIN — you end up silently dropping rows that do not have a match, which skews your analysis without any error or warning.
⬅️

LEFT JOIN

In plain English:
Give me ALL rows from the LEFT table, even if there is no match on the right.
Take every row from the first table you name. If there is a match in the second table, bring it along. If there is no match, fill the second table's columns with NULL. Nothing from the left table ever gets dropped.
Basic Syntax
SELECT c.customer_name, o.order_id, o.order_total
FROM customers c
LEFT JOIN orders o
  ON c.customer_id = o.customer_id;
📌 Real Example — India Context
A Noida-based retail analytics team needs a report of ALL registered customers — including those who signed up but never placed a single order. A marketing campaign needs this list to re-engage inactive users.
SELECT c.customer_name,
       c.city,
       COUNT(o.order_id)       AS total_orders,
       COALESCE(SUM(o.order_total), 0) AS lifetime_value
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name, c.city
ORDER BY total_orders ASC;
✓ Result: All customers appear. Those with zero orders show total_orders = 0 and lifetime_value = 0. You can see exactly who has never bought.
✦ When to use LEFT JOIN: Anytime your analysis must include records from the primary table even if the secondary table has no data. Most frequent use cases: finding customers with no orders, employees with no attendance records, products with no sales this month.
🎯 Interview Question: Find all employees who have NOT been assigned to any department.
SELECT e.employee_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
WHERE d.department_id IS NULL;
⚠️ Common mistake: Forgetting to filter on NULL in the right table when you actually want the "no match" records. Without WHERE right_table.id IS NULL, you get all rows including matched ones — which is just a more expensive INNER JOIN result.
➡️

RIGHT JOIN

In plain English:
Give me ALL rows from the RIGHT table, even if there is no match on the left.
The mirror image of LEFT JOIN. Every row from the second table you name is guaranteed to appear. Rows from the first table appear only if they match. Non-matching left rows get NULL for left-table columns.
Basic Syntax
SELECT e.employee_name, d.department_name
FROM employees e
RIGHT JOIN departments d
  ON e.department_id = d.department_id;
📌 Real Example — India Context
An HR analyst at a Gurgaon MNC needs to see ALL departments — including departments that have no employees yet (newly created teams). An INNER JOIN would silently exclude those empty departments.
SELECT d.department_name,
       COUNT(e.employee_id) AS employee_count,
       AVG(e.salary)        AS avg_salary
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.department_id
GROUP BY d.department_name
ORDER BY employee_count DESC;
✓ Result: All departments appear. Empty ones show employee_count = 0 and avg_salary = NULL. This immediately surfaces newly created but unstaffed departments.
✦ When to use RIGHT JOIN: When the focus is on ensuring no row from the second table is dropped. In practice, most analysts rewrite RIGHT JOINs as LEFT JOINs by swapping the table order — same result, easier to read.
🎯 Interview Question: Can you rewrite a RIGHT JOIN as a LEFT JOIN?
-- These two queries return identical results:

-- RIGHT JOIN version:
SELECT e.name, d.department_name
FROM employees e RIGHT JOIN departments d ON e.dept_id = d.id;

-- Equivalent LEFT JOIN (swap table order):
SELECT e.name, d.department_name
FROM departments d LEFT JOIN employees e ON d.id = e.dept_id;
⚠️ Common mistake: Writing RIGHT JOINs when working in a team — they make queries harder to follow because readers expect the "main" table to be on the left. Convert to LEFT JOIN with swapped table order for readability.
🔄

FULL OUTER JOIN

In plain English:
Give me ALL rows from BOTH tables, matched where possible, NULL where not.
Run a LEFT JOIN and a RIGHT JOIN simultaneously, then combine the results. Every row from both tables appears. Where there is a match, columns from both sides are populated. Where there is no match on either side, the other table's columns come back as NULL.
Basic Syntax
SELECT a.column1, b.column2
FROM table_a a
FULL OUTER JOIN table_b b
  ON a.id = b.a_id;
📌 Real Example — India Context
A data reconciliation task at a fintech company in Noida — you have two independently maintained tables of transactions: one from the payment gateway and one from the internal accounting system. You need to find transactions in either system that do not appear in the other (mismatches).
SELECT
  COALESCE(pg.txn_id, acc.txn_id) AS transaction_id,
  pg.amount                        AS gateway_amount,
  acc.amount                       AS accounting_amount,
  CASE
    WHEN pg.txn_id  IS NULL THEN 'Missing in Gateway'
    WHEN acc.txn_id IS NULL THEN 'Missing in Accounting'
    ELSE 'Matched'
  END AS status
FROM payment_gateway pg
FULL OUTER JOIN accounting acc ON pg.txn_id = acc.txn_id
WHERE pg.txn_id IS NULL OR acc.txn_id IS NULL;
✓ Result: Every unmatched transaction surfaces — whether it is missing from the gateway or from accounting. This is a classic audit and reconciliation query.
✦ When to use FULL OUTER JOIN: Data reconciliation between two systems, audits, finding discrepancies between two datasets that should match. Less common in daily reporting, essential for data quality work.
🎯 Interview Question: MySQL does not support FULL OUTER JOIN. How do you simulate it?
-- Simulate FULL OUTER JOIN in MySQL:
SELECT a.id, a.name, b.value
FROM table_a a LEFT JOIN table_b b ON a.id = b.a_id

UNION

SELECT a.id, a.name, b.value
FROM table_a a RIGHT JOIN table_b b ON a.id = b.a_id;
⚠️ Common mistake: Using FULL OUTER JOIN where a simpler INNER or LEFT JOIN would do — it is the most expensive join type. Reach for it only when you genuinely need all rows from both sides.
🔁

SELF JOIN

In plain English:
Join a table to itself to find relationships within the same dataset.
A table that contains rows which reference other rows in the same table — like an employee table where each employee has a manager_id that points to another employee's id. A SELF JOIN lets you query these internal relationships.
Basic Syntax
SELECT e.employee_name, m.employee_name AS manager_name
FROM employees e
LEFT JOIN employees m
  ON e.manager_id = m.employee_id;
📌 Real Example — India Context
An HR dashboard at a Delhi NCR IT company needs to display each employee alongside their manager's name. Both the employee and the manager are rows in the same employees table.
SELECT
  e.employee_name,
  e.designation,
  e.salary,
  COALESCE(m.employee_name, 'No Manager (CEO/Head)') AS reports_to
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id
ORDER BY m.employee_name, e.employee_name;
✓ Result: Every employee appears with their manager's name. The top-level person (no manager) shows "No Manager (CEO/Head)" due to COALESCE.
✦ When to use SELF JOIN: Organisational hierarchies, product categories with subcategories, finding previous and next records in a time series.
🎯 Interview Question: Write a query to find employees who earn more than their manager.
SELECT e.employee_name, e.salary,
       m.employee_name AS manager_name, m.salary AS manager_salary
FROM employees e
INNER JOIN employees m ON e.manager_id = m.employee_id
WHERE e.salary > m.salary;
⚠️ Common mistake: Forgetting to alias the table twice — you must give each "copy" of the table a different alias (e and m above) otherwise SQL cannot distinguish which version of the table each column comes from.
✖️

CROSS JOIN

In plain English:
Give me every possible combination of rows from both tables.
No ON condition. Every row in the first table is combined with every row in the second table. 10 rows × 5 rows = 50 rows in the result. This is called the Cartesian product. Use it intentionally — it grows very fast.
Basic Syntax
SELECT products.product_name, sizes.size_name
FROM products
CROSS JOIN sizes;
📌 Real Example — India Context
A clothing retailer in Noida wants to generate every combination of product and size for their inventory template — before they upload actual stock quantities. They have 20 products and 5 sizes, so they need 100 rows.
SELECT
  p.product_name,
  p.category,
  s.size_label,
  0 AS stock_quantity  -- placeholder, to be filled by the team
FROM products p
CROSS JOIN sizes s
ORDER BY p.product_name, s.size_label;
✓ Result: 100 rows — every product paired with every size. This template is exported to Excel and given to the inventory team to fill stock quantities.
✦ When to use CROSS JOIN: Generating all possible combinations — product + size, date + region, question + answer option. Rarely used in analytics reporting, commonly used in data engineering and test data generation.
🎯 Interview Question: When would you use a CROSS JOIN in real work?
Generating a date spine (every date in a range paired with every region) to fill gaps in time series data. Example: CROSS JOIN a dates table with a regions table to get a row for every date-region combination, then LEFT JOIN actual data onto it — so missing data appears as 0 instead of being invisible.
⚠️ Common mistake: Accidentally writing a CROSS JOIN by forgetting the ON clause in an INNER JOIN. If you write JOIN with no ON condition in some databases, you get a Cartesian product — millions of rows when you expected thousands.

Advanced JOIN Patterns — Used in Real Work

🔗 Multiple Table JOINs
You can chain as many JOINs as you need. SQL executes them left to right — each JOIN builds on the result of the previous one. Keep the logic clean by always joining on the most relevant key and being explicit about which table each column comes from using aliases.
-- Report: order details with customer name and product info
SELECT
  c.customer_name,
  c.city,
  o.order_date,
  p.product_name,
  p.category,
  o.quantity,
  o.quantity * p.unit_price AS line_total
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
INNER JOIN products  p ON o.product_id  = p.product_id
WHERE o.order_date >= '2026-01-01'
ORDER BY o.order_date DESC;
💡 Pro tip: Put the table with the most selective filter first in your join chain — it reduces the intermediate result size and speeds up the query.
📊 JOIN with GROUP BY — Aggregations Across Tables
The most common real-world pattern: JOIN to bring related data together, then GROUP BY to summarise it. Always alias aggregated columns for readability.
-- Monthly revenue per product category
SELECT
  p.category,
  DATE_FORMAT(o.order_date, '%Y-%m') AS month,
  COUNT(o.order_id)                   AS orders,
  SUM(o.quantity * p.unit_price)      AS revenue
FROM orders o
INNER JOIN products p ON o.product_id = p.product_id
GROUP BY p.category, DATE_FORMAT(o.order_date, '%Y-%m')
ORDER BY month DESC, revenue DESC;
💡 Pro tip: If you get unexpected row counts after joining and grouping, check for duplicate keys in your join column — a common source of double-counting.
🔍 JOIN with Subquery
Sometimes you need to join not to a raw table but to the result of another query. Write the subquery in parentheses and give it an alias — SQL treats it like a temporary table.
-- Join orders to only the top 10 customers by lifetime value
SELECT o.order_id, o.order_date, top_customers.customer_name
FROM orders o
INNER JOIN (
  SELECT customer_id, customer_name,
         SUM(order_total) AS lifetime_value
  FROM orders
  JOIN customers USING (customer_id)
  GROUP BY customer_id, customer_name
  ORDER BY lifetime_value DESC
  LIMIT 10
) AS top_customers ON o.customer_id = top_customers.customer_id;
💡 Pro tip: If you reference the same subquery more than once, move it into a CTE (WITH clause) instead — much cleaner and runs only once.
⚠️ Handling NULL Values After JOINs
After a LEFT, RIGHT or FULL JOIN, columns from the "optional" side come back as NULL when there is no match. Handle them explicitly to avoid incorrect totals and misleading charts.
-- Replace NULL order totals with 0 for inactive customers
SELECT
  c.customer_name,
  c.registration_date,
  COALESCE(COUNT(o.order_id), 0)       AS total_orders,
  COALESCE(SUM(o.order_total), 0.00)   AS lifetime_spend,
  CASE
    WHEN MAX(o.order_date) IS NULL         THEN 'Never Ordered'
    WHEN MAX(o.order_date) < NOW() - INTERVAL 90 DAY THEN 'Inactive (90+ days)'
    ELSE 'Active'
  END AS customer_status
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name, c.registration_date;
💡 Pro tip: Always wrap aggregates (SUM, COUNT, AVG) in COALESCE after a LEFT JOIN. SUM(NULL) returns NULL, not 0 — which can break calculations downstream.

Interview Traps — Questions That Catch Most Candidates

Q: What is the difference between WHERE and ON in a JOIN?
ON is the join condition — it defines how the two tables relate. WHERE filters the result after the join happens. In INNER JOINs the difference is subtle. In LEFT JOINs it matters critically: a filter in ON is applied before the join (so unmatched rows still appear as NULL), while a filter in WHERE is applied after (so it drops the NULL rows, turning your LEFT JOIN into an INNER JOIN silently).
-- These are NOT the same for a LEFT JOIN:

-- Filter in ON → still keeps all customers, just limits which orders match
SELECT c.name, o.order_total
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id AND o.status = 'completed';

-- Filter in WHERE → drops customers with no completed orders (acts like INNER JOIN)
SELECT c.name, o.order_total
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.status = 'completed';
Q: What happens when a JOIN column has duplicate values?
Row multiplication. If orders has 3 rows for customer_id = 5 and customers also has 2 rows for customer_id = 5 (data quality issue), the join produces 6 rows (3 × 2). This inflates your SUM and COUNT aggregations. Always check for duplicates on join keys before running aggregate queries on joined tables.
-- Check for duplicates in join key before joining
SELECT customer_id, COUNT(*) as cnt
FROM customers
GROUP BY customer_id
HAVING COUNT(*) > 1;
-- If this returns rows, your JOIN will multiply data
Q: How do you find records in Table A that do NOT exist in Table B?
Three approaches: LEFT JOIN with NULL check (most readable), NOT EXISTS (most semantically clear), NOT IN (works but has NULL trap — avoid when the subquery column can contain NULLs).
-- Method 1: LEFT JOIN + IS NULL (most common in interviews)
SELECT a.* FROM table_a a
LEFT JOIN table_b b ON a.id = b.a_id
WHERE b.a_id IS NULL;

-- Method 2: NOT EXISTS (clearest intent)
SELECT * FROM table_a a
WHERE NOT EXISTS (SELECT 1 FROM table_b b WHERE b.a_id = a.id);

-- Method 3: NOT IN (avoid if table_b.a_id can have NULLs)
SELECT * FROM table_a
WHERE id NOT IN (SELECT a_id FROM table_b WHERE a_id IS NOT NULL);

Quick Reference — All Joins at a Glance

Join Type
Returns
Use When
INNER JOIN
Only rows that match in both tables
You need clean, complete matches
LEFT JOIN
All left rows + matched right rows (NULL if no match)
All primary records must appear
RIGHT JOIN
All right rows + matched left rows (NULL if no match)
All secondary records must appear
FULL OUTER JOIN
All rows from both tables (NULL where no match)
Reconciliation, audits
SELF JOIN
Rows joined to other rows in the same table
Hierarchy, manager lookups
CROSS JOIN
Every row × every row (Cartesian product)
Generate all combinations

Frequently Asked Questions

Q: What is the most important SQL JOIN for data analyst interviews in India?
LEFT JOIN is the single most important join to know well for data analyst interviews. It appears in more real business scenarios than any other join type — finding customers with no orders, products with no sales, employees not assigned to departments. INNER JOIN is asked most frequently (it is the simplest), but LEFT JOIN is where most candidates get tripped up. Master both and you will clear 90% of SQL interview rounds.
Q: What is the difference between INNER JOIN and LEFT JOIN?
INNER JOIN returns only rows that have a match in both tables — unmatched rows from either table are dropped. LEFT JOIN returns all rows from the left (first) table whether or not there is a match in the right table. When there is no match, the right table's columns come back as NULL. Use INNER JOIN when you need clean, matched data. Use LEFT JOIN when you must preserve all records from the main table even if related data is missing.
Q: Does MySQL support FULL OUTER JOIN?
No. MySQL does not support FULL OUTER JOIN directly. To simulate it, use UNION to combine a LEFT JOIN and a RIGHT JOIN on the same tables and same condition. PostgreSQL, SQL Server, and Oracle all support FULL OUTER JOIN natively. In data analyst interviews in India, SQL Server and MySQL are the most tested databases — so knowing how to simulate FULL OUTER JOIN in MySQL is a useful trick to mention.
Q: What is a SELF JOIN and when is it used?
A SELF JOIN is when you join a table to itself — typically by giving it two different aliases. The most common real-world use case is an employee-manager relationship where both the employee and the manager are stored in the same employees table, and each employee row has a manager_id that points to another row in the same table. Self joins are also used for finding duplicate records, comparing adjacent rows, and working with category-subcategory hierarchies.
Q: How do you optimise a slow JOIN query?
First, make sure the columns used in the ON clause are indexed in both tables — this is the single biggest performance gain. Second, reduce the data before joining by filtering with WHERE early. Third, select only the columns you need rather than SELECT *. Fourth, check for duplicate values in the join key that might be causing row multiplication. Fifth, consider breaking a very complex multi-table join into steps using CTEs or temporary tables.
Continue Learning SQL
SQL Interview Questions for Data Analyst 2026 — 50+ with Answers30-Day SQL Practice Series — One Question Per DayData Analytics Roadmap India 2026 — Full Career PathSQL for Data Analytics Course — EVIKA Academy Noida
📍 SQL COURSE · NOIDA SECTOR 51 + ONLINE · ₹5,999

Learn SQL with Live Practice on Real Datasets

EVIKA Academy SQL course — 6 weeks, daily query practice, mock interview rounds, real e-commerce and HR datasets. Weekday and weekend batches.

🟢 WhatsApp — Book Free DemoView SQL Course →