INNER, LEFT, RIGHT, and FULL joinsPlannedSelf joins and many-to-many dataPlannedSubqueries and correlated subqueriesPlannedUNION, INTERSECT, and EXCEPTPlanned
Designing reliable schemas
CREATE TABLE and schema designPlannedConstraints and data integrityPlannedNormalization and intentional denormalizationPlannedViews and materialized viewsPlanned
Writing and protecting data
INSERT, UPDATE, and DELETEPlannedUpserts and conflict handlingPlannedTransactions and savepointsPlannedIsolation, locks, and concurrencyPlanned
Performance and administration
Indexes and access pathsPlannedEXPLAIN and query plansPlannedQuery tuning patternsPlannedBackups, restores, and migrationsPlanned
Security and production workflow
Users, roles, and least privilegePlannedPreventing SQL injectionPlannedStored procedures, functions, and triggersPlannedProduction data projectPlanned
Relational foundations · Lesson 04 130 min
Primary Keys, Foreign Keys, and Relationships
Keys turn separate tables into a connected model. A primary key says “this exact row.” A foreign key says “this row depends on that row.” Relationships let you store each fact once, protect references at write time, and rebuild useful results with joins when a question needs data from several tables.
Keys turn separate tables into a connected model
Primary keysGive every row a stable identity that other rows can reference safely.
Foreign keysStore a reference to a parent row and ask the database to protect that reference.
CardinalityDistinguish one-to-one, one-to-many, and many-to-many relationships before writing tables.
Referential integrityReject orphan rows and choose deliberate behavior when a parent row changes or disappears.
Read the relationship before you write SQL
Relational design is easier when you draw the sentence first. “One department has many courses” means the child table, courses, stores a department_id foreign key. The department name stays in departments. A query joins the tables when a report needs both the course title and department label.
The child key can also be unique, so each parent has at most one matching child row.
ONE TO MANY
Many courses per department
The child table stores one foreign key column pointing to the parent table.
MANY TO MANY
Many students per course
A bridge table stores one row per pair, often with its own relationship attributes.
Primary keys identify rows, not display labels
A primary key should be unique, non-null, and stable. A person’s display name, a course title, or a department name may look unique today, but those labels can change. A stable internal identifier lets human-facing labels evolve without breaking every relationship that points at the row.
That does not mean business values are unimportant. A username, SKU, slug, or email can still be protected with UNIQUE. The difference is responsibility: the primary key carries identity; unique business keys protect domain rules and lookup convenience.
SQL BROWSER RUNNER
Keep identity stable while labels change
Update a member display name while the member_id and username continue to identify the same row.
CREATETABLE members (
member_id INTEGERPRIMARYKEY,
username TEXTNOTNULLUNIQUE,
display_name TEXTNOTNULL);INSERTINTO members (member_id, username, display_name)VALUES(1,'ada','Ada Lovelace'),(2,'grace','Grace Hopper');UPDATE members
SET display_name ='Amazing Grace'WHERE member_id =2;SELECT member_id, username, display_name
FROM members
ORDERBY member_id;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
Good identity key
Stable, required, unique
Usually a generated integer, UUID, or another value designed specifically to identify one row.
Risky identity key
Meaning changes over time
Emails, usernames, titles, and names are useful domain values, but they may need to change.
Foreign keys protect relationships at write time
A foreign key column stores a value that must match a candidate key in another table. In SQLite, foreign-key enforcement must be enabled with PRAGMA foreign_keys = ON. In production systems, enforcement behavior and defaults vary, but the design goal is the same: prevent a child row from pointing to nothing.
SQL BROWSER RUNNER
Prevent orphan rows
Create authors and articles, then optionally uncomment the invalid article insert to see the foreign key reject it.
PRAGMA foreign_keys =ON;CREATETABLE authors (
author_id INTEGERPRIMARYKEY,
name TEXTNOTNULL);CREATETABLE articles (
article_id INTEGERPRIMARYKEY,
title TEXTNOTNULL,
author_id INTEGERNOTNULL,FOREIGNKEY(author_id)REFERENCES authors(author_id));INSERTINTO authors (author_id, name)VALUES(1,'Mina'),(2,'Omar');INSERTINTO articles (article_id, title, author_id)VALUES(10,'Readable SQL',1),(11,'Schema Habits',2);-- Uncomment this line to see the relationship rule fail:-- INSERT INTO articles (article_id, title, author_id) VALUES (12, 'Orphan Article', 99);SELECT articles.title, authors.name AS author
FROM articles
JOIN authors ON authors.author_id = articles.author_id
ORDERBY articles.article_id;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
Model one-to-many by placing the key on the many side
In a one-to-many relationship, each child row belongs to one parent row, while a parent can have many children. Put the foreign key on the child table. In the course example, each course belongs to one department, so courses.department_id references departments.department_id.
SQL BROWSER RUNNER
Join a one-to-many relationship
Create departments and courses, then join through department_id to produce a readable course catalog.
PRAGMA foreign_keys =ON;CREATETABLE departments (
department_id INTEGERPRIMARYKEY,
name TEXTNOTNULLUNIQUE);CREATETABLE courses (
course_id INTEGERPRIMARYKEY,
title TEXTNOTNULL,
department_id INTEGERNOTNULL,FOREIGNKEY(department_id)REFERENCES departments(department_id));INSERTINTO departments (department_id, name)VALUES(1,'Programming'),(2,'Design');INSERTINTO courses (course_id, title, department_id)VALUES(101,'SQL Foundations',1),(102,'JavaScript DOM',1),(201,'UX Research',2);SELECT courses.title, departments.name AS department
FROM courses
JOIN departments
ON departments.department_id = courses.department_id
ORDERBY departments.name, courses.title;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
Many-to-many relationships need a bridge table
If many students can take many courses, neither table should store a comma-separated list of the other table’s IDs. That creates unqueryable text, weak validation, and painful updates. A bridge table stores one row per relationship pair. It can also store facts about the relationship itself, such as enrolled_on, status, role, quantity, or sort order.
The bridge table below uses a composite primary key: (student_id, course_id). That means the same student cannot enroll in the same course twice. Each column is also a foreign key, so the pair must reference existing rows on both sides.
SQL BROWSER RUNNER
Build a student-course bridge table
Model many students taking many courses without storing lists inside a cell.
PRAGMA foreign_keys =ON;CREATETABLE students (
student_id INTEGERPRIMARYKEY,
email TEXTNOTNULLUNIQUE);CREATETABLE courses (
course_id INTEGERPRIMARYKEY,
title TEXTNOTNULLUNIQUE);CREATETABLE enrollments (
student_id INTEGERNOTNULL,
course_id INTEGERNOTNULL,
enrolled_on TEXTNOTNULL,PRIMARYKEY(student_id, course_id),FOREIGNKEY(student_id)REFERENCES students(student_id),FOREIGNKEY(course_id)REFERENCES courses(course_id));INSERTINTO students (student_id, email)VALUES(1,'ada@example.test'),(2,'grace@example.test');INSERTINTO courses (course_id, title)VALUES(101,'SQL Foundations'),(102,'Data Modeling');INSERTINTO enrollments (student_id, course_id, enrolled_on)VALUES(1,101,'2026-09-01'),(1,102,'2026-09-02'),(2,101,'2026-09-03');SELECT students.email, courses.title, enrollments.enrolled_on
FROM enrollments
JOIN students ON students.student_id = enrollments.student_id
JOIN courses ON courses.course_id = enrollments.course_id
ORDERBY students.email, courses.title;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
studentsstudent_id PKOne row per learnerenrollmentsstudent_id FKcourse_id FKOne row per student-course paircoursescourse_id PKOne row per course
Use role names when one table references another more than once
A table can reference the same parent table through different roles. A code review has an author and a reviewer, and both are users. Naming the columns author_id and reviewer_id makes the relationship readable. The query then joins users twice using aliases, once for each role.
SQL BROWSER RUNNER
Join the same parent table twice
Use clear foreign-key role names and table aliases to show author and reviewer names in one result.
PRAGMA foreign_keys =ON;CREATETABLE users (
user_id INTEGERPRIMARYKEY,
name TEXTNOTNULL);CREATETABLE code_reviews (
review_id INTEGERPRIMARYKEY,
pull_request_title TEXTNOTNULL,
author_id INTEGERNOTNULL,
reviewer_id INTEGERNOTNULL,FOREIGNKEY(author_id)REFERENCES users(user_id),FOREIGNKEY(reviewer_id)REFERENCES users(user_id),CHECK(author_id <> reviewer_id));INSERTINTO users (user_id, name)VALUES(1,'Ada'),(2,'Grace'),(3,'Linus');INSERTINTO code_reviews
(review_id, pull_request_title, author_id, reviewer_id)VALUES(100,'Add SQL exercises',1,2),(101,'Refine course navigation',3,1);SELECT
code_reviews.pull_request_title,
author.name AS author,
reviewer.name AS reviewer
FROM code_reviews
JOIN users AS author ON author.user_id = code_reviews.author_id
JOIN users AS reviewer ON reviewer.user_id = code_reviews.reviewer_id
ORDERBY code_reviews.review_id;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
Choose delete actions deliberately
What should happen to child rows when a parent row is deleted? SQL engines support referential actions such as reject the delete, set the child foreign key to NULL, set a default value, or cascade the delete to child rows. There is no universally correct option. The right choice depends on whether the child row has meaning without the parent.
In the example, tasks are treated as dependent records of a project, so deleting a project cascades to its tasks. That can be reasonable for disposable child data. It would be dangerous for financial records, audit logs, or purchases where history must remain even if a user or project is archived.
SQL BROWSER RUNNER
Observe ON DELETE CASCADE
Delete one project and watch its dependent tasks disappear while unrelated tasks remain.
PRAGMA foreign_keys =ON;CREATETABLE projects (
project_id INTEGERPRIMARYKEY,
name TEXTNOTNULL);CREATETABLE project_tasks (
task_id INTEGERPRIMARYKEY,
project_id INTEGERNOTNULL,
title TEXTNOTNULL,FOREIGNKEY(project_id)REFERENCES projects(project_id)ONDELETECASCADE);INSERTINTO projects (project_id, name)VALUES(1,'SQL course'),(2,'Template marketplace');INSERTINTO project_tasks (task_id, project_id, title)VALUES(10,1,'Write key lesson'),(11,1,'Add quiz'),(20,2,'Verify downloads');DELETEFROM projects
WHERE project_id =1;SELECT project_tasks.task_id, project_tasks.title, projects.name AS project
FROM project_tasks
JOIN projects ON projects.project_id = project_tasks.project_id
ORDERBY project_tasks.task_id;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
REFERENTIAL ACTION CHOICEWhat should happen when the parent changes?
01
Reject
Use when child records must keep a valid parent and deletion should stop.
Example
Do not delete a plan while active subscriptions reference it.
02
Cascade
Use when child records are truly owned by the parent and have no independent history.
Example
Delete draft checklist items when deleting the draft checklist.
03
Set NULL
Use only when the child relationship is optional and absence is a valid state.
Example
Keep a ticket after its optional assignee is removed.
04
Archive instead
Use application workflow when deleting would erase important history.
Example
Archive customers, invoices, and purchases instead of deleting them.
Common relationship mistakes
department_name copied into courses
Duplicated parent facts
Copying descriptive parent data into every child row creates update drift. Store the parent once and join when needed.
course_ids = "1,2,3"
Lists inside one cell
A comma-separated relationship cannot be protected by foreign keys and becomes hard to filter, count, and update.
user_id means many roles
Ambiguous foreign key role
Name the role: author_id, reviewer_id, owner_id, assignee_id, or manager_id.
delete first, think later
Unsafe referential actions
Cascade and restrict rules are business decisions. Choose them before production data depends on them.
Independent lab: model course enrollments
Extend the working enrollment model. Add a third student, add a third course, enroll the new student in two courses, mark one existing enrollment as completed, and return a report with student name, course title, status, and enrollment date. Then try to insert an enrollment for a nonexistent student and observe the foreign-key failure.
SQL BROWSER RUNNER
Build a protected enrollment model
Use two entity tables and one bridge table, then prove the model prevents duplicates and orphan relationships.
Edit the query, predict the rows it will return, then run it.
Lesson review
Primary keys give rows stable identity. Unique constraints protect business values that must not repeat. Foreign keys store and enforce references between rows. One-to-many relationships place the foreign key on the many side. Many-to-many relationships use a bridge table with foreign keys to both sides. Clear role names make repeated references understandable. Referential actions decide what happens when parent rows are updated or deleted, so they should be treated as part of the data model, not an afterthought.
I can explain why a primary key should be stable, unique, and non-null.
I can choose between a primary key and a unique business key.
I can place a foreign key on the correct side of a one-to-many relationship.
I can model a many-to-many relationship with a bridge table instead of a list inside one cell.
I can name foreign-key roles clearly when one table references the same parent more than once.
I can describe the trade-off between restrict, cascade, set null, and archive workflows.
KNOWLEDGE CHECK
Check your key and relationship model
Answer all eight questions, then use the explanations to repair any weak spot before you move into SELECT filtering.