Database Management Systems (DBMS) Module 6 – Functional dependencies
Redundancy and update, insert, delete anomalies
Why storing one fact in many rows causes update, insertion and deletion anomalies, and how a careless split of a table invents rows that never existed.
What you will learn
- Count the copies of a fact in a table and explain why they must change together
- Recognise insertion, deletion and update anomalies in a badly designed table
- Show how a lossy decomposition creates spurious tuples, with the row counts of a real join
- Test whether a split into two tables is lossless from the columns the pieces share
- State the goals of schema refinement, namely no redundancy, lossless joins and preserved dependencies
Before you start
What you’ll learn · Before you start
What you will learn
- Count the copies of a fact in a table and explain why they must change together
- Recognise insertion, deletion and update anomalies in a badly designed table
- Show how a lossy decomposition creates spurious tuples, with the row counts of a real join
- Test whether a split into two tables is lossless from the columns the pieces share
- State the goals of schema refinement, namely no redundancy, lossless joins and preserved dependencies
Before you start
On this page
An enrolment sheet that copies each instructor’s office into every enrolment row stores Iyer’s office in five rows, so one move to a new room has to change five rows. Storing a fact more than once is redundancy: Storing the same fact more than once, in several rows or tables, so that every copy has to change whenever the fact changes., and it causes three kinds of trouble, called anomalies: an update that leaves copies disagreeing, a fact that cannot be stored until an unrelated one exists, and a delete that takes an unrelated fact with it.
The cure is to split the table so that each fact is stored once, and the split needs care too: a careless one joins back into rows that never existed. This lesson runs all three anomalies on a real SQLite table, joins a careless split back into 13 rows from 8 and a careful one into exactly 8, and ends with the three goals that the rest of this module turns into rules.
One sheet stores the same office five times
Many college offices keep one big sheet of enrolments: a row for each student in each course, with everything about
the course and its instructor copied into the row. Here it is as an SQLite table. Its key is the pair
(roll_no, course), because a student takes a course once. The second query counts how many rows repeat each
instructor’s office:
-- One table for students, courses, instructors and grades: the enrolment sheet.
CREATE TABLE sheet (
roll_no INTEGER NOT NULL, student TEXT, course TEXT NOT NULL, title TEXT,
instructor TEXT, office TEXT, grade TEXT, PRIMARY KEY (roll_no, course));
INSERT INTO sheet VALUES
(101, 'Aarav', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(101, 'Aarav', 'CS302', 'Systems', 'Sen', 'C-110', 'B'),
(102, 'Diya', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(102, 'Diya', 'CS303', 'Networks', 'Iyer', 'B-204', 'B'),
(103, 'Kabir', 'CS302', 'Systems', 'Sen', 'C-110', 'C'),
(103, 'Kabir', 'CS301', 'Databases', 'Iyer', 'B-204', 'B'),
(104, 'Meera', 'CS304', 'Compilers', 'Khan', 'A-015', 'A'),
(104, 'Meera', 'CS303', 'Networks', 'Iyer', 'B-204', 'A');
SELECT * FROM sheet ORDER BY roll_no, course;
-- How many rows repeat each fact about an instructor's office?
SELECT instructor, office, count(*) AS copies
FROM sheet GROUP BY instructor, office ORDER BY copies DESC; Output
┌─────────┬─────────┬────────┬───────────┬────────────┬────────┬───────┐ │ roll_no │ student │ course │ title │ instructor │ office │ grade │ ├─────────┼─────────┼────────┼───────────┼────────────┼────────┼───────┤ │ 101 │ Aarav │ CS301 │ Databases │ Iyer │ B-204 │ A │ │ 101 │ Aarav │ CS302 │ Systems │ Sen │ C-110 │ B │ │ 102 │ Diya │ CS301 │ Databases │ Iyer │ B-204 │ A │ │ 102 │ Diya │ CS303 │ Networks │ Iyer │ B-204 │ B │ │ 103 │ Kabir │ CS301 │ Databases │ Iyer │ B-204 │ B │ │ 103 │ Kabir │ CS302 │ Systems │ Sen │ C-110 │ C │ │ 104 │ Meera │ CS303 │ Networks │ Iyer │ B-204 │ A │ │ 104 │ Meera │ CS304 │ Compilers │ Khan │ A-015 │ A │ └─────────┴─────────┴────────┴───────────┴────────────┴────────┴───────┘ ┌────────────┬────────┬────────┐ │ instructor │ office │ copies │ ├────────────┼────────┼────────┤ │ Iyer │ B-204 │ 5 │ │ Sen │ C-110 │ 2 │ │ Khan │ A-015 │ 1 │ └────────────┴────────┴────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < sheet.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
Count the copies. Iyer’s office, B-204, is written in five rows, because Iyer teaches two of the courses those rows
mention. The title of CS301 is written three times, once for each student in it. Each row holds four kinds of fact:
who the student is, what the course is called and who teaches it, where that instructor sits, and the student’s
grade. Only the grade is a fact about the whole key. The student’s name depends on roll_no alone and the title on
course alone, while the office depends on the instructor, who is not part of the key at all. Every one of those
facts is repeated in each row that mentions its subject.
Count another fact
Press Edit and make the last query count the copies of each title: select course, title, count(*) and group by
course, title. Every course but CS304 comes back with more than one copy.
An update that changes three of five copies
Iyer moves to room B-310. The college office updates the rows it is looking at, those of CS301, and stops there. Step through what the update does to the five rows that name Iyer:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Five copies. Iyer teaches CS301 and CS303, and the sheet copies Iyer's office into every enrolment in either course. Five rows say B-204.
- The update. The college office updates the rows it is looking at: UPDATE sheet SET office = 'B-310' WHERE course = 'CS301'. The three CS301 rows change.
- Two rows missed. The two CS303 rows name Iyer too, but the WHERE clause names a course, not an instructor, so they keep B-204. No constraint fails.
- Two rooms. The query that groups Iyer's rows by office now finds two answers, B-204 in 2 rows and B-310 in 3. The sheet contradicts itself.
The program runs the same update on the whole sheet and then asks where Iyer sits:
CREATE TABLE sheet (
roll_no INTEGER NOT NULL, student TEXT, course TEXT NOT NULL, title TEXT,
instructor TEXT, office TEXT, grade TEXT, PRIMARY KEY (roll_no, course));
INSERT INTO sheet VALUES
(101, 'Aarav', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(101, 'Aarav', 'CS302', 'Systems', 'Sen', 'C-110', 'B'),
(102, 'Diya', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(102, 'Diya', 'CS303', 'Networks', 'Iyer', 'B-204', 'B'),
(103, 'Kabir', 'CS302', 'Systems', 'Sen', 'C-110', 'C'),
(103, 'Kabir', 'CS301', 'Databases', 'Iyer', 'B-204', 'B'),
(104, 'Meera', 'CS304', 'Compilers', 'Khan', 'A-015', 'A'),
(104, 'Meera', 'CS303', 'Networks', 'Iyer', 'B-204', 'A');
-- Iyer moves to room B-310. The update names one course, so it changes
-- only the rows of that course.
UPDATE sheet SET office = 'B-310' WHERE course = 'CS301';
-- One instructor, two offices: the copies now disagree.
SELECT instructor, office, count(*) AS rows_saying_so
FROM sheet WHERE instructor = 'Iyer'
GROUP BY instructor, office ORDER BY office; Output
┌────────────┬────────┬────────────────┐ │ instructor │ office │ rows_saying_so │ ├────────────┼────────┼────────────────┤ │ Iyer │ B-204 │ 2 │ │ Iyer │ B-310 │ 3 │ └────────────┴────────┴────────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < update_anomaly.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
The database did exactly what it was told. It changed the three rows that name CS301 and left the two rows of CS303 alone, so the sheet now says that Iyer sits in two rooms at once. No constraint was broken, because the table has no rule that ties an instructor to a single office, and it cannot have one while the office is copied into every enrolment. This is an update anomaly: A fact copied into many rows has to be changed in all of them; changing only some leaves copies that disagree.. Every program and every person who changes an office must remember to change all the copies, and the cost of forgetting is a table that contradicts itself, whose reports disagree depending on which row they read.
You can look for this damage with a query that groups the rows by instructor and counts the different offices in each group. The exercise at the end of the lesson asks you to write it.
Checkpoint
Check: One fact stored in many rows · Update anomalies
A new course has nowhere to go
The department adds a course for the next term, CS405 Machine Learning, to be taught by Rao in room D-002. Nobody has enrolled yet. The sheet has no place for a course without a student, because every row needs a roll number:
CREATE TABLE sheet (
roll_no INTEGER NOT NULL, student TEXT, course TEXT NOT NULL, title TEXT,
instructor TEXT, office TEXT, grade TEXT, PRIMARY KEY (roll_no, course));
INSERT INTO sheet VALUES
(101, 'Aarav', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(101, 'Aarav', 'CS302', 'Systems', 'Sen', 'C-110', 'B'),
(102, 'Diya', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(102, 'Diya', 'CS303', 'Networks', 'Iyer', 'B-204', 'B'),
(103, 'Kabir', 'CS302', 'Systems', 'Sen', 'C-110', 'C'),
(103, 'Kabir', 'CS301', 'Databases', 'Iyer', 'B-204', 'B'),
(104, 'Meera', 'CS304', 'Compilers', 'Khan', 'A-015', 'A'),
(104, 'Meera', 'CS303', 'Networks', 'Iyer', 'B-204', 'A');
-- A new course, CS405 Machine Learning, will be taught by Rao in D-002.
-- Nobody has enrolled yet, so there is no roll number to give.
INSERT INTO sheet (course, title, instructor, office)
VALUES ('CS405', 'Machine Learning', 'Rao', 'D-002');
SELECT count(*) AS rows_about_cs405 FROM sheet WHERE course = 'CS405'; Output (exit status 1)
┌──────────────────┐ │ rows_about_cs405 │ ├──────────────────┤ │ 0 │ └──────────────────┘
Printed as an error (standard error)
Runtime error near line 16: NOT NULL constraint failed: sheet.roll_no
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < insert_anomaly.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
SQLite refuses the row because roll_no is part of the primary key and is declared NOT NULL. That declaration
matters in SQLite. The SQL standard says that a primary key column can never be NULL, but SQLite, to stay compatible
with early versions, accepts NULL in most primary key columns unless they are declared NOT NULL
(SQLite: CREATE TABLE). PostgreSQL follows the standard: the values of a
primary key must be unique and not null
(PostgreSQL 18: constraints). Without the NOT NULL, SQLite
would have stored the course next to an empty roll number, a fake enrolment that every count of students would then
have to skip. Either way the design forces a bad choice, an insertion anomaly: A fact that cannot be stored until an unrelated fact exists to share its row, such as a new course with no student enrolled yet.:
leave the fact out, or record it next to an invented one.
Deleting one enrolment deletes a course
Meera drops Compilers. Hers was the only enrolment in CS304, so deleting it deletes more than an enrolment:
CREATE TABLE sheet (
roll_no INTEGER NOT NULL, student TEXT, course TEXT NOT NULL, title TEXT,
instructor TEXT, office TEXT, grade TEXT, PRIMARY KEY (roll_no, course));
INSERT INTO sheet VALUES
(101, 'Aarav', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(101, 'Aarav', 'CS302', 'Systems', 'Sen', 'C-110', 'B'),
(102, 'Diya', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(102, 'Diya', 'CS303', 'Networks', 'Iyer', 'B-204', 'B'),
(103, 'Kabir', 'CS302', 'Systems', 'Sen', 'C-110', 'C'),
(103, 'Kabir', 'CS301', 'Databases', 'Iyer', 'B-204', 'B'),
(104, 'Meera', 'CS304', 'Compilers', 'Khan', 'A-015', 'A'),
(104, 'Meera', 'CS303', 'Networks', 'Iyer', 'B-204', 'A');
-- Meera drops Compilers. Hers was the only row for CS304.
DELETE FROM sheet WHERE roll_no = 104 AND course = 'CS304';
-- The course's title, its instructor and Khan's office went with it.
SELECT count(*) AS rows_about_cs304 FROM sheet WHERE course = 'CS304';
SELECT count(*) AS rows_about_khan FROM sheet WHERE instructor = 'Khan'; Output
┌──────────────────┐ │ rows_about_cs304 │ ├──────────────────┤ │ 0 │ └──────────────────┘ ┌─────────────────┐ │ rows_about_khan │ ├─────────────────┤ │ 0 │ └─────────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < delete_anomaly.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
After the delete, the database no longer knows that CS304 exists, what it is called, who teaches it or where Khan sits, although the statement mentioned none of that: a deletion anomaly: Deleting one fact also removes another that was stored only in the same rows, such as a course lost with its last enrolment.. It and the insertion anomaly are two sides of one fault. Step through both, the refused insert on the left and the delete on the right:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A course, no students. CS405 Machine Learning will be taught by Rao in D-002. Nobody has enrolled yet, so no row of the sheet can carry these facts.
- The insert is refused. Every row needs a roll number, the first part of the key, which is declared NOT NULL. SQLite refuses the row: the insertion anomaly.
- The only CS304 row. Meera's enrolment in CS304 is the only row that says what CS304 is called, who teaches it and where Khan sits.
- The delete. Meera drops Compilers. The delete names one enrolment, and the course's title, its instructor and Khan's office go with it.
- One fault, two faces. Both anomalies have the same cause. Facts about courses and instructors exist only while some enrolment row carries them.
Facts about courses and instructors live only inside enrolment rows, so they exist only while some enrolment carries them. These faults are as old as the relational model. E. F. Codd’s paper that introduced it, A Relational Model of Data for Large Shared Data Banks, already treats redundancy and the consistency of stored data as central problems, and William Kent’s A Simple Guide to Five Normal Forms in Relational Database Theory shows the same faults with a warehouse address kept in records about parts.
Checkpoint
Check: Insertion anomalies · Deletion anomalies
A careless split joins back into 13 rows
The fix is to give each fact a table of its own, keyed by whatever determines it. Before doing that properly, look at a split that seems reasonable and is not. It keeps each enrolment with its instructor, and moves the office and the course title into a table of staff, looked up by instructor:
CREATE TABLE sheet (
roll_no INTEGER NOT NULL, student TEXT, course TEXT NOT NULL, title TEXT,
instructor TEXT, office TEXT, grade TEXT, PRIMARY KEY (roll_no, course));
INSERT INTO sheet VALUES
(101, 'Aarav', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(101, 'Aarav', 'CS302', 'Systems', 'Sen', 'C-110', 'B'),
(102, 'Diya', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(102, 'Diya', 'CS303', 'Networks', 'Iyer', 'B-204', 'B'),
(103, 'Kabir', 'CS302', 'Systems', 'Sen', 'C-110', 'C'),
(103, 'Kabir', 'CS301', 'Databases', 'Iyer', 'B-204', 'B'),
(104, 'Meera', 'CS304', 'Compilers', 'Khan', 'A-015', 'A'),
(104, 'Meera', 'CS303', 'Networks', 'Iyer', 'B-204', 'A');
-- A tempting split: keep each enrolment with its instructor, and look up
-- the office and the course title by instructor.
CREATE TABLE enrolled AS SELECT roll_no, student, course, instructor, grade FROM sheet;
CREATE TABLE staff AS SELECT DISTINCT instructor, office, title FROM sheet;
-- Join them back on the only shared column, instructor.
CREATE TABLE rejoined AS SELECT * FROM enrolled NATURAL JOIN staff;
SELECT (SELECT count(*) FROM sheet) AS original_rows,
(SELECT count(*) FROM rejoined) AS rows_after_join;
-- Rows the join invented: courses paired with a title they never had.
SELECT roll_no, course, title FROM rejoined
EXCEPT
SELECT roll_no, course, title FROM sheet
ORDER BY roll_no, course; Output
┌───────────────┬─────────────────┐ │ original_rows │ rows_after_join │ ├───────────────┼─────────────────┤ │ 8 │ 13 │ └───────────────┴─────────────────┘ ┌─────────┬────────┬───────────┐ │ roll_no │ course │ title │ ├─────────┼────────┼───────────┤ │ 101 │ CS301 │ Networks │ │ 102 │ CS301 │ Networks │ │ 102 │ CS303 │ Databases │ │ 103 │ CS301 │ Networks │ │ 104 │ CS303 │ Databases │ └─────────┴────────┴───────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < lossy_split.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
The join returns 13 rows from 8. All 8 original rows are among them, and 5 more pair an enrolment with the wrong title. Step through the join for the five enrolments in Iyer’s courses to see where the extra rows come from:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Two tables. enrolled keeps each enrolment with its instructor, and staff keeps one row per instructor, office and title. Iyer has two staff rows, one per title.
- One enrolment, two matches. The join matches rows on instructor alone. Aarav's CS301 row meets both of Iyer's staff rows, so it comes back twice, once with the wrong title.
- Every Iyer enrolment. The same happens to all five enrolments in Iyer's courses. The solid arrows are the true titles; the dashed ones are the 5 rows the sheet never had.
- 13 rows. 5 enrolments meet 2 staff rows each, 10 rows; Sen's 2 and Khan's 1 enrolments meet one row each, 3 more. 13 rows come back from 8.
Rows that a join produces but the original table never had are called spurious tuple: A row that the join of a split's pieces returns although the original table never held it., and a split that produces them is a lossy decomposition: A split whose pieces join back into rows the original table never had, so the true rows can no longer be told apart.. The name sounds odd for a split that gains rows, but what it loses is information: from the two smaller tables, nobody can tell which of the 13 rows are true.
The cause is the column the two tables share. SQLite’s NATURAL JOIN matches rows on every column that appears in
both inputs (SQLite: SELECT), which here is instructor, and the instructor
does not determine the title, because Iyer teaches two courses. Spreadsheets and data tools behave the same way:
joining two exported files on a column that is not unique in either file multiplies rows.
Four tables join back into the same 8 rows
Now the careful split. Each fact goes into a table keyed by what determines it: a student’s name by roll number, a
course’s title and instructor by course, an instructor’s office by instructor, and a grade by the pair
(roll_no, course). The figure shows the four tables and the key that each join follows:
The enrolment sheet as four tables, each fact stored once
Text description of the diagram
The diagram shows the four tables of the lossless split and the keys that join them.
- enrolments has the columns roll_no and course, which together form its primary key, and grade. Each pair of a student and a course appears once.
- enrolments.roll_no points to students.roll_no, the primary key of students, which also holds the student's name. Each enrolment row finds exactly one student.
- enrolments.course points to courses.course, the primary key of courses, which also holds the title and the instructor. Each enrolment row finds exactly one course.
- courses.instructor points to offices.instructor, the primary key of offices, which holds the office. Each course finds exactly one office.
Every join follows a key, which is why joining the four tables gives back the original rows and nothing more.
The program builds the four tables from the sheet, joins them back, and moves Iyer once more:
CREATE TABLE sheet (
roll_no INTEGER NOT NULL, student TEXT, course TEXT NOT NULL, title TEXT,
instructor TEXT, office TEXT, grade TEXT, PRIMARY KEY (roll_no, course));
INSERT INTO sheet VALUES
(101, 'Aarav', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(101, 'Aarav', 'CS302', 'Systems', 'Sen', 'C-110', 'B'),
(102, 'Diya', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(102, 'Diya', 'CS303', 'Networks', 'Iyer', 'B-204', 'B'),
(103, 'Kabir', 'CS302', 'Systems', 'Sen', 'C-110', 'C'),
(103, 'Kabir', 'CS301', 'Databases', 'Iyer', 'B-204', 'B'),
(104, 'Meera', 'CS304', 'Compilers', 'Khan', 'A-015', 'A'),
(104, 'Meera', 'CS303', 'Networks', 'Iyer', 'B-204', 'A');
-- Each fact in a table of its own, keyed by what determines it.
CREATE TABLE students AS SELECT DISTINCT roll_no, student FROM sheet;
CREATE TABLE courses AS SELECT DISTINCT course, title, instructor FROM sheet;
CREATE TABLE offices AS SELECT DISTINCT instructor, office FROM sheet;
CREATE TABLE enrolments AS SELECT roll_no, course, grade FROM sheet;
CREATE TABLE rejoined AS
SELECT roll_no, student, course, title, instructor, office, grade
FROM enrolments NATURAL JOIN students NATURAL JOIN courses NATURAL JOIN offices;
SELECT (SELECT count(*) FROM sheet) AS original_rows,
(SELECT count(*) FROM rejoined) AS rows_after_join,
(SELECT count(*) FROM (SELECT * FROM rejoined EXCEPT SELECT * FROM sheet)) AS invented,
(SELECT count(*) FROM (SELECT * FROM sheet EXCEPT SELECT * FROM rejoined)) AS lost;
-- Iyer's move is now one change to one row.
UPDATE offices SET office = 'B-310' WHERE instructor = 'Iyer';
SELECT changes() AS rows_changed; Output
┌───────────────┬─────────────────┬──────────┬──────┐ │ original_rows │ rows_after_join │ invented │ lost │ ├───────────────┼─────────────────┼──────────┼──────┤ │ 8 │ 8 │ 0 │ 0 │ └───────────────┴─────────────────┴──────────┴──────┘ ┌──────────────┐ │ rows_changed │ ├──────────────┤ │ 1 │ └──────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < lossless_split.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
The join returns exactly the 8 original rows, with none invented and none lost. Each step of the join matches on a
column that is the key of the table it brings in, roll_no for students, course for courses and instructor for
offices, so every enrolment finds exactly one partner in each table. The anomalies are gone too. Iyer’s move changes
one row, a course without students is simply a row in courses, and dropping the last enrolment in CS304 leaves
CS304 in courses and Khan’s room in offices.
For a split of a table into two pieces there is a simple test: the split is a
lossless decomposition: A split whose pieces always join back into exactly the rows of the original table, none invented and none lost. when the columns the pieces share determine every
column of one of the pieces, which means the shared columns are a key of that piece. The careless split fails it,
because instructor is a key of neither enrolled nor staff. A later lesson of this track proves the test and gives
the general one for any number of pieces, which comes from the theory of joins of Aho, Beeri and Ullman
(The Theory of Joins in Relational Databases).
Key idea
A split into two tables is lossless when the columns the tables share are a key of one of them; a join on any other column multiplies rows.
Checkpoint
Check: Lossy splits and spurious rows · The lossless test for two pieces
Schema refinement has three goals
Splitting tables to remove redundancy is called normalisation: Refining a schema by splitting tables so that each fact is stored once, keyed by what determines it, without losing rows or rules., and the next lessons of this module make it precise. A good refinement aims for three things at once, and this lesson has already met each of them:
| Goal | What it asks | Seen in this lesson |
|---|---|---|
| No redundancy | Each fact is stored once | Broken by the sheet: five copies of one office |
| Lossless joins | The pieces join back into exactly the original rows | Broken by the careless split: 13 rows from 8 |
| Dependency preservation | Every rule can be checked inside one table | Kept by the four tables, each rule in its own table |
A fact stored once is changed in one place, recorded without an unrelated fact and deleted only on purpose, so the first goal removes all three anomalies. The first goal says what to remove; the other two say what a split must not break, and later lessons show a case where they pull against each other. dependency preservation: A split preserves dependencies when every functional dependency of the original table can still be checked within the pieces, without a join. More in “Lossless joins and dependency preservation” matters because a rule that spans two tables needs a join, or a trigger, on every insert to stay true. The rules that say which attribute determines which are called functional dependency: A rule X -> Y on a relation: any two rows that have the same values for the attributes X also have the same values for Y. More in “Functional dependencies and Armstrong's axioms”, the subject of the next lesson.
Normalisation is a tool, not a goal in itself: some systems keep copies on purpose, for speed or to record history, and accept the work of keeping them in step. What matters is that the copy is a decision, never an accident.
Checkpoint
Check: The goals of schema refinement
Practise: find the copies that disagree
The exercise gives you a sheet after a careless update and asks for every instructor whose rows name more than one office. Its sample tests run your query on three small sheets in your browser:
Exercise · Easy · SQL
Find the instructors whose copies of an office disagree
The table sheet is the enrolment sheet of this lesson, with the columns roll_no, student, course, title, instructor, office and grade. Every row copies the office of the row's instructor, so after a careless update the copies can disagree.
Write one query that returns every instructor whose rows name more than one office, with two columns:
instructor: the instructor's name;offices: how many different offices their rows name.
Rows whose office is NULL do not count as an office. An instructor whose rows all agree must not appear, so on a consistent sheet the query returns no rows. The sample tests run your query on three small sheets and compare its result with the expected rows, in any order.
Starter code · disagreeing.sql
-- Every instructor whose rows name more than one office, and how many.
SELECT instructor, count(*) AS offices
FROM sheet; The sample tests · tests.yaml
# Sample tests: each one builds a small sheet, runs your query on it and compares the rows it returns.
tests:
- name: finds the instructor whose office was updated in only some rows
setup: |
CREATE TABLE sheet (roll_no INTEGER, student TEXT, course TEXT, title TEXT, instructor TEXT, office TEXT, grade TEXT);
INSERT INTO sheet VALUES
(101, 'Aarav', 'CS301', 'Databases', 'Iyer', 'B-310', 'A'),
(101, 'Aarav', 'CS302', 'Systems', 'Sen', 'C-110', 'B'),
(102, 'Diya', 'CS301', 'Databases', 'Iyer', 'B-310', 'A'),
(102, 'Diya', 'CS303', 'Networks', 'Iyer', 'B-204', 'B'),
(103, 'Kabir', 'CS302', 'Systems', 'Sen', 'C-110', 'C'),
(104, 'Meera', 'CS303', 'Networks', 'Iyer', 'B-204', 'A');
expected:
columns: [instructor, offices]
rows:
- [Iyer, 2]
- name: returns no rows when every instructor's copies agree
setup: |
CREATE TABLE sheet (roll_no INTEGER, student TEXT, course TEXT, title TEXT, instructor TEXT, office TEXT, grade TEXT);
INSERT INTO sheet VALUES
(101, 'Aarav', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(102, 'Diya', 'CS301', 'Databases', 'Iyer', 'B-204', 'B'),
(103, 'Kabir', 'CS304', 'Compilers', 'Khan', 'A-015', 'A');
expected:
columns: [instructor, offices]
rows: []
- name: counts three offices, and does not count a missing office as a different one
setup: |
CREATE TABLE sheet (roll_no INTEGER, student TEXT, course TEXT, title TEXT, instructor TEXT, office TEXT, grade TEXT);
INSERT INTO sheet VALUES
(101, 'Aarav', 'CS304', 'Compilers', 'Khan', 'A-015', 'A'),
(102, 'Diya', 'CS304', 'Compilers', 'Khan', 'A-016', 'B'),
(103, 'Kabir', 'CS305', 'Graphics', 'Khan', 'A-017', 'B'),
(104, 'Meera', 'CS302', 'Systems', 'Sen', 'C-110', 'A'),
(105, 'Vikram', 'CS302', 'Systems', 'Sen', NULL, 'C'),
(106, 'Zoya', 'CS301', 'Databases', 'Iyer', 'B-204', 'A'),
(107, 'Rohan', 'CS303', 'Networks', 'Iyer', 'B-310', 'B');
expected:
columns: [instructor, offices]
rows:
- [Khan, 3]
- [Iyer, 2] A hint
One row per instructor means GROUP BY instructor. count(DISTINCT office) counts the different offices in each group and skips NULLs. A condition on a group, rather than on a row, goes in HAVING, after the grouping.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
The self-check below draws on fourteen questions, from Iyer’s five copies to the three goals. It begins with the idea you have answered worst and raises the level after each right answer:
Practice that adapts to you
Practise this lesson
14 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What are update, insertion and deletion anomalies? Give an example of each.
They are the three problems of a table that stores one fact in many rows. An update anomaly: an enrolment sheet copies each instructor’s office into every enrolment, so a change of office must update every copy, and updating only some leaves the table contradicting itself. An insertion anomaly: a new course cannot be recorded until a student enrols, because every row needs a roll number. A deletion anomaly: when the last student drops a course, deleting that enrolment also deletes the course’s title and its instructor’s office. All three come from mixing facts about different things in one table; storing each fact in a table keyed by what determines it removes them.
Interviewers tend to push on two points of this answer. Choose a reply to each; every choice explains what it gets right or misses:
Checkpoint
Interview round: two follow-up questions
Two design questions of this module start from this lesson:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
An orders table stores each customer's delivery address in every order row. Support finds customers whose recent orders show different addresses. Is this an update anomaly, and how would you change the schema and move 40 million rows without stopping the service?
-
Fresher to mid
You split a wide table into two to remove duplication. A monthly revenue report built on a join of the two tables then jumps by 30 %. How do you prove whether the split caused it, and how do you choose a split that cannot add rows?
Key takeaways
- Redundancy is one fact stored in many rows: the sheet kept Iyer’s office in five rows, and nothing in the table made the copies agree.
- An update anomaly leaves copies that disagree, as the update by course that changed three of Iyer’s five rows did.
- An insertion anomaly needs an unrelated fact to store a new one, and a deletion anomaly removes a fact along with another; both come from facts that live only inside enrolment rows.
- A split into two tables is lossless when the shared columns are a key of one of the pieces; otherwise the join invents spurious rows, 5 of 13 in the careless split.
- Schema refinement aims for no redundancy, lossless joins and preserved dependencies, and keeps a copy only as a decision.
Terms in this lesson
- deletion anomaly
- Deleting one fact also removes another that was stored only in the same rows, such as a course lost with its last enrolment.
- insertion anomaly
- A fact that cannot be stored until an unrelated fact exists to share its row, such as a new course with no student enrolled yet.
- lossless decomposition
- A split whose pieces always join back into exactly the rows of the original table, none invented and none lost.
- lossy decomposition
- A split whose pieces join back into rows the original table never had, so the true rows can no longer be told apart.
- normalisation
- Refining a schema by splitting tables so that each fact is stored once, keyed by what determines it, without losing rows or rules.
- redundancy
- Storing the same fact more than once, in several rows or tables, so that every copy has to change whenever the fact changes.
- spurious tuple
- A row that the join of a split's pieces returns although the original table never held it.
- update anomaly
- A fact copied into many rows has to be changed in all of them; changing only some leaves copies that disagree.
References
- E. F. Codd: A Relational Model of Data for Large Shared Data Banks (section 2, redundancy and consistency) (Communications of the ACM)
- W. Kent: A Simple Guide to Five Normal Forms in Relational Database Theory (Communications of the ACM)
- A. V. Aho, C. Beeri and J. D. Ullman: The Theory of Joins in Relational Databases (ACM Transactions on Database Systems)
- SQLite: CREATE TABLE (primary keys and NULL) (SQLite)
- SQLite: SELECT (NATURAL JOIN and USING) (SQLite)
- PostgreSQL 18 Documentation: Constraints (primary keys) (The PostgreSQL Global Development Group)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress