CMPUT-291 · Lesson 7 of 12 · 31 min
From ER diagrams to tables
An ER diagram describes what a database should record; a relational schema says how, as tables, columns, keys and foreign keys. The translation from one to the other is largely mechanical, following a handful of rules, but each rule involves choices, especially for one-to-one relationships, weak entities and specialization hierarchies. This lesson applies the rules to the Riverbend library diagram from lesson 6, producing a complete schema that runs in SQLite, and explains the alternatives and their trade-offs. Lesson 8 then checks the resulting tables for redundancy using functional dependencies.
Strong entities and their attributes
Each strong entity set becomes a table. Its simple attributes become columns, its key becomes the primary key, and NOT NULL marks attributes that every entity must have. Composite attributes are flattened into one column per component: the branch address becomes street, city and postal_code. Derived attributes, such as the number of days a loan is overdue, are normally not stored but computed in queries or views, so they never go stale. If a natural key is long, unstable or might be missing, a surrogate integer key is introduced and the natural key kept with a UNIQUE constraint, as with branch, whose name is unique but whose identifier is branch_id.
Multivalued attributes cannot be stored in one column without breaking atomicity, so each becomes a separate table containing the owner's key and the value, with the pair as the primary key. A member's phone numbers become the table member_phone(card_no, phone). This rule is the root of first normal form and of good design generally: a list of values hidden in a single text column, such as '780-555-0101; 780-555-0199', cannot be indexed, constrained or queried reliably.
Relationships
How a relationship becomes tables depends on its cardinality. The general rule is to put the foreign key on the side that relates to at most one entity, because a column holds one value.
| ER construct | Relational mapping | Library example |
|---|---|---|
| 1:N relationship | Foreign key in the table on the N side, referencing the 1 side | copy.branch_id references branch |
| Total participation on the N side | That foreign key is NOT NULL | Every copy has a branch: branch_id NOT NULL |
| Partial participation on the N side | That foreign key may be NULL | A staff member's supervisor may be NULL |
| 1:1 relationship | Foreign key on either side, declared UNIQUE; prefer the side with total participation | A staff desk assignment |
| M:N relationship | A new junction table with both keys as foreign keys; their combination is the primary key | writes(author_id, isbn) |
| Relationship attributes | Columns of the table that holds the relationship | hold.placed_on |
| n-ary relationship | A table with a foreign key to each participant; key chosen from the cardinalities | teaches(instr, course, term) |
| Recursive relationship | Foreign key to the same table, named for the role | staff_member.supervisor_card |
A one-to-many relationship needs no table of its own: the foreign key on the many side records it fully. A many-to-many relationship always needs a junction table (also called a link, bridge or associative table), because neither participant can hold a list of partners. For a one-to-one relationship, either side can hold the foreign key with a UNIQUE constraint; putting it on the side with total participation avoids NULLs. If both sides are total and the entities share a lifecycle, merging them into one table is often simplest.
Weak entities and associative entities
A weak entity set becomes a table whose primary key combines the owner's key with the weak entity's partial key; the owner's key is also a foreign key, normally ON DELETE CASCADE, because a weak entity cannot exist without its owner. Copy therefore gets the key (isbn, copy_no). A consequence is that any table referring to a copy needs both columns as a composite foreign key, as loan does. If that becomes awkward, a designer may give copies a surrogate barcode key instead, at the cost of declaring (isbn, copy_no) UNIQUE separately.
Lesson 6 argued that loans, which happen repeatedly between the same member and copy and carry their own dates, are best modelled as an associative entity. In tables, that means loan gets its own surrogate key, loan_id, rather than a primary key of (card_no, isbn, copy_no), which would forbid borrowing the same copy twice. Including date_out in a composite key is an alternative. Holds, by contrast, are kept as a plain many-to-many relationship keyed by (card_no, isbn), because the library allows only one hold per member per title at a time.
The complete library schema
CREATE TABLE branch (
branch_id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
street TEXT NOT NULL,
city TEXT NOT NULL,
postal_code TEXT NOT NULL
);
CREATE TABLE title (
isbn TEXT PRIMARY KEY,
title TEXT NOT NULL,
pub_year INTEGER CHECK (pub_year BETWEEN 1450 AND 2100)
);
CREATE TABLE author (
author_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE writes ( -- M:N Author-Title
author_id INTEGER REFERENCES author(author_id),
isbn TEXT REFERENCES title(isbn),
PRIMARY KEY (author_id, isbn)
);
CREATE TABLE copy ( -- weak entity owned by title
isbn TEXT NOT NULL REFERENCES title(isbn) ON DELETE CASCADE,
copy_no INTEGER NOT NULL,
condition TEXT NOT NULL DEFAULT 'good' CHECK (condition IN ('new', 'good', 'worn', 'damaged')),
branch_id INTEGER NOT NULL REFERENCES branch(branch_id), -- N:1, total participation
PRIMARY KEY (isbn, copy_no)
);
CREATE TABLE member (
card_no INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);
CREATE TABLE member_phone ( -- multivalued attribute
card_no INTEGER REFERENCES member(card_no) ON DELETE CASCADE,
phone TEXT,
PRIMARY KEY (card_no, phone)
);
CREATE TABLE student_member ( -- ISA subclass
card_no INTEGER PRIMARY KEY REFERENCES member(card_no) ON DELETE CASCADE,
school TEXT NOT NULL
);
CREATE TABLE staff_member ( -- ISA subclass
card_no INTEGER PRIMARY KEY REFERENCES member(card_no) ON DELETE CASCADE,
hire_date TEXT NOT NULL,
branch_id INTEGER NOT NULL REFERENCES branch(branch_id),
supervisor_card INTEGER REFERENCES staff_member(card_no) -- recursive, optional
);
CREATE TABLE loan ( -- associative entity
loan_id INTEGER PRIMARY KEY,
card_no INTEGER NOT NULL REFERENCES member(card_no),
isbn TEXT NOT NULL,
copy_no INTEGER NOT NULL,
date_out TEXT NOT NULL,
due_date TEXT NOT NULL,
returned TEXT,
FOREIGN KEY (isbn, copy_no) REFERENCES copy(isbn, copy_no),
CHECK (due_date > date_out),
CHECK (returned IS NULL OR returned >= date_out)
);
CREATE TABLE hold ( -- M:N Member-Title with attributes
card_no INTEGER REFERENCES member(card_no) ON DELETE CASCADE,
isbn TEXT REFERENCES title(isbn),
placed_on TEXT NOT NULL,
PRIMARY KEY (card_no, isbn)
);
SELECT COUNT(*) AS tables FROM sqlite_schema WHERE type = 'table';tables ------ 11
Each design decision from lesson 6 is visible: the weak Copy table with its composite key and cascading owner reference; NOT NULL on copy.branch_id for total participation; junction tables writes and hold; member_phone for the multivalued attribute; a separate table for each subclass; a recursive supervisor reference that may be NULL; and CHECK constraints for rules that the ER diagram recorded in notes, such as due dates following the date out. Storing dates as ISO 8601 text (YYYY-MM-DD) in SQLite makes string comparison agree with date order, which is what the CHECK constraints rely on; other systems have a native DATE type.
-- Who has which copy out right now, and where it lives
SELECT m.name, t.title, l.copy_no, b.name AS branch, l.due_date
FROM loan l
JOIN member m ON m.card_no = l.card_no
JOIN copy c ON c.isbn = l.isbn AND c.copy_no = l.copy_no
JOIN title t ON t.isbn = c.isbn
JOIN branch b ON b.branch_id = c.branch_id
WHERE l.returned IS NULL
ORDER BY l.due_date;
-- Titles with their authors, combined into one string
SELECT t.title, group_concat(a.name, ' and ') AS authors
FROM title t JOIN writes w ON w.isbn = t.isbn JOIN author a ON a.author_id = w.author_id
GROUP BY t.isbn ORDER BY t.title;
-- The same member borrowing the same copy twice is allowed by the loan_id key
SELECT card_no, isbn, copy_no, COUNT(*) AS times_borrowed FROM loan GROUP BY card_no, isbn, copy_no HAVING COUNT(*) > 1;name title copy_no branch due_date ---- ------------------------- ------- --------- ---------- Noor The Northern Lights Atlas 1 Central 2026-10-22 Owen Small Engines 1 Northside 2026-10-26 title authors ------------------------- ------------------------ Bread and Salt S. Patel Small Engines S. Patel The Northern Lights Atlas R. Okoye and M. Gauthier card_no isbn copy_no times_borrowed ------- ----------------- ------- -------------- 5001 978-0-00-000001-1 1 2
Specialization hierarchies
An ISA hierarchy can be mapped in three main ways, and the choice depends on the disjointness and completeness constraints and on how the data is used.
| Approach | Tables | Strengths | Weaknesses |
|---|---|---|---|
| Table per class (used above) | member, plus student_member and staff_member keyed by card_no | No NULL columns; supports overlapping and partial specializations; superclass queries are simple | Subclass queries need a join |
| Table per concrete subclass | student_member and staff_member each with all member columns; no member table | No joins for subclass queries | Only works for total, disjoint specializations; superclass queries need UNION; the key is not enforced unique across tables |
| Single table | One member table with all columns plus a type column | No joins at all; simple | Many NULLs; subclass-specific NOT NULL rules need CHECK constraints; overlapping needs flags rather than one type |
The library's specialization is overlapping (a staff member can also be a student) and partial (most members are neither), which rules out the table-per-concrete-subclass approach, since ordinary members would have nowhere to live. Table per class handles both properties naturally: a member who is both student and staff has a row in each subclass table. Object-relational mapping tools, discussed in lesson 11, offer all three strategies under names like joined, table-per-class and single-table inheritance.
Worked problem
Mapping a hospital fragment
A hospital ER diagram has Patient (health card number, name, birth date), Physician (physician number, name, specialty), a 1:N relationship Patient has primary Physician (every patient must have one), and an M:N relationship Physician treats Patient with attribute date of visit, where the same physician can treat the same patient many times. Give the tables.
Show solution
- Strong entities: patient(health_card PK, name, birth_date) and physician(phys_id PK, name, specialty).
- The 1:N "primary physician" puts a foreign key on the patient side: patient.primary_phys_id REFERENCES physician, NOT NULL because participation is total.
- The M:N "treats" with repeated visits needs an associative table with its own identity: visit(visit_id PK, phys_id FK NOT NULL, health_card FK NOT NULL, visit_date NOT NULL).
- Alternatively key visit by (phys_id, health_card, visit_date) if at most one visit per pair per day is a real rule.
Answer: patient(health_card, name, birth_date, primary_phys_id), physician(phys_id, name, specialty), visit(visit_id, phys_id, health_card, visit_date), with foreign keys as described.
Worked problem
Where does the foreign key go in a 1:1?
Each branch has exactly one manager, who is a staff member; a staff member manages at most one branch, and most staff manage none. Where should the foreign key go?
Show solution
- Option A: staff_member.manages_branch REFERENCES branch, UNIQUE. Most staff manage nothing, so this column is NULL for almost every row.
- Option B: branch.manager_card REFERENCES staff_member, UNIQUE, NOT NULL. Every branch has a manager, so the column is always filled.
- Branch's participation is total and staff's is partial, so option B avoids NULLs and enforces "exactly one manager" with NOT NULL.
Answer: Put manager_card in branch, declared NOT NULL and UNIQUE: the side with total participation holds the foreign key.
Checking the mapping
After mapping, several checks catch common errors. Every foreign key should reference a primary or unique key. Every M:N relationship should have become a junction table, and every multivalued attribute a separate table. NOT NULL should match total participation. Composite keys of weak entities should include the owner's key. Constraints that the diagram could not show should appear as CHECK constraints, UNIQUE constraints, or documented application rules. Finally, the tables should be checked for redundancy, a step the ER model does not guarantee; functional dependencies and normal forms, the subject of lesson 8, make that check precise.
import sqlite3, textwrap
con = sqlite3.connect(":memory:")
con.executescript(textwrap.dedent("""
CREATE TABLE branch (branch_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE copy (isbn TEXT NOT NULL, copy_no INTEGER NOT NULL,
branch_id INTEGER NOT NULL REFERENCES branch(branch_id),
PRIMARY KEY (isbn, copy_no));
CREATE TABLE loan (loan_id INTEGER PRIMARY KEY, isbn TEXT, copy_no INTEGER,
FOREIGN KEY (isbn, copy_no) REFERENCES copy(isbn, copy_no));
"""))
# SQLite's schema tables describe the mapping: list each table's foreign keys
for (table,) in con.execute("SELECT name FROM sqlite_schema WHERE type='table' ORDER BY name"):
fks = con.execute(f"PRAGMA foreign_key_list({table})").fetchall()
for fk in fks:
print(f"{table}.{fk[3]} -> {fk[2]}.{fk[4]}")
notnull = [c[1] for c in con.execute(f"PRAGMA table_info({table})") if c[3]]
print(f" {table}: NOT NULL columns {notnull}")branch: NOT NULL columns ['name'] copy.branch_id -> branch.branch_id copy: NOT NULL columns ['isbn', 'copy_no', 'branch_id'] loan.isbn -> copy.isbn loan.copy_no -> copy.copy_no loan: NOT NULL columns []
The catalogue query shows the composite foreign key of loan as two lines, one per column, and the NOT NULL columns of each table. Real projects automate such audits, and many teams generate an ER diagram back from the live schema with tools to compare it with the intended design.
Common mistakes
MistakeStoring a list of values, such as several phone numbers, in one text column.
InsteadMap multivalued attributes to a separate table of (owner key, value).
MistakeTrying to represent an M:N relationship with a foreign key in one of the tables.
InsteadA column holds one value. M:N relationships need a junction table with both foreign keys.
MistakePutting the foreign key of a 1:N relationship on the 1 side.
InsteadThe foreign key goes on the N side, which refers to exactly one entity of the other side.
MistakeForgetting the owner's key in a weak entity's primary key.
InsteadA weak entity is identified by owner key plus partial key, and the owner key is also a foreign key.
MistakeUsing table-per-concrete-subclass for a partial or overlapping specialization.
InsteadEntities that belong to no subclass, or to several, have no correct home; use table per class or single table.
Summary and key terms
- Each strong entity set becomes a table; composite attributes are flattened, derived attributes are computed rather than stored, and multivalued attributes become separate tables.
- A 1:N relationship becomes a foreign key on the N side; total participation makes it NOT NULL.
- A 1:1 relationship becomes a UNIQUE foreign key on one side, preferably the side with total participation.
- An M:N relationship becomes a junction table whose primary key combines the participants' keys, plus any relationship attributes.
- A weak entity's table has a primary key of owner key plus partial key, with a cascading foreign key to the owner.
- Repeated events are best mapped with their own surrogate key; recursive relationships become self-referencing foreign keys.
- Specializations map to table per class, table per concrete subclass, or a single table, chosen by the disjointness and completeness constraints.
- Junction table
- A table that represents an M:N relationship, holding foreign keys to both participants.
- Composite foreign key
- A foreign key made of several columns, referencing a composite key.
- Surrogate key
- A system-generated identifier used instead of a natural key.
- Natural key
- A key drawn from real-world data, such as an ISBN.
- Table per class
- Mapping each class of a hierarchy to its own table, with subclasses keyed by the superclass key.
- Single-table inheritance
- Mapping a whole hierarchy to one table with a type column.
- Total participation
- A participation constraint mapped to a NOT NULL foreign key.
- Self-referencing foreign key
- A foreign key that refers to the same table, mapping a recursive relationship.
- ISO 8601 date
- The YYYY-MM-DD date format, whose text order matches date order.
- Catalogue
- The DBMS's own tables describing the schema, which can be queried.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Map Department (dept_id, name) and Employee (emp_id, name) with "each employee works in exactly one department".
Show answer
department(dept_id PK, name), employee(emp_id PK, name, dept_id NOT NULL REFERENCES department).
Map Student and Club with the M:N relationship "member of" having attribute joined_on.
Show answer
membership(student_id FK, club_id FK, joined_on, PRIMARY KEY (student_id, club_id)).
A building has rooms numbered within the building. Give the room table's key.
Show answer
(building_id, room_no), with building_id a foreign key to building, usually ON DELETE CASCADE.
Why does loan use loan_id rather than (card_no, isbn, copy_no) as its key?
Show answer
Because the same member can borrow the same copy more than once, which a key of those three columns would forbid.
Which hierarchy mapping avoids NULLs and handles overlapping subclasses?
Show answer
Table per class (a superclass table plus one table per subclass keyed by the superclass key).
How do you map a multivalued attribute "skills" of Employee?
Show answer
employee_skill(emp_id FK, skill, PRIMARY KEY (emp_id, skill)).
In a 1:1 between Country and Capital, where every country has exactly one capital, where should the foreign key go?
Show answer
In country, as capital_id NOT NULL UNIQUE, the side with total participation.
How is a recursive relationship such as "employee reports to employee" mapped?
Show answer
A foreign key column such as manager_id in the employee table referencing employee, nullable for the top manager.
Why are dates stored as YYYY-MM-DD text in SQLite?
Show answer
Because string comparison then matches chronological order, so ORDER BY and CHECK comparisons work.
Give one check to perform after mapping an ER diagram.
Show answer
For example: every M:N became a junction table, or every total participation became NOT NULL.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.