Learn SQL with PostgreSQL - Advanced Table Joins (Relationships Between Tables)
Episode 6 of 21

Learn SQL with PostgreSQL - Advanced Table Joins (Relationships Between Tables)

This episode covers the concept of joins in relational databases: INNER JOIN, LEFT RIGHT and FULL OUTER JOIN, CROSS JOIN, as well as SELF JOIN for hierarchical data, including techniques for combining three or more tables in a single query.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

Welcome to episode 6 of the Learn SQL with PostgreSQL series! So far we've learned to manage a single table: creating it, filling it, and processing it. But now it's time for relational databases to show their true power: combining data from many tables. This is what distinguishes a relational database from a mere collection of spreadsheet files.

Imagine you have an orders table and a customers table. An order only stores customer_id, not the customer's full name. To display "Order #123 belonging to Budi Santoso", you have to combine both tables based on the foreign key relationship we built in episode 2. That's the job of JOIN.

In this episode, we'll cover the concept of joins thoroughly: INNER JOIN to take matching data, LEFT and RIGHT OUTER JOIN to preserve one side, FULL OUTER JOIN for all data, CROSS JOIN for Cartesian combinations, and SELF JOIN for hierarchical data. We'll also learn to combine three or more tables in a single query.

Basic Join Concepts

JOIN is an operation that combines rows from two tables based on a specific condition — usually a foreign key match. The result is new rows combining columns from both tables.

The easiest visualization is the circle diagram:

  • INNER JOIN: the intersection of two circles — only rows that match on both sides.
  • LEFT JOIN: the full left circle + the intersection — all left rows, only the matching right data.
  • RIGHT JOIN: the opposite of LEFT — all right rows.
  • FULL JOIN: the complete union of both circles.
Basic JOIN structure
SELECT *
FROM orders
JOIN customers ON orders.customer_id = customers.id;

The ON clause determines the joining condition — here it matches customer_id on orders with id on customers. The JOIN keyword itself is identical to INNER JOIN.

Tip

Because joined columns often share the same name (e.g. both have an id column), get in the habit of using table aliases: FROM orders o JOIN customers c ON o.customer_id = c.id. The query becomes shorter and column ambiguity disappears.

INNER JOIN: Only Matching Data

INNER JOIN is the most common type of join. It only returns rows that have a match in both tables. Orders without a matching customer won't appear.

INNER JOIN
SELECT
    o.id AS order_id,
    c.full_name AS customer,
    o.total
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
ORDER BY o.total DESC;

LEFT, RIGHT, and FULL OUTER JOIN

LEFT OUTER JOIN

LEFT JOIN takes ALL rows from the left table, plus the matching data from the right table. If there's no match, the right columns are filled with NULL. This is the most common pattern for finding "data without a partner":

LEFT JOIN: all customers including those without orders
SELECT c.full_name, o.id AS order_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
ORDER BY c.full_name;

Notice: customers who have never ordered will still appear with a order_id of NULL. This answers the query "show all customers along with their orders — including those who have never ordered".

Pattern: find customers without orders
SELECT c.full_name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;

The LEFT JOIN + WHERE right side IS NULL pattern is a classic idiom for finding left rows that have no partner.

RIGHT and FULL OUTER JOIN

RIGHT JOIN is the mirror image of LEFT JOIN: all right rows are preserved, left is filled with NULL if there's no match. FULL JOIN combines both: all rows from both sides, with NULL on the side that doesn't match.

RIGHT and FULL JOIN
SELECT c.full_name, o.id AS order_id
FROM orders o
RIGHT JOIN customers c ON o.customer_id = c.id;
 
SELECT c.full_name, o.id AS order_id
FROM customers c
FULL JOIN orders o ON o.customer_id = c.id;

Note

In practice, LEFT JOIN covers 95% of production join needs — you just place the "main" table on the left side. RIGHT JOIN can be replaced by swapping the table positions, and FULL JOIN is rarely used except for data auditing (finding data that exists on only one side).

CROSS JOIN: Cartesian Combination

CROSS JOIN produces the combination of every row in the left table with every row in the right table — without an ON condition. If the left table has 10 rows and the right has 5, the result is 50 rows.

CROSS JOIN
SELECT p.name, s.size_label
FROM products p
CROSS JOIN product_sizes s;

The example above is suitable for generating a product-per-size list. But be careful: CROSS JOIN can explode easily — a 1000×1000 table produces 1 million rows.

Warning

CROSS JOIN is one of the most common causes of "hanging" queries in production — forgetting to write ON on a regular join will silently turn it into a CROSS JOIN in some databases, or produce wrong results. Always check that every JOIN has a correct ON condition.

SELF JOIN: A Table Joining Itself

SELF JOIN joins a table with itself. This odd-sounding name is very useful for hierarchical data — e.g. an employees table with a manager_id column pointing to another employee, or categories that have a parent category.

Table for hierarchical data
CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    manager_id INTEGER REFERENCES employees(id)
);

To display each employee's name along with their manager's name, we join the table with itself using different aliases:

SELF JOIN for the employee hierarchy
SELECT
    e.name AS karyawan,
    m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id
ORDER BY m.name NULLS FIRST;

Notice two things: the same table is joined twice with the aliases e (employee) and m (manager), and we use LEFT JOIN so that employees without a manager (the CEO) still appear. This SELF JOIN pattern is also used for multi-level category structures and social networks ("friends of friends" relationships).

Multi-Table Joins: Three Tables or More

The real world rarely involves only two tables. An order consists of an order header, order items, and products. To display the product name on each order, we combine three tables at once:

Join three tables
SELECT
    o.id AS order_id,
    c.full_name AS customer,
    p.name AS product,
    oi.quantity
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.id = 123;

Each JOIN is added sequentially, and each has an ON condition connecting its new table to the previous result. There's no hard limit on the number of joins, but the more joins, the higher the execution cost — in episode 15 we'll learn to analyze their performance.

Tip

To avoid inflated results (accidental duplicates) in multi-joins, think about the direction of the relationship first. If one order has 3 items, joining orders → order_items will multiply the order rows into 3. That's when aggregations like SUM(oi.quantity) often get summed multiple times over. Make sure you understand the data structure before summing.

Common Join Mistakes

#MistakeSymptomSolution
1Join without ONExploding results (Cartesian)Always write an ON condition
2Using INNER JOIN when "empty" data is neededRows without a partner disappearSwitch to LEFT JOIN
3Forgetting table aliases on ambiguous columnsError column reference is ambiguousAlias the tables and write table.column
4Summing after a multi-joinInflated aggregate numbersAggregate before joining, or understand the data structure

Closing

In this episode 6 we've mastered joins in all their forms: INNER JOIN for matching data, LEFT/RIGHT/FULL JOIN to preserve one or both sides, CROSS JOIN for Cartesian combinations, SELF JOIN for hierarchical data, and the multi-table join technique for combining many tables in a single query.

Key takeaways:

  • INNER JOIN takes the intersection; LEFT JOIN preserves the left table.
  • The LEFT JOIN ... WHERE right side IS NULL pattern finds data without a partner.
  • SELF JOIN is the key to hierarchical data (employee-manager, multi-level categories).
  • Always write an ON condition — without it the query becomes a dangerous CROSS JOIN.
  • Understand the direction of the relationship before aggregating multi-join results.

In episode 7, we'll expand our query capabilities: Subqueries, Set Operations & Expressions — from subqueries in WHERE, FROM, and SELECT, the CASE WHEN branching logic, NULL-handling functions like COALESCE and NULLIF, to the set operations UNION, INTERSECT, and EXCEPT. You'll see just how expressive SQL really is.

Learn SQL with PostgreSQL - Advanced Table Joins (Relationships Between Tables) | Learn SQL with PostgreSQL