Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 20 minutes
  • Examples run with sql.js 1.14.2
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

The enrolment sheet, and how often one fact repeats SQL · sheet.sql
-- 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

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:

Interactive · step by step Iyer moves to B-310, and the sheet hears it three times Step through the update. Each box is a row of the sheet that names Iyer, with the office it stores.
Iyer moves to B-310, and the sheet hears it three times, step 4 of 4. Showing Aarav, CS301 (office B-310), Diya, CS301 (office B-310), Kabir, CS301 (office B-310), Diya, CS303, Meera, CS303. In focus: Diya, CS303, Meera, CS303.Aarav, CS301office B-310Diya, CS301office B-310Kabir, CS301office B-310Diya, CS303office B-204Meera, CS303office B-204

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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:

Iyer moves office, and only some rows hear about it SQL · update_anomaly.sql
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

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

  1. Question 1 of 2 Three more students enrol in CS303, which Iyer teaches. How many rows of the sheet then store Iyer's office?

    Type a number.

    A hint

    Count the rows that name Iyer today, then add one for every new enrolment in one of Iyer's courses.

    Show the answer: Question 1 of 2

    Answer: 8 rows

    Today five rows name Iyer: three enrolments in CS301 and two in CS303. Each new enrolment in CS303 copies the office once more, so 5 + 3 = 8 rows store it, and a move to a new room must now change eight rows.

  2. Question 2 of 2 After UPDATE sheet SET office = 'B-310' WHERE course = 'CS301', what does the sheet say about Iyer's office?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Three rows say B-310 and two rows say B-204

    The update changes exactly the rows its WHERE clause names: the three enrolments in CS301. The two enrolments in CS303 still say B-204, so the sheet now places Iyer in two rooms, and no constraint noticed.

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:

A new course cannot be recorded until someone enrols SQL · insert_anomaly.sql
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

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:

Meera drops a course, and the course disappears SQL · delete_anomaly.sql
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

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:

Interactive · step by step Course facts live only inside enrolment rows Step through one insert that is refused and one delete that removes too much.
Course facts live only inside enrolment rows, step 5 of 5. Showing New course CS405, INSERT with no roll_no (refused: NOT NULL), Meera, CS304 (deleted), DELETE that one row. In focus: New course CS405, Meera, CS304.New course CS405Rao, room D-002INSERT with no roll_norefused: NOT NULLMeera, CS304deletedDELETE that one row

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. The delete. Meera drops Compilers. The delete names one enrolment, and the course's title, its instructor and Khan's office go with it.
  5. 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

  1. Question 1 of 2 Which change lets the department record CS405, its title and its instructor before anyone enrols, without a fake enrolment?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Store courses in a table of their own, keyed by course

    The insertion anomaly comes from storing course facts only inside enrolment rows. Giving courses their own table, keyed by course, lets a course exist without students, and the sheet's other facts can move the same way.

  2. Question 2 of 2 Which single DELETE on the sheet also erases the fact that Khan sits in A-015?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Deleting Meera's enrolment in CS304

    A fact disappears when the last row that carries it is deleted. Khan's office lives only in Meera's CS304 row, so deleting that enrolment deletes the office, the title of CS304 and the fact that Khan teaches it.

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:

A careless split, joined back: 8 rows become 13 SQL · lossy_split.sql
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

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:

Interactive · step by step Why the careless split returns 13 rows Step through the join on instructor. Left, the enrolments in Iyer's courses; right, Iyer's two rows in staff.
Why the careless split returns 13 rows, step 4 of 4. Showing Aarav, CS301 (2 rows, 1 spurious), Diya, CS301, Kabir, CS301, Diya, CS303, Meera, CS303, Iyer, Databases, Iyer, Networks.Aarav, CS3012 rows, 1 spuriousDiya, CS301Kabir, CS301Diya, CS303Meera, CS303Iyer, Databasesstaff row 1Iyer, Networksstaff row 2instructor does not decide title

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.

Join CSV/Excel Files (VLOOKUP Online) Join two CSV files on a column and compare the row counts before and after.

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:

Four tables: enrolments point to one student and one course, and each course points to its instructor in offices.enrolmentsroll_nointegerPK, FKcoursetextPK, FKgradetextstudentsroll_nointegerPKstudenttextcoursescoursetextPKtitletextinstructortextFKofficesinstructortextPKofficetextone studentone courseone office

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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:

Four tables, joined back: the same 8 rows, and a one-row update SQL · lossless_split.sql
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

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

  1. Question 1 of 2 A sixth enrolment in CS303 is added to the sheet before the careless split. How many rows does the join of enrolled and staff then return?

    Type a number.

    A hint

    Each enrolment in one of Iyer's courses meets both of Iyer's staff rows; the others meet one.

    Show the answer: Question 1 of 2

    Answer: 15 rows

    Iyer's enrolments rise from 5 to 6, and each meets Iyer's two staff rows: 12 rows. Sen's 2 enrolments and Khan's 1 meet one staff row each: 3 rows. The join returns 12 + 3 = 15 rows from 9, and 6 of them are spurious.

  2. Question 2 of 2 results(roll_no, course, grade, title) has the rule course -> title. It is split into (roll_no, course, grade) and (course, title). Is the split lossless?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Yes, because the shared column course determines title, so it is a key of (course, title)

    For a split into two pieces, the join is lossless when the shared columns are a key of one piece. course determines title, so each enrolment meets exactly one (course, title) row and the join returns the original rows.

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

  1. One question A split stores each fact once and joins back into exactly the original rows, but checking that a course has one instructor now needs a join on every insert. Which goal does it miss?

    Choose one answer.

    Show the answer: One question

    Answer: Dependency preservation

    The three goals are separate. This split removes redundancy and keeps a lossless join, but a rule whose attributes end up in different tables can no longer be checked inside one table, which is what dependency preservation asks for.

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.

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.

  1. Question 1 of 14 What makes a column redundant in the sense of this lesson?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: It stores a fact that other rows already store, so all the copies must change together

    A column is redundant when it repeats a fact that is stored elsewhere, such as an instructor's office copied into every enrolment. Each copy must change whenever the fact does, and the table itself enforces nothing.

  2. Question 2 of 14 One more student enrols in CS303 and one more in CS302. How many rows of the sheet then store Iyer's office?

    Type a number.

    A hint

    CS302 is Sen's course, not Iyer's.

    Show the answer: Question 2 of 14

    Answer: 6 rows

    Iyer teaches CS301 and CS303. The five rows that name Iyer gain one for the new CS303 enrolment, so six rows store the office. The CS302 enrolment copies Sen's office instead.

  3. Question 3 of 14 A library keeps one table, loans(member_id, member_name, member_phone, book_id, title, due_date), with a row per loan. A member gives a new phone number, and the clerk changes it only in the row of the member's latest loan. Which anomaly is this?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: An update anomaly

    The phone number is a fact about the member, copied into every loan row. Changing one copy leaves the other loans with the old number, so the table now disagrees with itself about one member.

  4. Question 4 of 14 Which design makes Iyer's move a change to one row, whatever WHERE clause the next person writes?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: An offices table keyed by instructor, which holds each office once

    An update anomaly disappears only when the fact has one place. Indexes, habits and repair jobs leave the copies in place; a table keyed by instructor leaves nothing to keep in step.

  5. Question 5 of 14 A shop keeps one table, shipments(order_id, customer, courier, courier_helpline, weight_kg), with a row per order. It signs up a new courier company, but cannot store the courier's helpline number until some order is shipped with it. Which anomaly is this?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: An insertion anomaly

    The helpline is a fact about the courier, but the only place to store it is a row about an order. Until an order exists, the fact has nowhere to go.

  6. Question 6 of 14 Why did SQLite refuse the row for CS405?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: roll_no is part of the primary key and is declared NOT NULL, and the row has no roll number

    Without a student there is no roll number, and roll_no is declared NOT NULL as part of the primary key. Without that declaration SQLite would have stored the course next to a NULL roll number, a fake enrolment.

  7. Question 7 of 14 A clinic keeps one table, visits(patient_id, patient_name, doctor, doctor_ward, visit_date). The only visit booked with a newly joined doctor is cancelled and its row deleted. Afterwards nobody can find which ward the doctor works in. Which anomaly is this?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: A deletion anomaly

    The ward was stored only inside visit rows. Deleting the last visit with that doctor also deleted the only copy of a fact about the doctor.

  8. Question 8 of 14 What do the insertion and the deletion anomaly of the sheet have in common?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: Facts about courses and instructors are stored only inside enrolment rows

    An enrolment row is the only place a course or an instructor fact can live in the sheet. A fact that needs a row cannot be stored before an enrolment exists, and is deleted when the last enrolment goes.

  9. Question 9 of 14 In the careless split of this lesson, the 8 rows of the sheet joined back into a larger result. How many of the rows after the join are spurious, that is, rows that were never in the sheet?

    Type a number.

    Show the answer: Question 9 of 14

    Answer: 5 rows

    The join returned 13 rows: the 8 original ones and 5 more. Each enrolment in one of Iyer's two courses was paired with both titles Iyer teaches, because instructor, the shared column, does not determine the title.

  10. Question 10 of 14 In the careless split, why does Diya's CS303 enrolment also come back with the title Databases?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: The join matches on instructor, and Iyer has a staff row for Databases as well as for Networks

    NATURAL JOIN pairs rows that agree on every shared column, here instructor alone. Iyer has two staff rows, so every enrolment in an Iyer course comes back twice, once with each of Iyer's titles.

  11. Question 11 of 14 A table R(A, B, C) is split into R1(A, B) and R2(B, C), and the two are joined back on B. Which condition guarantees that the join returns exactly the rows of R?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: B determines C, or B determines A

    A split into two tables is lossless when the shared columns are a key of one of the pieces. If B determines C, every row of R1 matches exactly one row of R2, so no row is invented; B determining A works the same way from the other side.

  12. Question 12 of 14 After the careful split into four tables, how many rows does Iyer's move to B-310 change?

    Type a number.

    Show the answer: Question 12 of 14

    Answer: 1 rows

    offices holds one row per instructor, so Iyer's office is stored once and the update changes one row, as the program's changes() count showed.

  13. Question 13 of 14 Which of these does schema refinement aim for?

    Choose every answer that is right.

    Show the answer: Question 13 of 14

    Answer:

    • Every rule of the data can be checked inside one table
    • Each fact is stored once
    • Joining the pieces gives back exactly the original rows

    The three goals are no redundancy, lossless joins and dependency preservation. More tables are not a goal: a split that serves none of the three only adds joins.

  14. Question 14 of 14 When is it sound to keep a copy of a fact in a second table?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: When the copy is a decision, made for speed or to record history, and the work of keeping it in step is planned

    Normalisation is a tool, not a goal. A copy kept on purpose, with a plan for keeping it in step or a reason it must never change, is a design decision; a copy nobody chose is the redundancy this lesson removes.

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

  1. Question 1 of 2 A teammate says that disk is cheap, so copying the instructor's office into every enrolment costs nothing. What is the strongest answer?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The cost is not space. Every copy is a promise that all copies change together, and nothing in the table keeps it

    Redundancy is a consistency problem, not a storage problem. The copies have to change together, the table has no rule that makes them, and the update anomaly of this lesson needed nothing but an ordinary UPDATE to appear.

  2. Question 2 of 2 After splitting a table in two, how do you show quickly whether the split is lossy?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Join the pieces back, compare the row count with the original, and list the extra rows with EXCEPT

    A lossy split shows itself in the join: more rows than the original, and rows the original never had. Counting and EXCEPT prove it on the data, and the key test explains it: the shared columns must be a key of one piece.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.