Module F-5·18 min read

INNER JOIN, LEFT JOIN, FULL OUTER JOIN, self-joins, and multi-table queries — the most feared concept made simple.

Written by Jatin Jain Saraf · Architect of SupraScan

F-5 — Connecting Tables — Joins Demystified

Who this module is for: You completed F-4 and understand aggregation. Now you need to combine data from multiple tables — the foundational skill that makes relational databases genuinely powerful. Joins confuse almost every beginner because they require a new mental model. This module builds that model step by step.


Why Data Lives in Multiple Tables

Imagine a simple online store. You could store everything in one table:

orders_messy:
  order_id | customer_name | customer_email | product_name | product_price | quantity
  1        | Alice         | alice@...      | Keyboard     | 129.99        | 1
  2        | Alice         | alice@...      | Mouse        | 49.99         | 2
  3        | Bob           | bob@...        | Keyboard     | 129.99        | 1

This has problems:

  • Alice's email is stored twice — if she changes it, you must update multiple rows
  • The Keyboard's price is stored twice — if it changes, you must update multiple rows
  • Adding a new customer who has not ordered yet is impossible without a fake order
  • Removing all orders for a customer accidentally removes their account

The solution: split the data into separate tables, each representing one concept, and connect them with foreign keys.

customers:                    products:
  id | name  | email          id | name     | price
  1  | Alice | alice@...      1  | Keyboard | 129.99
  2  | Bob   | bob@...        2  | Mouse    | 49.99
 
orders:
  id | customer_id | product_id | quantity
  1  | 1           | 1          | 1        ← Alice bought Keyboard
  2  | 1           | 2          | 2        ← Alice bought Mouse
  3  | 2           | 1          | 1        ← Bob bought Keyboard

Now Alice's email lives in one place. The Keyboard's price lives in one place. Joins let you reconstruct the combined view when you need it.


Setting Up the Example Schema

Create these tables to follow along:

CREATE DATABASE store;
\c store
 
CREATE TABLE customers (
  id    BIGSERIAL   PRIMARY KEY,
  name  TEXT        NOT NULL,
  email TEXT        NOT NULL UNIQUE,
  city  TEXT
);
 
CREATE TABLE products (
  id       BIGSERIAL      PRIMARY KEY,
  name     TEXT           NOT NULL,
  price    NUMERIC(10,2)  NOT NULL,
  category TEXT           NOT NULL
);
 
CREATE TABLE orders (
  id          BIGSERIAL   PRIMARY KEY,
  customer_id BIGINT      REFERENCES customers(id),
  product_id  BIGINT      REFERENCES products(id),
  quantity    INTEGER     NOT NULL,
  ordered_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Note: REFERENCES only creates the constraint — PostgreSQL does NOT
-- automatically index customer_id or product_id. Every join below relies on
-- these columns, and on a large orders table an unindexed FK column turns
-- every one of these joins into a sequential scan on `orders`.
 
INSERT INTO customers (name, email, city) VALUES
  ('Alice',  'alice@example.com', 'New York'),
  ('Bob',    'bob@example.com',   'London'),
  ('Carol',  'carol@example.com', 'Berlin'),
  ('David',  'david@example.com', NULL);       -- David has no city yet
 
INSERT INTO products (name, price, category) VALUES
  ('Mechanical Keyboard', 129.99, 'peripherals'),
  ('Wireless Mouse',       49.99, 'peripherals'),
  ('Monitor Stand',        39.99, 'accessories'),
  ('USB-C Hub',            64.99, 'accessories'),
  ('Laptop Sleeve',        24.99, 'accessories');
 
INSERT INTO orders (customer_id, product_id, quantity) VALUES
  (1, 1, 1),   -- Alice bought Keyboard
  (1, 2, 2),   -- Alice bought Mouse (2x)
  (2, 1, 1),   -- Bob bought Keyboard
  (3, 3, 1),   -- Carol bought Monitor Stand
  (3, 4, 1);   -- Carol bought USB-C Hub
-- Note: David (id=4) has no orders
-- Note: Laptop Sleeve (id=5) has no orders

INNER JOIN — Only Matching Rows

An INNER JOIN returns rows that have a match in both tables. If a customer has no orders, they do not appear. If an order references a customer that doesn't exist, it doesn't appear.

SELECT customers.name, orders.quantity, products.name AS product
FROM orders
INNER JOIN customers ON orders.customer_id = customers.id
INNER JOIN products  ON orders.product_id  = products.id;
 
--  name  | quantity |       product
-- -------+----------+---------------------
--  Alice |        1 | Mechanical Keyboard
--  Alice |        2 | Wireless Mouse
--  Bob   |        1 | Mechanical Keyboard
--  Carol |        1 | Monitor Stand
--  Carol |        1 | USB-C Hub

Notice:

  • David does not appear — he has no orders
  • Laptop Sleeve does not appear — nobody ordered it

The syntax:

SELECT [columns]
FROM table_a
INNER JOIN table_b ON table_a.column = table_b.column;
 
-- 'INNER' is optional — bare JOIN means INNER JOIN
SELECT [columns]
FROM table_a
JOIN table_b ON table_a.column = table_b.column;

Using aliases to shorten table names:

SELECT c.name, o.quantity, p.name AS product
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN products  p ON o.product_id  = p.id;

LEFT JOIN — Keep All Left Rows

A LEFT JOIN returns all rows from the left table, plus matching rows from the right. If there is no match in the right table, the right side columns are NULL.

-- List ALL customers, and their orders if they have any
SELECT c.name, o.quantity, p.name AS product
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
LEFT JOIN products p ON o.product_id = p.id
ORDER BY c.name;
 
--  name  | quantity |       product
-- -------+----------+---------------------
--  Alice |        1 | Mechanical Keyboard
--  Alice |        2 | Wireless Mouse
--  Bob   |        1 | Mechanical Keyboard
--  Carol |        1 | Monitor Stand
--  Carol |        1 | USB-C Hub
--  David |   (null) |              (null)  ← David has no orders

David appears with NULL values for the order columns because he has no orders — but he is not excluded. This is the key difference from INNER JOIN.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.