Skip to content

SQLite - JOINS

The JOIN clause is a fundamental SQL concept used to combine rows from two or more tables based on a related column between them. A JOIN allows you to retrieve a consolidated dataset from normalized tables, which is a cornerstone of relational database design.

Modern SQL defines several types of joins, all of which are supported in recent versions of SQLite:

  • INNER JOIN: Returns records that have matching values in both tables.
  • LEFT OUTER JOIN (or LEFT JOIN): Returns all records from the left table, and the matched records from the right table. The result is NULL from the right side if there is no match.
  • CROSS JOIN: Returns the Cartesian product of the two tables, i.e., every row from the first table combined with every row from the second table.
  • [New] RIGHT OUTER JOIN & FULL OUTER JOIN: Supported in SQLite 3.39.0+, these offer more flexible ways to combine datasets, similar to other database systems like PostgreSQL or SQL Server.

For our examples, let’s create two tables: employees and departments. This common scenario helps illustrate how JOINs work in a real-world context.

-- The 'employees' table stores information about employees.
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL,
salary REAL
);
-- The 'departments' table links employees to their department.
CREATE TABLE departments (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
employee_id INTEGER,
FOREIGN KEY (employee_id) REFERENCES employees(id)
);
-- Populate the tables with sample data.
INSERT INTO employees (id, name, age, salary)
VALUES
(1, 'Paul', 32, 70000.00),
(2, 'Allen', 25, 55000.00),
(3, 'Teddy', 23, 50000.00),
(4, 'Mark', 25, 90000.00),
(7, 'James', 24, 45000.00);
INSERT INTO departments (id, name, employee_id)
VALUES
(1, 'IT', 1),
(2, 'Engineering', 2),
(3, 'Finance', 7),
(4, 'Marketing', NULL); -- A department with no assigned employee

INNER JOIN is the most common join. It selects records that have matching values in both tables. Think of it as finding the intersection of two sets.

Let’s find all employees who are assigned to a department.

-- Best Practice: Use table aliases (e.g., 'e' for employees) for readability.
SELECT
e.name AS employee_name,
d.name AS department_name
FROM
employees AS e
INNER JOIN
departments AS d ON e.id = d.employee_id;

This query produces:

employee_name department_name
------------- ---------------
Paul IT
Allen Engineering
James Finance

LEFT OUTER JOIN: Keeping All from the Left

Section titled “LEFT OUTER JOIN: Keeping All from the Left”

LEFT JOIN returns all records from the left table (employees), and the matched records from the right table (departments). If there’s no match, the columns from the right table will be NULL. This is useful for finding employees who are not assigned to a department.

-- Let's find all employees and their department, including those without one.
SELECT
e.name AS employee_name,
d.name AS department_name
FROM
employees AS e
LEFT JOIN
departments AS d ON e.id = d.employee_id;

The result now includes employees Teddy and Mark, who have no department assigned:

employee_name department_name
------------- ---------------
Paul IT
Allen Engineering
Teddy (NULL)
Mark (NULL)
James Finance

As of SQLite 3.39.0, RIGHT JOIN and FULL OUTER JOIN are fully supported, aligning SQLite with other major SQL databases. This was a significant limitation in older versions.

RIGHT JOIN: The opposite of LEFT JOIN. It returns all records from the right table (departments) and matched records from the left. This can show us which departments have no employees.

-- Requires SQLite 3.39.0+
SELECT
e.name AS employee_name,
d.name AS department_name
FROM
employees AS e
RIGHT JOIN
departments AS d ON e.id = d.employee_id;

The result includes the ‘Marketing’ department, which has no assigned employee:

employee_name department_name
------------- ---------------
Paul IT
Allen Engineering
James Finance
(NULL) Marketing

FULL OUTER JOIN: Combines the results of both LEFT and RIGHT joins. It returns all records when there is a match in either the left or right table. It’s useful for seeing all data from both tables at once.

-- Requires SQLite 3.39.0+
SELECT
e.name AS employee_name,
d.name AS department_name
FROM
employees AS e
FULL OUTER JOIN
departments AS d ON e.id = d.employee_id;

This result includes unassigned employees and empty departments:

employee_name department_name
------------- ---------------
Paul IT
Allen Engineering
Teddy (NULL)
Mark (NULL)
James Finance
(NULL) Marketing

CROSS JOIN creates a combination of every row from the first table with every row from the second. It’s rarely used in practice and can create massive result sets. Use with caution!

-- This will match every employee with every department, regardless of the actual link.
SELECT
e.name AS employee_name,
d.name AS department_name
FROM
employees AS e
CROSS JOIN
departments AS d;

This produces 5 employees * 4 departments = 20 rows.

  • Index Your Join Keys: For faster lookups, always create an index on the columns used in your JOIN conditions (e.g., departments.employee_id). SQLite automatically indexes PRIMARY KEY and UNIQUE columns.
  • Use Table Aliases: They make your queries shorter and more readable, especially in complex joins.
  • Choose the Right Join: Understand your data and question. Do you need only matching data (INNER JOIN) or all data from a primary table (LEFT JOIN)?
  • Filter Early: Use a WHERE clause to filter data as early as possible to reduce the size of the dataset being joined.