SQL Joins

April 3, 2026
#sql #databases #joins #intermediate

In the SQL Basics tutorial, we worked with a single table at a time. But real databases almost always have multiple related tables — customers and orders, students and courses, products and categories. Joins are how you combine rows from two or more tables based on a related column.

Understanding joins is one of the most important SQL skills you can have. They come up constantly in application development, data analysis, and technical interviews.

Setting Up the Example Data

We’ll use a small database with three tables: students, courses, and enrollments. If you followed the SQL Basics tutorial, you’ve already seen these. Let’s start fresh so everything is consistent:

CREATE TABLE students (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT UNIQUE
);

CREATE TABLE courses (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    instructor TEXT
);

CREATE TABLE enrollments (
    student_id INTEGER,
    course_id INTEGER,
    grade TEXT,
    FOREIGN KEY (student_id) REFERENCES students(id),
    FOREIGN KEY (course_id) REFERENCES courses(id)
);

INSERT INTO students (name, email) VALUES ('Alice', 'alice@example.com');
INSERT INTO students (name, email) VALUES ('Bob', 'bob@example.com');
INSERT INTO students (name, email) VALUES ('Charlie', 'charlie@example.com');
INSERT INTO students (name, email) VALUES ('Diana', 'diana@example.com');

INSERT INTO courses (title, instructor) VALUES ('Intro to SQL', 'Dr. Smith');
INSERT INTO courses (title, instructor) VALUES ('Web Development', 'Prof. Jones');
INSERT INTO courses (title, instructor) VALUES ('Data Science', 'Dr. Lee');

INSERT INTO enrollments (student_id, course_id, grade) VALUES (1, 1, 'A');
INSERT INTO enrollments (student_id, course_id, grade) VALUES (1, 2, 'B');
INSERT INTO enrollments (student_id, course_id, grade) VALUES (2, 1, 'A');
INSERT INTO enrollments (student_id, course_id, grade) VALUES (3, 3, 'C');

Notice that Diana (id 4) isn’t enrolled in any course, and Bob (id 2) is only in one course. These gaps will help illustrate the differences between join types.

INNER JOIN

An INNER JOIN returns only the rows where there’s a match in both tables. If a row in one table has no corresponding row in the other, it’s excluded from the results.

SELECT students.name, courses.title, enrollments.grade
FROM enrollments
INNER JOIN students ON enrollments.student_id = students.id
INNER JOIN courses ON enrollments.course_id = courses.id;
name title grade
Alice Intro to SQL A
Alice Web Development B
Bob Intro to SQL A
Charlie Data Science C

Diana doesn’t appear because she has no enrollment records. The Data Science course shows up because Charlie is enrolled in it. This is the most common type of join — when you only want rows that have matching data on both sides.

LEFT JOIN

A LEFT JOIN returns all rows from the left table, plus matching rows from the right table. If there’s no match, the right side columns are filled with NULL.

SELECT students.name, courses.title, enrollments.grade
FROM students
LEFT JOIN enrollments ON students.id = enrollments.student_id
LEFT JOIN courses ON enrollments.course_id = courses.id;
name title grade
Alice Intro to SQL A
Alice Web Development B
Bob Intro to SQL A
Charlie Data Science C
Diana NULL NULL

Now Diana appears — with NULL for the course and grade columns since she’s not enrolled in anything. This is useful when you want to include records even if they don’t have related data. For example, “show me all students and their courses, including students who haven’t enrolled yet.”

RIGHT JOIN

A RIGHT JOIN is the mirror of a LEFT JOIN — it returns all rows from the right table, plus matching rows from the left table.

SELECT students.name, courses.title
FROM enrollments
RIGHT JOIN courses ON enrollments.course_id = courses.id
LEFT JOIN students ON enrollments.student_id = students.id;
name title
Alice Intro to SQL
Bob Intro to SQL
Alice Web Development
Charlie Data Science

In practice, most developers prefer LEFT JOIN and just rearrange the table order rather than using RIGHT JOIN. The result is the same, and it’s easier to read consistently.

FULL OUTER JOIN

A FULL OUTER JOIN returns all rows from both tables. Where there’s a match, you get the combined data. Where there isn’t, you get NULL on the side that’s missing.

-- PostgreSQL / SQL Server syntax
SELECT students.name, courses.title
FROM students
FULL OUTER JOIN enrollments ON students.id = enrollments.student_id
FULL OUTER JOIN courses ON enrollments.course_id = courses.id;
name title
Alice Intro to SQL
Alice Web Development
Bob Intro to SQL
Charlie Data Science
Diana NULL

This is the most inclusive join — nothing gets dropped. It’s less common in everyday queries but useful when you need a complete picture of both sides.

CROSS JOIN

A CROSS JOIN produces the Cartesian product of two tables — every row from the first table paired with every row from the second. No ON condition is needed.

SELECT students.name, courses.title
FROM students
CROSS JOIN courses;

This returns 4 students × 3 courses = 12 rows. Every possible student-course combination.

Cross joins are rarely used in application code, but they’re handy for generating test data or creating lookup combinations.

Table Aliases

When queries involve multiple tables, column names can get verbose. Aliases let you give tables short names:

SELECT s.name, c.title, e.grade
FROM enrollments e
JOIN students s ON e.student_id = s.id
JOIN courses c ON e.course_id = c.id;

Same result as our first INNER JOIN query, but much easier to read. You’ll see aliases used in virtually every multi-table query.

Self Joins

A self join is when a table is joined to itself. This is useful for hierarchical or comparative data. Imagine an employees table where each employee has a manager_id that references another employee:

CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    manager_id INTEGER,
    FOREIGN KEY (manager_id) REFERENCES employees(id)
);

INSERT INTO employees (name, manager_id) VALUES ('Sarah', NULL);
INSERT INTO employees (name, manager_id) VALUES ('Tom', 1);
INSERT INTO employees (name, manager_id) VALUES ('Lisa', 1);
INSERT INTO employees (name, manager_id) VALUES ('Mike', 2);

SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
employee manager
Sarah NULL
Tom Sarah
Lisa Sarah
Mike Tom

Aliases are essential here — without them, the database wouldn’t know which “copy” of the table you’re referring to.

Joining with Conditions

You can add WHERE clauses to filter joined results, just like with single-table queries:

SELECT s.name, c.title, e.grade
FROM enrollments e
JOIN students s ON e.student_id = s.id
JOIN courses c ON e.course_id = c.id
WHERE e.grade = 'A';
name title grade
Alice Intro to SQL A
Bob Intro to SQL A

You can also use aggregate functions with joins. For example, counting how many courses each student is enrolled in:

SELECT s.name, COUNT(e.course_id) AS course_count
FROM students s
LEFT JOIN enrollments e ON s.id = e.student_id
GROUP BY s.id, s.name
ORDER BY course_count DESC;
name course_count
Alice 2
Bob 1
Charlie 1
Diana 0

We use LEFT JOIN here so Diana (with zero enrollments) still appears in the results.

Quick Reference

Join Type Returns
INNER JOIN Only matching rows from both tables
LEFT JOIN All rows from the left table, matches from the right
RIGHT JOIN All rows from the right table, matches from the left
FULL OUTER JOIN All rows from both tables
CROSS JOIN Every combination of rows (Cartesian product)
Self Join A table joined to itself

What’s Next

Joins are the foundation for working with relational data. Once you’re comfortable with them, you’ll want to explore aggregate functions like COUNT, SUM, and AVG with GROUP BY, and eventually subqueries and window functions for more advanced analysis.

Thanks for visiting
We are actively updating content to this site. Thanks for visiting! Please bookmark this page and visit again soon.
Sponsor