skip to content

A table course_offering(course_id, textbook, instructor) stores one row for every combination of a course's textbooks and its instructors, so a course with 3 textbooks and 2 instructors occupies 6 rows. What problems does that shape create, and how would you restructure it?

level: juniorimportance: should knowfreq 35%

answer

  1. one row, one fact
  2. m x n rows for m + n facts
  3. independent lists, separate tables
  4. join on course_id rebuilds it
  5. pairing meaningful means keep the table

basics

~20 s

Textbooks and instructors are unrelated facts about a course, so the table is forced to store their cross product. Row counts multiply, adding one textbook costs one insert per instructor, and partial writes imply pairings that are not real. Split into two tables keyed by course_id.

solid answer

~50 s

The table mixes two facts that have nothing to do with each other: which textbooks a course uses, and who teaches it. Because one flat row must carry both, the only truthful representation is the full cross product, m textbooks times n instructors. That is redundancy with teeth: adding a textbook needs n inserts, removing an instructor needs m deletes, and any partial write leaves the table asserting a textbook/instructor pairing that is not a fact. Row count grows multiplicatively instead of additively. The fix is to decompose into course_textbook(course_id, textbook) and course_instructor(course_id, instructor). Each row then states one fact, writes are single-row, and joining on course_id reproduces the cross product whenever anyone needs it. The one case where the original shape is right is when the pairing itself carries meaning, for example this instructor uses that textbook, because then the row is a genuine fact rather than an artefact of layout.

code

sql · 11 lines
sql
CREATE TABLE course_textbook (
  course_id  varchar(16) NOT NULL,
  textbook   varchar(200) NOT NULL,
  PRIMARY KEY (course_id, textbook)
);

CREATE TABLE course_instructor (
  course_id   varchar(16) NOT NULL,
  instructor  varchar(200) NOT NULL,
  PRIMARY KEY (course_id, instructor)
);

go deeper

for a junior

Name the duplication, show that adding one textbook costs several rows, and propose the two-table split with compound keys.

for a middle

Add the vocabulary: this is a multivalued dependency and the split is the 4NF decomposition, and note that the join must rebuild the original exactly.

for a senior

Lead with the test for independence, then discuss detection in an existing system, aggregate skew, and the migration to two tables.

for a principal

Frame it as a modelling discipline question: one relationship per table by default, and explain when a genuine ternary relationship justifies keeping three columns together.

## Every row should be one fact In a well-formed table each row asserts a single statement about the world. Read one row of course_offering: (CS101, Algorithms, Dr Chen). What does it mean? Not that Dr Chen uses that book, only that CS101 has that textbook and CS101 has that instructor. The row is the accidental pairing of two independent facts, and the table has no way to record one without the other. ## Why the cross product is forced Suppose CS101 has textbooks A, B, C and instructors X, Y. If you stored only three rows, (A,X), (B,Y), (C,X), a reader could reasonably conclude that book B is tied to instructor Y. To avoid asserting a pairing that does not exist, you must store all 6 combinations, or start putting NULLs in half the columns. Both are bad: the first is redundant, the second breaks the meaning of the key. This is exactly the situation Fourth Normal Form describes, and the formal name for what holds here is a multivalued dependency, course_id determines a set of textbooks independently of the instructors. ## The concrete costs - Write amplification. Adding one textbook is not one insert, it is one insert per instructor. Adding an instructor is one insert per textbook. A course with 10 books and 5 staff is 50 rows for 15 facts. - Partial-write corruption. If the loop that inserts 5 rows fails after 3, the table now claims a specific book/instructor pairing that nobody intended. There is no constraint that can catch this, because the table cannot tell a real pairing from an accidental one. - Deletion anomalies. Removing the last instructor removes every trace of which textbooks the course uses. - Query confusion. SELECT DISTINCT is required everywhere. Any aggregate over the table double counts, for instance summing textbook prices per course gives price times instructor count. ## The restructure Two tables, each with a compound primary key: course_textbook(course_id, textbook) and course_instructor(course_id, instructor). Each is a clean many-to-many junction between a course and one other thing. Every row is one fact, every insert and delete is one row, and a natural join on course_id rebuilds the original table exactly, no rows lost and no rows invented. That exact reversibility is what makes the split safe. ## When the original table is correct If the business actually pairs the two columns, say each instructor picks their own textbook for the course, then (CS101, Algorithms, Dr Chen) is a real three-way fact and there is no cross product to eliminate. The test is empirical: ask whether choosing a textbook constrains which instructor can appear with it. If not, the columns are independent and belong apart. This is why the decision needs domain knowledge; you cannot infer it from a sample of data alone, since a small dataset can look like a cross product by coincidence. ## How to spot it elsewhere The generic smell is a table whose primary key has three or more columns where two of them vary independently. Row counts that jump by a factor when one value is added, aggregates inflated by a constant multiple, and queries that always need SELECT DISTINCT are the practical signals.

  • How would you rebuild the original table from the two new ones?
    Join them on course_id, which yields every textbook paired with every instructor for that course. The result is exactly the original set of rows, no more and no less, which is what makes the decomposition safe. If a query truly needs that cross product it can ask for it, rather than the schema storing it permanently.
  • What if a course has textbooks but no instructor assigned yet?
    In the split schema that is natural: rows exist in course_textbook and none in course_instructor. In the single-table version it is impossible without a NULL instructor, which pollutes a key column and makes counting instructors unreliable. That inability to record one fact independently of the other is itself an argument for the split.

Like recording a person's allergies and their phone numbers in one spreadsheet row: with 3 allergies and 2 numbers you write 6 lines, and none of the pairings mean anything.

saying these in an interview costs you the question

  • Claiming the wide table is fine because SELECT DISTINCT hides the duplication at read time
  • Saying the row count does not matter, when it grows multiplicatively and skews every aggregate
  • Proposing a comma-separated textbook list in one column instead of a second table
  • Splitting without checking whether the pairing is meaningful, which would destroy real information

context