DevLearningTools

LEARN · SQL

SQL Joins

Joins combine rows from two or more related tables in a single query. Learn why Beacon Robotics' data is split across tables, and the schema and vocabulary the rest of this module builds on.

Every query so far has asked one table, employees, for data, and employees.department has been a plain text column since Module 1. That's been fine for learning WHERE and aggregates, but it hides a real problem: nothing stops two rows from storing 'Engineering' and 'Enginering', and renaming a department means updating every single employee row that mentions it. Real schemas fix this by storing each department once, in its own table, and referencing it by ID. This lesson introduces joins, the tool that puts that split data back together, and formally updates Beacon Robotics' schema to the normalized version used for the rest of this course.

Learning Objectives

  • Explain why splitting data into multiple related tables (normalization) avoids duplication and inconsistency.
  • Read Beacon Robotics' updated schema: employees, departments, projects, and project_assignments.
  • Write a basic JOIN with an ON condition and understand what it actually does.

employees

department_id column

JOIN ... ON

Matches department_id = id

departments

id, name columns

Combined Rows

Employee + department name together

Why One Table Becomes Several

Storing department as free text on every employee row means the same department name gets typed out over and over, Engineering on one row, Engineering on another, a hundred more times for a hundred employees. Nothing enforces that every row spells it identically, and changing a department's name later means finding and updating every row that mentions it. The fix: store each department exactly once, in its own departments table, and have employees point to it with an ID. This is called normalization, and this course treats it properly starting here, a full module on database design comes later, this lesson only covers the slice needed to understand joins.

Beacon Robotics' Updated Schema

department_id is an integer that points at a row in departments, the same idea as salary or hire_date, just a column whose value happens to be another table's primary key. manager_id works the same way but points back into employees itself, an employee's manager is just another employee, used in the self-join lesson later in this module. project_assignments exists because one employee can work on several projects and one project has several employees, a many-to-many relationship needs its own linking table, covered properly in the Database Design module later in this course. From this lesson forward, every example uses department_id and departments, not the old department text column.

departments
id (primary key)
name

1  Engineering
2  Research
3  Marketing
4  Sales
5  Legal   -- no employees yet
employees (department_id replaces the old department text column)
id (primary key)
first_name
last_name
department_id (foreign key -> departments.id)
salary
hire_date
manager_id (foreign key -> employees.id, NULL for whoever has no manager)
projects
id (primary key)
name

1  Warehouse Bot v2
2  Night Vision Sensors
3  Fleet Dashboard
project_assignments (links employees to projects, many-to-many)
employee_id (foreign key -> employees.id)
project_id (foreign key -> projects.id)
NOTE

Full CREATE TABLE syntax for building these tables yourself comes in Module 7. For now, just read these as the shape of the data the rest of this module queries against.

A Basic JOIN

JOIN departments adds a second table to the query. ON employees.department_id = departments.id is the join condition, it tells SQL exactly how a row from employees relates to a row from departments: match them when the employee's department_id equals the department's id. Without ON, SQL has no way to know which department row belongs with which employee row.

Plain JOIN here means INNER JOIN, the default and most common type, covered in full in the next lesson. employees.first_name and departments.name use table.column to say exactly which table each column comes from, necessary once a query involves more than one table.

MySQL / MariaDB / PostgreSQL / SQLite / SQL Server / Oracle
SELECT employees.first_name, departments.name AS department_name
FROM employees
JOIN departments ON employees.department_id = departments.id;

The Join Types This Module Covers

Each gets its own lesson next, with real Beacon Robotics examples, including the department with no employees (Legal) and the self-referencing manager_id column.

Join TypeReturns
INNER JOINOnly rows with a match in both tables
LEFT JOINEvery row from the left table, matched data or NULL from the right
RIGHT JOINEvery row from the right table, matched data or NULL from the left
FULL JOINEvery row from both tables, matched where possible, NULL where not
SELF JOINA table joined to itself, for comparing rows within the same table

Try It Yourself

Write a query that returns every employee's last_name alongside their department's name.

Answer
SELECT employees.last_name, departments.name AS department_name
FROM employees
JOIN departments ON employees.department_id = departments.id;

Frequently Asked Questions

Is JOIN the same as INNER JOIN?

Yes, JOIN on its own defaults to INNER JOIN in every database in this course. Many teams write INNER JOIN explicitly anyway, purely for readability, so a reader doesn't have to remember the default.

Can a query join more than two tables?

Yes, a query can chain as many JOIN ... ON clauses as needed, for example employees joined to departments and to project_assignments in the same query. Covered with a real multi-table example later in this module.

Common Beginner Mistakes

Writing JOIN with no ON clause

This produces a cross join, every row from the first table paired with every row from the second, almost never what's actually wanted, and the row count multiplies fast. Always pair JOIN with an ON condition unless a cross join is genuinely the goal.

Mixing up which column belongs to which table

employees.department_id = departments.id is correct; departments.department_id doesn't exist, that table's primary key is just id. Check each table's actual column list rather than guessing a matching name on both sides.

Best Practices

  • Write INNER JOIN explicitly rather than relying on the JOIN default, it reads more clearly.
  • Always include an ON condition, a JOIN without one is a cross join almost by accident.
  • Qualify column names with their table (employees.first_name) once a query touches more than one table.

Interview Questions

What problem does normalizing a department column into a separate departments table actually solve?

It removes duplicate, inconsistent copies of the same text (typos, mismatched capitalization) and means a department's name only ever needs updating in one place, its own row in departments, instead of in every employee row that mentions it.

What happens if you write a JOIN without an ON clause?

It becomes a cross join, every row of the first table paired with every row of the second, with no filtering by relationship. A 100-row employees table joined to a 5-row departments table this way returns 500 rows, almost never the intended result.

Summary

Beacon Robotics' schema is now normalized: departments and projects each hold their own data once, employees references a department through department_id and a manager through manager_id, and project_assignments links employees to projects. A JOIN with an ON condition is how a query pulls matching rows from more than one of these tables back together.

What's Next?

INNER JOIN next, the default join type and the one used above, covered in full with more examples including filtering and multi-table joins.

</>

Ready to Practice SQL?

Write and run real SQL queries online, right in your browser, no install or signup required. Pick a playground based on which database you want to practice on.

SQLite / PostgreSQL

Fast, no server, no ads.

Open DB Fiddle ↗

MySQL / SQL Server

Switch databases with one dropdown.

Open SQL Fiddle ↗

Both open a third-party SQL playground in a new tab. External site terms and privacy policy apply.