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 7 – Normal forms and decomposition

First, second and third normal forms

Test a relation for 1NF, 2NF and 3NF against every candidate key, spot partial and transitive dependencies, and find its highest normal form step by step.

  • Intermediate
  • 20 minutes
  • Examples run with sql.js 1.14.2, Python 3.14.8 and Pyodide 314.0.7
  • 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

  • Test a relation for 1NF and explain what a list in one column hides from keys and constraints
  • Identify prime attributes and partial and transitive dependencies against every candidate key
  • Split a relation into 2NF and then 3NF tables keyed by the left sides of their dependencies
  • Find every candidate key before testing, and show how a missed key changes the verdict
  • Find the highest normal form of a relation with a reliable procedure

Before you start

What you’ll learn · Before you start

What you will learn

  • Test a relation for 1NF and explain what a list in one column hides from keys and constraints
  • Identify prime attributes and partial and transitive dependencies against every candidate key
  • Split a relation into 2NF and then 3NF tables keyed by the left sides of their dependencies
  • Find every candidate key before testing, and show how a missed key changes the verdict
  • Find the highest normal form of a relation with a reliable procedure

Before you start

On this page

The enrolment sheet of this module stores Iyer’s office in five rows and each student’s name once per course, and the first, second and third normal forms are the three tests that find such repeats. 1NF asks for one atomic value in every cell, never a list. 2NF forbids a non-prime attribute that depends on part of a candidate key, the way a student’s name depends on the roll number alone in a table keyed by roll number and course. 3NF asks that, for every non-trivial dependency X -> A, X be a superkey or A be prime, which means part of some candidate key.

Each form implies the ones before it, and all three tests are only as good as the list of candidate keys behind them. This lesson takes the sheet to 3NF, catches a seating plan that a check against its primary key misjudges, and ends with a procedure that finds the highest normal form of any relation, run in 14 languages.

A list in one cell hides its values from the database

A relation is in first normal form (1NF): Every cell of the relation holds a single atomic value from its domain, never a list, a set or a nested table. when every attribute holds atomic values: each cell has one value from its domain, never a list, a set or a table. Codd’s paper that introduced the relational model calls this the normal form. It turns relations whose domains are “nonsimple”, with values that are relations themselves, into flat relations, a step it names normalization (A Relational Model of Data for Large Shared Data Banks). Every later normal form assumes 1NF, because the dependencies it tests compare whole values.

A list in a column is the usual way 1NF breaks in practice. Here a college keeps each student’s courses as one comma-separated string, then stores the same facts with one value in each cell:

Courses kept as a list in one column, then one per row SQL · lists/lists_in_a_cell.sql
-- The courses of each student kept as a list in one column.
CREATE TABLE registrations (roll_no INTEGER PRIMARY KEY, student TEXT, courses TEXT);
INSERT INTO registrations VALUES
  (101, 'Aarav', 'CS301,CS302'),
  (102, 'Diya',  'CS303,CS301L'),
  (103, 'Kabir', 'CS302,CS301'),
  (104, 'Meera', 'CS304,CS3O1');

-- Who takes CS301? The pattern also matches the lab course CS301L,
-- and it misses Meera, whose entry was typed with a letter O.
SELECT roll_no, student FROM registrations WHERE courses LIKE '%CS301%';

-- The same facts with one value per cell: a row per student and course.
CREATE TABLE courses (course TEXT PRIMARY KEY, title TEXT);
INSERT INTO courses VALUES ('CS301', 'Databases'), ('CS301L', 'Databases lab'),
  ('CS302', 'Systems'), ('CS303', 'Networks'), ('CS304', 'Compilers');
PRAGMA foreign_keys = ON;
CREATE TABLE enrolments (
  roll_no INTEGER REFERENCES registrations,
  course TEXT REFERENCES courses,
  PRIMARY KEY (roll_no, course));
INSERT INTO enrolments VALUES (101, 'CS301'), (101, 'CS302'), (102, 'CS303'),
  (102, 'CS301L'), (103, 'CS302'), (103, 'CS301'), (104, 'CS304');
INSERT INTO enrolments VALUES (104, 'CS3O1');

SELECT roll_no FROM enrolments WHERE course = 'CS301';
SELECT course, count(*) AS students FROM enrolments GROUP BY course ORDER BY course;

Output (exit status 1)

┌─────────┬─────────┐
│ roll_no │ student │
├─────────┼─────────┤
│ 101     │ Aarav   │
│ 102     │ Diya    │
│ 103     │ Kabir   │
└─────────┴─────────┘
┌─────────┐
│ roll_no │
├─────────┤
│ 101     │
│ 103     │
└─────────┘
┌────────┬──────────┐
│ course │ students │
├────────┼──────────┤
│ CS301  │ 2        │
│ CS301L │ 1        │
│ CS302  │ 2        │
│ CS303  │ 1        │
│ CS304  │ 1        │
└────────┴──────────┘

Printed as an error (standard error)

Runtime error near line 24: FOREIGN KEY constraint failed

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < lists_in_a_cell.sql

The pattern search for CS301 returns Diya, who takes only the lab course CS301L, and misses Meera, whose list was typed with the letter O. Neither mistake raises an error: the database sees one string per student and cannot check what is inside it. With one course per row, the same questions become ordinary SQL, and the foreign key refuses CS3O1 as soon as someone types it, which is the error at the end of the output:

Question Courses as a list in one cell One course per row
Who takes CS301? LIKE '%CS301%' also finds CS301L and misses CS3O1 course = 'CS301' finds exactly 101 and 103
How many students per course? Every string has to be split first GROUP BY course
A mistyped course code Stored without a word Refused by the foreign key

SQLite checks foreign keys only after PRAGMA foreign_keys = ON (SQLite: foreign keys), which is why the example turns them on. Engines can store lists all the same. SQLite reads a JSON array as rows with json_each (SQLite: JSON functions), and PostgreSQL has array columns, whose manual points out that arrays are not sets and suggests a separate table with a row per element when you search for elements (PostgreSQL 18: arrays). Neither feature changes the design question, because a key, a foreign key or a unique constraint cannot reach inside a list. Atomic is relative to use: a date or a phone number is one value as long as nothing queries its parts.

Each dependency is fine, partial or transitive

2NF and 3NF are stated with three terms, each defined against the candidate keys:

  • an attribute is a prime attribute: An attribute that belongs to at least one candidate key of the relation. An attribute in no candidate key is non-prime. when it belongs to at least one candidate key, and non-prime otherwise;
  • a partial dependency: A dependency X -> A in which A is non-prime and X is a proper part of a candidate key. It breaks second normal form. is X -> A where A is non-prime and X is a proper part of some candidate key;
  • a transitive dependency: A dependency X -> A in which A is non-prime and X is neither a superkey nor part of a candidate key, so A depends on a key only through X. is X -> A where A is non-prime and X is neither a superkey nor contained in a candidate key: a key determines X, and X determines A, so A depends on the key only through X.

Three questions, asked in order, put each dependency into one of these boxes. Step through three dependencies of the enrolment sheet:

Interactive · stage by stage Three questions label a dependency of the sheet Step through three of the sheet's dependencies. Each stops at the first question it answers yes to.
Three questions label a dependency of the sheet, step 4 of 4. Showing One dependency X -> A (roll_no course -> grade), Is X a superkey?, Is A prime?, Is X part of a key?, Otherwise: transitive. In focus: Is X a superkey?.One dependency X -> Aroll_no course -> gradeIs X a superkey?yes: fine, even for BCNFIs A prime?yes: fine for 3NFIs X part of a key?yes: partial, breaks 2NFOtherwise: transitivebreaks 3NF

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

Every step as text
  1. The sheet's key. The sheet has one candidate key, (roll_no, course), so roll_no and course are its only prime attributes. Every other attribute is non-prime.
  2. roll_no -> student. roll_no alone reaches only roll_no and student, so it is not a superkey. student is non-prime. roll_no is half of the key, so the dependency is partial.
  3. instructor -> office. instructor determines only office, and office is non-prime. instructor is part of no candidate key, so the office depends on the key only through it. Transitive.
  4. roll_no course -> grade. The left side is the key itself, so the first question already says yes. The grade is the one fact about the whole key.

The program finds the sheet’s candidate keys and labels every dependency, one attribute on the right at a time:

Every dependency of the enrolment sheet, labelled against its key Python · labels/sheet_dependencies.py
"""The enrolment sheet's dependencies, each classified against its candidate keys."""
from itertools import combinations

SHEET = ["roll_no", "student", "course", "title", "instructor", "office", "grade"]
FDS = [
    ({"roll_no"}, {"student"}),
    ({"course"}, {"title", "instructor"}),
    ({"instructor"}, {"office"}),
    ({"roll_no", "course"}, {"grade"}),
]


def closure(attrs, fds):
    """Every attribute that attrs determine: keep firing dependencies until nothing is added."""
    result = set(attrs)
    grew = True
    while grew:
        grew = False
        for lhs, rhs in fds:
            if lhs <= result and not rhs <= result:
                result |= rhs
                grew = True
    return result


def candidate_keys(relation, fds):
    """The minimal sets of attributes whose closure is the whole relation (smallest first)."""
    keys = []
    for size in range(1, len(relation) + 1):
        for combo in combinations(relation, size):
            if not any(k <= set(combo) for k in keys) and closure(combo, fds) >= set(relation):
                keys.append(set(combo))
    return keys


def show(attrs):
    return ", ".join(a for a in SHEET if a in attrs)


keys = candidate_keys(SHEET, FDS)
prime = set().union(*keys)
print("candidate keys:", " | ".join("{" + show(k) + "}" for k in keys))
print("prime:", show(prime), "   non-prime:", show(set(SHEET) - prime))
print()
for lhs, rhs in FDS:
    for a in sorted(rhs - lhs, key=SHEET.index):
        if closure(lhs, FDS) >= set(SHEET):
            kind = "fine: the left side is a superkey"
        elif a in prime:
            kind = "fine for 3NF: the right side is prime"
        elif any(lhs < k for k in keys):
            kind = "PARTIAL: depends on part of a key, breaks 2NF"
        else:
            kind = "TRANSITIVE: depends on a non-key, breaks 3NF"
        print(f"{show(lhs):>15} -> {a:<10} {kind}")

Output

candidate keys: {roll_no, course}
prime: roll_no, course    non-prime: student, title, instructor, office, grade

        roll_no -> student    PARTIAL: depends on part of a key, breaks 2NF
         course -> title      PARTIAL: depends on part of a key, breaks 2NF
         course -> instructor PARTIAL: depends on part of a key, breaks 2NF
     instructor -> office     TRANSITIVE: depends on a non-key, breaks 3NF
roll_no, course -> grade      fine: the left side is a superkey

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sheet_dependencies.py

The only candidate key is (roll_no, course), so only those two attributes are prime. Three dependencies are partial: a student’s name depends on the roll number alone, and a course’s title and instructor on the course alone. instructor -> office is transitive: the key determines the course, the course determines the instructor, and the instructor, who is not a key of the sheet, determines the office. Only the grade is a fact about the whole key.

Checkpoint

Check: First normal form and lists in a cell · Prime attributes, partial and transitive dependencies

  1. Question 1 of 2 registrations keeps Kabir's courses as the string 'CS302,CS301'. Which query finds exactly the students who take CS302, with no lab course and no typo slipping through?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: WHERE course = 'CS302' on enrolments, after moving to one course per row

    A list in one column turns every question into a string search that the database cannot check. One course per row makes the question exact, and lets a foreign key refuse a code that does not exist.

  2. Question 2 of 2 In the enrolment sheet, keyed by (roll_no, course), what kind of dependency is course -> instructor?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Partial, because course is half of the key and instructor is non-prime

    The sheet's only key is (roll_no, course). course is a proper part of it and instructor is non-prime, so course -> instructor is partial and the sheet breaks 2NF, as the labelling program printed.

Second normal form moves facts about part of a key

A relation is in second normal form (2NF): First normal form with no partial dependency, so every non-prime attribute depends on whole candidate keys. when it is in 1NF and has no partial dependency: every non-prime attribute depends on whole candidate keys, never on a part of one. 2NF can only fail when some candidate key has two or more attributes, because a key of one attribute has no smaller part that could determine anything.

The fix moves each fact about part of a key into a table keyed by that part. Step through the split of the sheet:

Interactive · step by step The 2NF split of the sheet, one partial dependency at a time Step through the split. Each fact about part of the key moves to a table keyed by that part.
The 2NF split of the sheet, one partial dependency at a time, step 4 of 4. Showing sheet, students, courses, enrolments. In focus: enrolments.roll_no -> studentcourse -> title ...what is leftsheetkey: roll_no, coursestudentskey roll_no: studentcourseskey course: title, ...enrolmentskey roll_no, course: gradestill holds instructor -> office

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

Every step as text
  1. Two partial dependencies. In the sheet, roll_no determines student and course determines title and instructor, and through the instructor the office. Both left sides are halves of the key.
  2. Students. The fact about roll_no alone moves to students, keyed by roll_no. Each name is now stored once per student, not once per enrolment.
  3. Courses. Everything course determines moves to courses, keyed by course. The office comes along, because course determines it through the instructor.
  4. Enrolments. What is left is the key and the one fact about the whole key, the grade. Each new table is keyed by the left side of the facts it took, so the join is lossless.

The split gives three tables: students(roll_no, student), courses(course, title, instructor, office) and enrolments(roll_no, course, grade). Each new table is keyed by the left-hand side of the dependencies it took, so joining the tables on those keys returns the original rows and no others: the lossless split of the first lesson of the previous module.

Third normal form moves facts about a non-key

A relation is in third normal form (3NF): For every non-trivial functional dependency X -> A that holds in the relation, X is a superkey or A is a prime attribute. when, for every non-trivial dependency X -> A that holds in it, X is a superkey or A is prime. This one-line test is the form of the definition given by Carlo Zaniolo in A New Normal Form for the Design of Relational Database Schemata. It says the same as the older wording, “in 2NF, and no non-prime attribute depends transitively on a key”, without needing the word transitive.

The courses table of the 2NF split fails it: instructor is not a superkey of courses, and office is not prime. Splitting off offices(instructor, office) leaves four tables, each with one key that is the left-hand side of every dependency in it. The figure shows both steps, from the sheet to the four tables:

The enrolment sheet split into students, enrolments and courses for 2NF, then courses split again into courses and offices for 3NF.sheet (1NF)key: roll_no, coursestudent, title, instructor,office, grade2NF step: each fact aboutpart of the key moves toa table keyed by that partthe 2NF split3NF step: the fact abouta non-key moves to atable keyed by itthe 3NF splitstudents (BCNF)key: roll_noenrolments (BCNF)key: roll_no, coursecourses (2NF)key: courseinstructor -> officecourses (BCNF)key: courseoffices (BCNF)key: instructor

The enrolment sheet taken to 3NF in two steps

Text description of the diagram

The diagram shows the two steps that take the enrolment sheet from first to third normal form.

  1. The sheet has the key (roll_no, course) and the attributes student, title, instructor, office and grade. It is in 1NF only, because student depends on roll_no alone and title and instructor depend on course alone.
  2. The 2NF step moves each fact about part of the key into a table keyed by that part. It gives students (key roll_no, with student), enrolments (key roll_no and course, with grade) and courses (key course, with title, instructor and office). students and enrolments are in BCNF; courses is in 2NF.
  3. courses is not in 3NF, because instructor, which is not a key, determines office. The 3NF step moves that fact into offices (key instructor, with office) and leaves courses with title and instructor.

The result is four tables, each in BCNF: students, enrolments, courses and offices.

Each of the four tables is in 3NF, and also in the stricter Boyce-Codd normal form of the next lesson. The clause “or A is prime” is what separates 3NF from Boyce-Codd normal form, and it is deliberate: it lets every schema have a 3NF design that keeps each dependency checkable inside one table, which the stricter form cannot always do. The next lesson builds a relation where that difference decides the design.

Checkpoint

Check: Second normal form · Third normal form

  1. Question 1 of 2 lines(order_id, line_no, product_id, product_name, quantity) has order_id line_no -> product_id quantity and product_id -> product_name. What does the 2NF step change?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing, because no part of the key (order_id, line_no) determines anything on its own

    2NF removes facts about part of a key. lines has none, so it is already in 2NF; its one problem, product_id -> product_name, has a non-key on the left and is removed by the 3NF step.

  2. Question 2 of 2 After the 2NF split of the sheet, which table still breaks 3NF, and why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: courses, because instructor -> office has no superkey on its left and office is not prime

    The 2NF split moved instructor -> office into courses together with everything else course determines. The 3NF step splits off offices(instructor, office), and all four tables are then in 3NF.

A missed candidate key gives the wrong verdict

Each test above speaks of candidate keys, and the declared primary key is only one of them. Testing against the primary key alone is an easy way to misjudge a relation. Take the seating plan of an exam held in several sessions, seating(roll_no, session, seat, hall), with three rules:

  • a candidate has one seat in a session: roll_no session -> seat;
  • a seat holds one candidate in a session: session seat -> roll_no;
  • a seat label such as H2-17 names its hall: seat -> hall.

The exam office keys the table on (roll_no, session). But the closure of session and seat reaches every attribute too. Step through it:

Interactive · step by step The closure of session and seat finds a second key Step through the closure. Each step fires one dependency of the seating plan and adds what it determines.
The closure of session and seat finds a second key, step 5 of 5. Showing session, seat (prime now), roll_no, hall. In focus: hall.session seat -> roll_noseat -> hallsessionseatprime nowroll_nohallpartial dependency

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

Every step as text
  1. Start. The closure starts from the two attributes, session and seat. The declared key, roll_no with session, is not involved yet.
  2. Who sits there. A seat holds one candidate in a session, so session seat -> roll_no fires and adds roll_no.
  3. Which hall. The seat label names its hall, so seat -> hall fires and adds hall. The closure now holds all four attributes.
  4. A second key. Neither session nor seat alone reaches the rest, so (session, seat) is a candidate key, and seat is prime.
  5. The verdict moves. seat -> hall now has part of a key on its left and a non-prime attribute on its right. It is partial, so the seating plan is in 1NF, not 2NF.

The program judges the table twice, with the primary key alone and with every candidate key, in each of 14 languages:

One program · 14 languages The seating plan judged with one key, then with both O(n · 2ⁿ) closures for n attributes

Python 3.14 · main.py

# Sets of attributes are bit masks.
ROLL_NO, SESSION, SEAT, HALL = 1, 2, 4, 8
NAMES = ["roll_no", "session", "seat", "hall"]
ALL = ROLL_NO | SESSION | SEAT | HALL
FDS = [(ROLL_NO | SESSION, SEAT), (SESSION | SEAT, ROLL_NO), (SEAT, HALL)]


def closure(x: int) -> int:
    grew = True
    while grew:
        grew = False
        for lhs, rhs in FDS:
            if lhs & x == lhs and rhs & ~x:
                x |= rhs
                grew = True
    return x


def show(x: int) -> str:
    return " ".join(name for i, name in enumerate(NAMES) if x >> i & 1)


def judge(keys: list[int]) -> None:
    prime = 0
    for key in keys:
        prime |= key
    print("  keys: " + ", ".join("{" + show(key) + "}" for key in keys))
    for lhs, rhs in FDS:
        if closure(lhs) == ALL:
            verdict = "superkey, fine"
        elif rhs & prime:
            verdict = "prime, fine for 3NF"
        elif any(lhs & key == lhs for key in keys):
            verdict = "partial, breaks 2NF"
        else:
            verdict = "transitive, breaks 3NF"
        print(f"  {show(lhs)} -> {show(rhs)}: {verdict}")


# A key reaches ALL, and none of its attributes can be dropped.
bits = [1 << i for i in range(len(NAMES))]
keys = [
    x
    for x in range(1, ALL + 1)
    if closure(x) == ALL and all(closure(x & ~bit) != ALL for bit in bits if x & bit)
]
print("with the primary key only")
judge([ROLL_NO | SESSION])
print("with every candidate key")
judge(keys)

In Python: & binds tighter than == in Python, so lhs & x == lhs needs no parentheses. C, C++, Java, C#, JavaScript, TypeScript and PHP write the same test as (lhs & x) == lhs.

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

// Sets of attributes are bit masks.
const [ROLL_NO, SESSION, SEAT, HALL] = [1, 2, 4, 8];
const NAMES = ["roll_no", "session", "seat", "hall"];
const ALL = ROLL_NO | SESSION | SEAT | HALL;
const FDS = [
  [ROLL_NO | SESSION, SEAT],
  [SESSION | SEAT, ROLL_NO],
  [SEAT, HALL],
];

function closure(x) {
  let grew = true;
  while (grew) {
    grew = false;
    for (const [lhs, rhs] of FDS) {
      if ((lhs & x) === lhs && rhs & ~x) {
        x |= rhs;
        grew = true;
      }
    }
  }
  return x;
}

const show = (x) => NAMES.filter((_, i) => (x >> i) & 1).join(" ");

function judge(keys) {
  const prime = keys.reduce((p, key) => p | key, 0);
  console.log("  keys: " + keys.map((key) => `{${show(key)}}`).join(", "));
  for (const [lhs, rhs] of FDS) {
    let verdict;
    if (closure(lhs) === ALL) verdict = "superkey, fine";
    else if (rhs & prime) verdict = "prime, fine for 3NF";
    else if (keys.some((key) => (lhs & key) === lhs)) verdict = "partial, breaks 2NF";
    else verdict = "transitive, breaks 3NF";
    console.log(`  ${show(lhs)} -> ${show(rhs)}: ${verdict}`);
  }
}

// A key reaches ALL, and none of its attributes can be dropped.
const keys = [];
for (let x = 1; x <= ALL; x++) {
  let key = closure(x) === ALL;
  for (let bit = 1; bit <= x; bit <<= 1) key &&= (x & bit) === 0 || closure(x & ~bit) !== ALL;
  if (key) keys.push(x);
}
console.log("with the primary key only");
judge([ROLL_NO | SESSION]);
console.log("with every candidate key");
judge(keys);

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

import java.util.ArrayList;
import java.util.List;

public class Main {
  // Sets of attributes are bit masks.
  static final int ROLL_NO = 1;
  static final int SESSION = 2;
  static final int SEAT = 4;
  static final int HALL = 8;
  static final String[] NAMES = {"roll_no", "session", "seat", "hall"};
  static final int ALL = ROLL_NO | SESSION | SEAT | HALL;
  static final int[][] FDS = {{ROLL_NO | SESSION, SEAT}, {SESSION | SEAT, ROLL_NO}, {SEAT, HALL}};

  static int closure(int x) {
    boolean grew = true;
    while (grew) {
      grew = false;
      for (int[] fd : FDS) {
        if ((fd[0] & x) == fd[0] && (fd[1] & ~x) != 0) {
          x |= fd[1];
          grew = true;
        }
      }
    }
    return x;
  }

  static String show(int x) {
    List<String> out = new ArrayList<>();
    for (int i = 0; i < NAMES.length; i++) {
      if ((x >> i & 1) == 1) {
        out.add(NAMES[i]);
      }
    }
    return String.join(" ", out);
  }

  static void judge(List<Integer> keys) {
    int prime = 0;
    List<String> shown = new ArrayList<>();
    for (int key : keys) {
      prime |= key;
      shown.add("{" + show(key) + "}");
    }
    System.out.println("  keys: " + String.join(", ", shown));
    for (int[] fd : FDS) {
      int lhs = fd[0];
      int rhs = fd[1];
      String verdict;
      if (closure(lhs) == ALL) {
        verdict = "superkey, fine";
      } else if ((rhs & prime) != 0) {
        verdict = "prime, fine for 3NF";
      } else if (keys.stream().anyMatch(key -> (lhs & key) == lhs)) {
        verdict = "partial, breaks 2NF";
      } else {
        verdict = "transitive, breaks 3NF";
      }
      System.out.println("  " + show(lhs) + " -> " + show(rhs) + ": " + verdict);
    }
  }

  public static void main(String[] args) {
    // A key reaches ALL, and none of its attributes can be dropped.
    List<Integer> keys = new ArrayList<>();
    for (int x = 1; x <= ALL; x++) {
      boolean key = closure(x) == ALL;
      for (int bit = 1; bit <= x; bit <<= 1) {
        key = key && ((x & bit) == 0 || closure(x & ~bit) != ALL);
      }
      if (key) {
        keys.add(x);
      }
    }
    System.out.println("with the primary key only");
    judge(List.of(ROLL_NO | SESSION));
    System.out.println("with every candidate key");
    judge(keys);
  }
}

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <algorithm>
#include <array>
#include <print>
#include <string>
#include <utility>
#include <vector>

// Sets of attributes are bit masks.
enum : unsigned { ROLL_NO = 1, SESSION = 2, SEAT = 4, HALL = 8 };
constexpr unsigned ALL = ROLL_NO | SESSION | SEAT | HALL;
constexpr std::array<const char *, 4> NAMES = {"roll_no", "session", "seat",
                                               "hall"};
constexpr std::array<std::pair<unsigned, unsigned>, 3> FDS = {
    {{ROLL_NO | SESSION, SEAT}, {SESSION | SEAT, ROLL_NO}, {SEAT, HALL}}};

unsigned closure(unsigned x) {
  for (bool grew = true; grew;) {
    grew = false;
    for (const auto &[lhs, rhs] : FDS) {
      if ((lhs & x) == lhs && (rhs & ~x) != 0) {
        x |= rhs;
        grew = true;
      }
    }
  }
  return x;
}

std::string show(unsigned x) {
  std::string out;
  for (std::size_t i = 0; i < NAMES.size(); i++) {
    if (x >> i & 1) {
      out += (out.empty() ? "" : " ") + std::string(NAMES[i]);
    }
  }
  return out;
}

void judge(const std::vector<unsigned> &keys) {
  unsigned prime = 0;
  std::string line;
  for (unsigned key : keys) {
    prime |= key;
    line += (line.empty() ? "{" : ", {") + show(key) + "}";
  }
  std::println("  keys: {}", line);
  for (const auto &[lhs, rhs] : FDS) {
    const bool partial = std::ranges::any_of(
        keys, [lhs](unsigned key) { return (lhs & key) == lhs; });
    const char *verdict = closure(lhs) == ALL  ? "superkey, fine"
                          : (rhs & prime) != 0 ? "prime, fine for 3NF"
                          : partial            ? "partial, breaks 2NF"
                                               : "transitive, breaks 3NF";
    std::println("  {} -> {}: {}", show(lhs), show(rhs), verdict);
  }
}

int main() {
  // A key reaches ALL, and none of its attributes can be dropped.
  std::vector<unsigned> keys;
  for (unsigned x = 1; x <= ALL; x++) {
    bool key = closure(x) == ALL;
    for (unsigned bit = 1; bit <= x; bit <<= 1) {
      key = key && ((x & bit) == 0 || closure(x & ~bit) != ALL);
    }
    if (key) {
      keys.push_back(x);
    }
  }
  std::println("with the primary key only");
  judge({ROLL_NO | SESSION});
  std::println("with every candidate key");
  judge(keys);
}

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

// Sets of attributes are bit masks.
const int RollNo = 1;
const int Session = 2;
const int Seat = 4;
const int Hall = 8;
const int All = RollNo | Session | Seat | Hall;
string[] names = ["roll_no", "session", "seat", "hall"];
(int Lhs, int Rhs)[] fds = [(RollNo | Session, Seat), (Session | Seat, RollNo), (Seat, Hall)];

int Closure(int x)
{
    for (bool grew = true; grew;)
    {
        grew = false;
        foreach (var (lhs, rhs) in fds)
        {
            if ((lhs & x) == lhs && (rhs & ~x) != 0)
            {
                x |= rhs;
                grew = true;
            }
        }
    }
    return x;
}

string Show(int x) => string.Join(" ", names.Where((_, i) => (x >> i & 1) == 1));

void Judge(List<int> keys)
{
    int prime = keys.Aggregate(0, (p, key) => p | key);
    Console.WriteLine("  keys: " + string.Join(", ", keys.Select(key => "{" + Show(key) + "}")));
    foreach (var (lhs, rhs) in fds)
    {
        string verdict =
            Closure(lhs) == All ? "superkey, fine"
            : (rhs & prime) != 0 ? "prime, fine for 3NF"
            : keys.Any(key => (lhs & key) == lhs) ? "partial, breaks 2NF"
            : "transitive, breaks 3NF";
        Console.WriteLine($"  {Show(lhs)} -> {Show(rhs)}: {verdict}");
    }
}

// A key reaches All, and none of its attributes can be dropped.
var keys = new List<int>();
for (int x = 1; x <= All; x++)
{
    bool key = Closure(x) == All;
    for (int bit = 1; bit <= x; bit <<= 1)
    {
        key = key && ((x & bit) == 0 || Closure(x & ~bit) != All);
    }
    if (key)
    {
        keys.Add(x);
    }
}
Console.WriteLine("with the primary key only");
Judge([RollNo | Session]);
Console.WriteLine("with every candidate key");
Judge(keys);

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

#include <stdio.h>
#include <string.h>

// Sets of attributes are bit masks.
enum { ROLL_NO = 1, SESSION = 2, SEAT = 4, HALL = 8 };
enum { ALL = ROLL_NO | SESSION | SEAT | HALL };
static const char *const NAMES[] = {"roll_no", "session", "seat", "hall"};
static const unsigned FDS[][2] = {
    {ROLL_NO | SESSION, SEAT}, {SESSION | SEAT, ROLL_NO}, {SEAT, HALL}};

static unsigned closure(unsigned x) {
  for (int grew = 1; grew;) {
    grew = 0;
    for (int i = 0; i < 3; i++)
      if ((FDS[i][0] & x) == FDS[i][0] && (FDS[i][1] & ~x)) {
        x |= FDS[i][1];
        grew = 1;
      }
  }
  return x;
}

static char *show(unsigned x, char *out) {
  out[0] = '\0';
  for (int i = 0; i < 4; i++)
    if (x >> i & 1)
      strcat(strcat(out, out[0] ? " " : ""), NAMES[i]);
  return out;
}

static void judge(const unsigned *keys, int count) {
  char a[64], b[64];
  unsigned prime = 0;
  printf("  keys: ");
  for (int k = 0; k < count; k++) {
    prime |= keys[k];
    printf("%s{%s}", k ? ", " : "", show(keys[k], a));
  }
  printf("\n");
  for (int i = 0; i < 3; i++) {
    unsigned lhs = FDS[i][0], rhs = FDS[i][1];
    int partial = 0;
    for (int k = 0; k < count; k++)
      partial |= (lhs & keys[k]) == lhs;
    const char *verdict = closure(lhs) == ALL ? "superkey, fine"
                          : rhs & prime       ? "prime, fine for 3NF"
                          : partial           ? "partial, breaks 2NF"
                                              : "transitive, breaks 3NF";
    printf("  %s -> %s: %s\n", show(lhs, a), show(rhs, b), verdict);
  }
}

int main(void) {
  // A key reaches ALL, and none of its attributes can be dropped.
  unsigned keys[ALL];
  int count = 0;
  for (unsigned x = 1; x <= ALL; x++) {
    int key = closure(x) == ALL;
    for (unsigned bit = 1; bit <= x; bit <<= 1)
      key = key && ((x & bit) == 0 || closure(x & ~bit) != ALL);
    if (key)
      keys[count++] = x;
  }
  printf("with the primary key only\n");
  judge((const unsigned[]){ROLL_NO | SESSION}, 1);
  printf("with every candidate key\n");
  judge(keys, count);
  return 0;
}

Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main

PHP 8.5 · main.php

<?php

declare(strict_types=1);

// Sets of attributes are bit masks.
const ROLL_NO = 1;
const SESSION = 2;
const SEAT = 4;
const HALL = 8;
const ALL = ROLL_NO | SESSION | SEAT | HALL;
const NAMES = ['roll_no', 'session', 'seat', 'hall'];
const FDS = [[ROLL_NO | SESSION, SEAT], [SESSION | SEAT, ROLL_NO], [SEAT, HALL]];

function closure(int $x): int
{
    do {
        $grew = false;
        foreach (FDS as [$lhs, $rhs]) {
            if (($lhs & $x) === $lhs && ($rhs & ~$x) !== 0) {
                $x |= $rhs;
                $grew = true;
            }
        }
    } while ($grew);
    return $x;
}

function show(int $x): string
{
    $names = array_filter(NAMES, fn(int $i): bool => ($x >> $i & 1) === 1, ARRAY_FILTER_USE_KEY);
    return implode(' ', $names);
}

/** @param list<int> $keys */
function judge(array $keys): void
{
    $prime = array_reduce($keys, fn(int $p, int $key): int => $p | $key, 0);
    $shown = array_map(fn(int $key): string => '{' . show($key) . '}', $keys);
    echo '  keys: ' . implode(', ', $shown) . "\n";
    foreach (FDS as [$lhs, $rhs]) {
        $partial = array_filter($keys, fn(int $key): bool => ($lhs & $key) === $lhs) !== [];
        $verdict = match (true) {
            closure($lhs) === ALL => 'superkey, fine',
            ($rhs & $prime) !== 0 => 'prime, fine for 3NF',
            $partial => 'partial, breaks 2NF',
            default => 'transitive, breaks 3NF',
        };
        echo '  ' . show($lhs) . ' -> ' . show($rhs) . ": {$verdict}\n";
    }
}

// A key reaches ALL, and none of its attributes can be dropped.
$keys = [];
for ($x = 1; $x <= ALL; $x++) {
    $key = closure($x) === ALL;
    for ($bit = 1; $bit <= $x; $bit <<= 1) {
        $key = $key && (($x & $bit) === 0 || closure($x & ~$bit) !== ALL);
    }
    if ($key) {
        $keys[] = $x;
    }
}
echo "with the primary key only\n";
judge([ROLL_NO | SESSION]);
echo "with every candidate key\n";
judge($keys);

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

// Sets of attributes are bit masks.
const ROLL_NO: u32 = 1;
const SESSION: u32 = 2;
const SEAT: u32 = 4;
const HALL: u32 = 8;
const ALL: u32 = ROLL_NO | SESSION | SEAT | HALL;
const NAMES: [&str; 4] = ["roll_no", "session", "seat", "hall"];
const FDS: [(u32, u32); 3] = [
    (ROLL_NO | SESSION, SEAT),
    (SESSION | SEAT, ROLL_NO),
    (SEAT, HALL),
];

fn closure(mut x: u32) -> u32 {
    let mut grew = true;
    while grew {
        grew = false;
        for (lhs, rhs) in FDS {
            if lhs & x == lhs && rhs & !x != 0 {
                x |= rhs;
                grew = true;
            }
        }
    }
    x
}

fn show(x: u32) -> String {
    let names: Vec<&str> = (0..NAMES.len())
        .filter(|i| x >> i & 1 == 1)
        .map(|i| NAMES[i])
        .collect();
    names.join(" ")
}

fn judge(keys: &[u32]) {
    let prime = keys.iter().fold(0, |p, key| p | key);
    let shown: Vec<String> = keys
        .iter()
        .map(|&key| format!("{{{}}}", show(key)))
        .collect();
    println!("  keys: {}", shown.join(", "));
    for (lhs, rhs) in FDS {
        let verdict = if closure(lhs) == ALL {
            "superkey, fine"
        } else if rhs & prime != 0 {
            "prime, fine for 3NF"
        } else if keys.iter().any(|&key| lhs & key == lhs) {
            "partial, breaks 2NF"
        } else {
            "transitive, breaks 3NF"
        };
        println!("  {} -> {}: {verdict}", show(lhs), show(rhs));
    }
}

fn main() {
    // A key reaches ALL, and none of its attributes can be dropped.
    let keys: Vec<u32> = (1..=ALL)
        .filter(|&x| closure(x) == ALL)
        .filter(|&x| (0..NAMES.len()).all(|i| x >> i & 1 == 0 || closure(x & !(1 << i)) != ALL))
        .collect();
    println!("with the primary key only");
    judge(&[ROLL_NO | SESSION]);
    println!("with every candidate key");
    judge(&keys);
}

In Rust: Rust has no ~ operator: ! flips every bit of an integer, so x & !(1 << i) drops attribute i from the set.

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

// Sets of attributes are bit masks.
const [ROLL_NO, SESSION, SEAT, HALL] = [1, 2, 4, 8];
const NAMES = ["roll_no", "session", "seat", "hall"];
const ALL = ROLL_NO | SESSION | SEAT | HALL;
const FDS: [number, number][] = [
  [ROLL_NO | SESSION, SEAT],
  [SESSION | SEAT, ROLL_NO],
  [SEAT, HALL],
];

function closure(x: number): number {
  let grew = true;
  while (grew) {
    grew = false;
    for (const [lhs, rhs] of FDS) {
      if ((lhs & x) === lhs && (rhs & ~x) !== 0) {
        x |= rhs;
        grew = true;
      }
    }
  }
  return x;
}

const show = (x: number): string => NAMES.filter((_, i) => (x >> i) & 1).join(" ");

function judge(keys: number[]): void {
  const prime = keys.reduce((p, key) => p | key, 0);
  console.log("  keys: " + keys.map((key) => `{${show(key)}}`).join(", "));
  for (const [lhs, rhs] of FDS) {
    let verdict: string;
    if (closure(lhs) === ALL) verdict = "superkey, fine";
    else if ((rhs & prime) !== 0) verdict = "prime, fine for 3NF";
    else if (keys.some((key) => (lhs & key) === lhs)) verdict = "partial, breaks 2NF";
    else verdict = "transitive, breaks 3NF";
    console.log(`  ${show(lhs)} -> ${show(rhs)}: ${verdict}`);
  }
}

// A key reaches ALL, and none of its attributes can be dropped.
const keys: number[] = [];
for (let x = 1; x <= ALL; x++) {
  let key = closure(x) === ALL;
  for (let bit = 1; bit <= x; bit <<= 1) key &&= (x & bit) === 0 || closure(x & ~bit) !== ALL;
  if (key) keys.push(x);
}
console.log("with the primary key only");
judge([ROLL_NO | SESSION]);
console.log("with every candidate key");
judge(keys);

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import (
	"fmt"
	"strings"
)

// Sets of attributes are bit masks.
const RollNo, Session, Seat, Hall = 1, 2, 4, 8
const All = RollNo | Session | Seat | Hall

var names = []string{"roll_no", "session", "seat", "hall"}
var fds = [][2]int{{RollNo | Session, Seat}, {Session | Seat, RollNo}, {Seat, Hall}}

func closure(x int) int {
	for grew := true; grew; {
		grew = false
		for _, fd := range fds {
			if fd[0]&x == fd[0] && fd[1]&^x != 0 {
				x |= fd[1]
				grew = true
			}
		}
	}
	return x
}

func show(x int) string {
	var out []string
	for i, name := range names {
		if x>>i&1 == 1 {
			out = append(out, name)
		}
	}
	return strings.Join(out, " ")
}

func judge(keys []int) {
	prime, shown := 0, []string{}
	for _, key := range keys {
		prime |= key
		shown = append(shown, "{"+show(key)+"}")
	}
	fmt.Println("  keys: " + strings.Join(shown, ", "))
	for _, fd := range fds {
		lhs, rhs := fd[0], fd[1]
		partial := false
		for _, key := range keys {
			partial = partial || lhs&key == lhs
		}
		verdict := "transitive, breaks 3NF"
		switch {
		case closure(lhs) == All:
			verdict = "superkey, fine"
		case rhs&prime != 0:
			verdict = "prime, fine for 3NF"
		case partial:
			verdict = "partial, breaks 2NF"
		}
		fmt.Printf("  %s -> %s: %s\n", show(lhs), show(rhs), verdict)
	}
}

func main() {
	// A key reaches All, and none of its attributes can be dropped.
	var keys []int
	for x := 1; x <= All; x++ {
		key := closure(x) == All
		for bit := 1; bit <= x; bit <<= 1 {
			key = key && (x&bit == 0 || closure(x&^bit) != All)
		}
		if key {
			keys = append(keys, x)
		}
	}
	fmt.Println("with the primary key only")
	judge([]int{RollNo | Session})
	fmt.Println("with every candidate key")
	judge(keys)
}

In Go: x&^bit is Go's AND NOT, so closure(x&^bit) is the closure of x with one attribute dropped.

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

// Sets of attributes are bit masks.
let rollNo = 1
let session = 2
let seat = 4
let hall = 8
let all = rollNo | session | seat | hall
let names = ["roll_no", "session", "seat", "hall"]
let fds = [(rollNo | session, seat), (session | seat, rollNo), (seat, hall)]

func closure(_ start: Int) -> Int {
  var x = start
  var grew = true
  while grew {
    grew = false
    for (lhs, rhs) in fds where lhs & x == lhs && rhs & ~x != 0 {
      x |= rhs
      grew = true
    }
  }
  return x
}

func show(_ x: Int) -> String {
  names.indices.filter { x >> $0 & 1 == 1 }.map { names[$0] }.joined(separator: " ")
}

func judge(_ keys: [Int]) {
  let prime = keys.reduce(0, |)
  print("  keys: " + keys.map { "{" + show($0) + "}" }.joined(separator: ", "))
  for (lhs, rhs) in fds {
    let verdict: String
    if closure(lhs) == all {
      verdict = "superkey, fine"
    } else if rhs & prime != 0 {
      verdict = "prime, fine for 3NF"
    } else if keys.contains(where: { lhs & $0 == lhs }) {
      verdict = "partial, breaks 2NF"
    } else {
      verdict = "transitive, breaks 3NF"
    }
    print("  \(show(lhs)) -> \(show(rhs)): \(verdict)")
  }
}

// A key reaches all, and none of its attributes can be dropped.
let keys = (1...all).filter { x in
  closure(x) == all
    && names.indices.allSatisfy { x >> $0 & 1 == 0 || closure(x & ~(1 << $0)) != all }
}
print("with the primary key only")
judge([rollNo | session])
print("with every candidate key")
judge(keys)

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

# Sets of attributes are bit masks.
ROLL_NO = 1
SESSION = 2
SEAT = 4
HALL = 8
ALL = ROLL_NO | SESSION | SEAT | HALL
NAMES = %w[roll_no session seat hall].freeze
FDS = [[ROLL_NO | SESSION, SEAT], [SESSION | SEAT, ROLL_NO], [SEAT, HALL]].freeze

def closure(start)
  x = start
  loop do
    grown = FDS.reduce(x) { |acc, (lhs, rhs)| lhs & acc == lhs ? acc | rhs : acc }
    return x if grown == x

    x = grown
  end
end

def show(set)
  NAMES.each_index.select { |i| set[i] == 1 }.map { |i| NAMES[i] }.join(' ')
end

def verdict(lhs, rhs, keys, prime)
  return 'superkey, fine' if closure(lhs) == ALL
  return 'prime, fine for 3NF' if rhs & prime != 0
  return 'partial, breaks 2NF' if keys.any? { |key| lhs & key == lhs }

  'transitive, breaks 3NF'
end

def judge(keys)
  prime = keys.reduce(0, :|)
  puts "  keys: #{keys.map { |key| "{#{show(key)}}" }.join(', ')}"
  FDS.each { |lhs, rhs| puts "  #{show(lhs)} -> #{show(rhs)}: #{verdict(lhs, rhs, keys, prime)}" }
end

# A key reaches ALL, and none of its attributes can be dropped.
keys = (1..ALL).select do |x|
  closure(x) == ALL && NAMES.each_index.all? { |i| x[i].zero? || closure(x & ~(1 << i)) != ALL }
end
puts 'with the primary key only'
judge([ROLL_NO | SESSION])
puts 'with every candidate key'
judge(keys)

In Ruby: Integer#[] reads one bit, so x[i].zero? asks whether attribute i is missing from the set x.

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

// Sets of attributes are bit masks.
const val ROLL_NO = 1
const val SESSION = 2
const val SEAT = 4
const val HALL = 8
const val ALL = ROLL_NO or SESSION or SEAT or HALL
val names = listOf("roll_no", "session", "seat", "hall")
val fds = listOf(ROLL_NO or SESSION to SEAT, SESSION or SEAT to ROLL_NO, SEAT to HALL)

fun closure(start: Int): Int {
    var x = start
    var grew = true
    while (grew) {
        grew = false
        for ((lhs, rhs) in fds) {
            if (lhs and x == lhs && rhs and x.inv() != 0) {
                x = x or rhs
                grew = true
            }
        }
    }
    return x
}

fun show(x: Int) = names.indices.filter { x shr it and 1 == 1 }.joinToString(" ") { names[it] }

fun judge(keys: List<Int>) {
    val prime = keys.fold(0) { p, key -> p or key }
    println("  keys: " + keys.joinToString(", ") { "{" + show(it) + "}" })
    for ((lhs, rhs) in fds) {
        val verdict =
            when {
                closure(lhs) == ALL -> "superkey, fine"
                rhs and prime != 0 -> "prime, fine for 3NF"
                keys.any { lhs and it == lhs } -> "partial, breaks 2NF"
                else -> "transitive, breaks 3NF"
            }
        println("  ${show(lhs)} -> ${show(rhs)}: $verdict")
    }
}

fun main() {
    // A key reaches ALL, and none of its attributes can be dropped.
    val keys =
        (1..ALL).filter { x ->
            closure(x) == ALL &&
                names.indices.all { x shr it and 1 == 0 || closure(x and (1 shl it).inv()) != ALL }
        }
    println("with the primary key only")
    judge(listOf(ROLL_NO or SESSION))
    println("with every candidate key")
    judge(keys)
}

In Kotlin: and, or and shr are infix functions, which bind tighter than ==, so the mask tests need no parentheses; inv() is a plain call that flips every bit.

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

// Sets of attributes are bit masks.
const rollNo = 1, session = 2, seat = 4, hall = 8;
const all = rollNo | session | seat | hall;
const names = ['roll_no', 'session', 'seat', 'hall'];
const fds = [(rollNo | session, seat), (session | seat, rollNo), (seat, hall)];

int closure(int x) {
  for (var grew = true; grew;) {
    grew = false;
    for (final (lhs, rhs) in fds) {
      if (lhs & x == lhs && rhs & ~x != 0) {
        x |= rhs;
        grew = true;
      }
    }
  }
  return x;
}

String show(int x) => [
  for (var i = 0; i < names.length; i++)
    if (x >> i & 1 == 1) names[i],
].join(' ');

void judge(List<int> keys) {
  final prime = keys.fold(0, (p, key) => p | key);
  print('  keys: ${keys.map((key) => '{${show(key)}}').join(', ')}');
  for (final (lhs, rhs) in fds) {
    final verdict = closure(lhs) == all
        ? 'superkey, fine'
        : rhs & prime != 0
        ? 'prime, fine for 3NF'
        : keys.any((key) => lhs & key == lhs)
        ? 'partial, breaks 2NF'
        : 'transitive, breaks 3NF';
    print('  ${show(lhs)} -> ${show(rhs)}: $verdict');
  }
}

void main() {
  // A key reaches all, and none of its attributes can be dropped.
  final bits = [for (var i = 0; i < names.length; i++) 1 << i];
  final keys = [
    for (var x = 1; x <= all; x++)
      if (closure(x) == all &&
          bits.every((bit) => x & bit == 0 || closure(x & ~bit) != all))
        x,
  ];
  print('with the primary key only');
  judge([rollNo | session]);
  print('with every candidate key');
  judge(keys);
}

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

with the primary key only
  keys: {roll_no session}
  roll_no session -> seat: superkey, fine
  session seat -> roll_no: superkey, fine
  seat -> hall: transitive, breaks 3NF
with every candidate key
  keys: {roll_no session}, {session seat}
  roll_no session -> seat: superkey, fine
  session seat -> roll_no: superkey, fine
  seat -> hall: partial, breaks 2NF

With the primary key alone, seat looks like a non-key, so seat -> hall passes for a transitive dependency and the verdict is 2NF. But (session, seat) is a second candidate key, which makes seat part of a key. The hall, which belongs to no key, then depends on part of a key: a partial dependency, so the relation is not even in 2NF. The data shows the cost. The hall of a seat is stored again for every session in which the seat is used, the redundancy that 2NF exists to remove, and seats(seat, hall) is the table where that fact belongs. What went wrong was the first step: find every candidate key before running any test.

A second key can move a verdict up as well. When every attribute belongs to some key, as in R2 of the next section’s program, no dependency can break 3NF, whatever its left-hand side.

Key idea

Find every candidate key before any normal-form test: an attribute in a second key is prime, and that alone can turn a transitive dependency into a partial one.

Checkpoint

Check: Testing against every candidate key

  1. Question 1 of 2 Why is (session, seat) a candidate key of the seating plan?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Its closure holds all four attributes, and neither session nor seat alone reaches the rest

    A candidate key is a minimal set whose closure is the whole relation. The closure of (session, seat) is all four attributes, and session or seat alone is not enough, so it is a second candidate key besides (roll_no, session).

  2. Question 2 of 2 R(A, B, C, D) has the dependencies AB -> C, C -> D and D -> B. How many candidate keys does it have?

    Type a number.

    A hint

    A appears on no right-hand side, so it is in every key. Try A with each other attribute.

    Show the answer: Question 2 of 2

    Answer: 3 keys

    A appears on no right-hand side, so it is in every key, and A alone determines nothing else. AB, AC and AD each determine all four attributes (for example AC gives D, and D gives B), while BC, BD and CD never reach A. So the keys are AB, AC and AD, every attribute is prime, and R is in 3NF but not in BCNF.

The highest normal form in seven steps

The forms nest: BCNF implies 3NF, 3NF implies 2NF and 2NF implies 1NF. So one procedure finds the highest normal form of a relation:

  1. Check that every value is atomic. The rest assumes 1NF.
  2. Find every candidate key with attribute closures, and list the prime attributes.
  3. Split every right-hand side into single attributes and drop the trivial dependencies.
  4. BCNF holds when the left-hand side of every dependency is a superkey.
  5. Otherwise, 3NF holds when every dependency that breaks BCNF has a prime right-hand side.
  6. Otherwise, 2NF holds when no proper part of a candidate key has a non-prime attribute in its closure.
  7. Otherwise, the relation is in 1NF only.

Steps 4 and 5 may read only the dependencies written in F. If a derived dependency broke BCNF or 3NF, the closure computation that derives it would pass through a written dependency that breaks the same form. Step 6 is different: it needs the closures, because a written set of dependencies can hide a partial one. Step through the seven steps on a relation built to show the difference:

Interactive · stage by stage The highest normal form of R3, step by step Step through the seven steps on R3(A, B, C, D, E) with A -> C, AC -> E, AB -> D and CD -> AB.
The highest normal form of R3, step by step, step 7 of 7. Showing Every value atomic?, Every candidate key (AB, AD, CD; prime ABCD), Single right-hand sides, BCNF on the written ones (no: A -> C), 3NF on the written ones (no: AC -> E), 2NF on key-part closures (no: A -> E, not written), Highest normal form (1NF). In focus: Highest normal form.Every value atomic?step 1: assume 1NFEvery candidate keyAB, AD, CD; prime ABCDSingle right-hand sidesstep 3: CD -> A, CD -> BBCNF on the written onesno: A -> C3NF on the written onesno: AC -> E2NF on key-part closuresno: A -> E, not writtenHighest normal form1NF

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

Every step as text
  1. Atomic. Every value of R3 is a single value, so the procedure starts from 1NF.
  2. Three keys. AB, AD and CD each reach all five attributes, and no single attribute does. So A, B, C and D are prime, and E is the only non-prime attribute.
  3. Single right sides. CD -> AB becomes CD -> A and CD -> B. The other three dependencies already have one attribute on the right.
  4. BCNF fails. A -> C has a left side that is no superkey, since the closure of A is only A, C and E. BCNF fails.
  5. 3NF fails. A -> C passes for 3NF, because C is prime, but AC -> E does not. AC is no superkey and E is not prime.
  6. 2NF needs closures. No written dependency has part of a key on its left and E on its right. The closure of A, part of the key AB, is A, C and E, so A -> E holds and is partial.
  7. 1NF only. R3 is in 1NF only. A check that read only the written dependencies would have called it 2NF.

The program runs the same procedure on three relations written the way exam questions write them, with single letters. R1(A, B, C, D) has A -> BC and C -> D, the shape of the courses table; R2(A, B, C) has AB -> C and C -> B, so every attribute is prime; and R3 is the relation of the figure:

One program · 14 languages The highest normal form of three relations O((n + k) · 2ⁿ) closures, n attributes and k keys

Python 3.14 · main.py

# Sets of attributes are bit masks.
A, B, C, D, E = 1, 2, 4, 8, 16


def show(x: int) -> str:
    return "".join(letter for i, letter in enumerate("ABCDE") if x >> i & 1)


def closure(x: int, fds: list[tuple[int, int]]) -> int:
    grew = True
    while grew:
        grew = False
        for lhs, rhs in fds:
            if lhs & x == lhs and rhs & ~x:
                x |= rhs
                grew = True
    return x


def highest(fds: list[tuple[int, int]]) -> str:
    full = 0
    for lhs, rhs in fds:
        full |= lhs | rhs
    keys = [x for x in range(1, full + 1) if closure(x, fds) == full]
    keys = [
        x
        for x in keys
        if all(closure(x & ~b, fds) != full for b in (A, B, C, D, E) if x & b)
    ]
    prime = 0
    for key in keys:
        prime |= key
    if all(closure(lhs, fds) == full for lhs, rhs in fds if rhs & ~lhs):
        return "BCNF"
    if all(closure(lhs, fds) == full or not rhs & ~lhs & ~prime for lhs, rhs in fds):
        return "3NF"
    for key in keys:  # closures catch derived ones
        for part in range(1, key):
            extra = closure(part, fds) & ~part & ~prime
            if part & key == part and extra:
                return f"1NF, as {show(part)} -> {show(extra)} breaks 2NF"
    return "2NF"


print("R1:", highest([(A, B | C), (C, D)]))
print("R2:", highest([(A | B, C), (C, B)]))
print("R3:", highest([(A, C), (A | C, E), (A | B, D), (C | D, A | B)]))

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

// Sets of attributes are bit masks.
const [A, B, C, D, E] = [1, 2, 4, 8, 16];

const show = (x) => [..."ABCDE"].filter((_, i) => (x >> i) & 1).join("");

function closure(x, fds) {
  let grew = true;
  while (grew) {
    grew = false;
    for (const [lhs, rhs] of fds) {
      if ((lhs & x) === lhs && rhs & ~x) {
        x |= rhs;
        grew = true;
      }
    }
  }
  return x;
}

function highest(fds) {
  const full = fds.reduce((all, [lhs, rhs]) => all | lhs | rhs, 0);
  const keys = [];
  for (let x = 1; x <= full; x++) {
    let key = closure(x, fds) === full;
    for (const b of [A, B, C, D, E]) key &&= !(x & b) || closure(x & ~b, fds) !== full;
    if (key) keys.push(x);
  }
  const prime = keys.reduce((p, key) => p | key, 0);
  if (fds.every(([lhs, rhs]) => !(rhs & ~lhs) || closure(lhs, fds) === full)) return "BCNF";
  if (fds.every(([lhs, rhs]) => closure(lhs, fds) === full || !(rhs & ~lhs & ~prime))) {
    return "3NF";
  }
  for (const key of keys) {
    // closures catch derived ones
    for (let part = 1; part < key; part++) {
      const extra = closure(part, fds) & ~part & ~prime;
      if ((part & key) === part && extra) {
        return `1NF, as ${show(part)} -> ${show(extra)} breaks 2NF`;
      }
    }
  }
  return "2NF";
}

console.log(
  "R1:",
  highest([
    [A, B | C],
    [C, D],
  ]),
);
console.log(
  "R2:",
  highest([
    [A | B, C],
    [C, B],
  ]),
);
console.log(
  "R3:",
  highest([
    [A, C],
    [A | C, E],
    [A | B, D],
    [C | D, A | B],
  ]),
);

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

import java.util.ArrayList;
import java.util.List;

public class Main {
  // Sets of attributes are bit masks.
  static final int A = 1;
  static final int B = 2;
  static final int C = 4;
  static final int D = 8;
  static final int E = 16;

  record Fd(int lhs, int rhs) {}

  static String show(int x) {
    StringBuilder out = new StringBuilder();
    for (int i = 0; i < 5; i++) {
      if ((x >> i & 1) == 1) {
        out.append("ABCDE".charAt(i));
      }
    }
    return out.toString();
  }

  static int closure(int x, List<Fd> fds) {
    boolean grew = true;
    while (grew) {
      grew = false;
      for (Fd fd : fds) {
        if ((fd.lhs() & x) == fd.lhs() && (fd.rhs() & ~x) != 0) {
          x |= fd.rhs();
          grew = true;
        }
      }
    }
    return x;
  }

  static String highest(List<Fd> fds) {
    int full = fds.stream().mapToInt(fd -> fd.lhs() | fd.rhs()).reduce(0, (a, b) -> a | b);
    List<Integer> keys = new ArrayList<>();
    for (int x = 1; x <= full; x++) {
      boolean key = closure(x, fds) == full;
      for (int bit = A; bit <= E; bit <<= 1) {
        key = key && ((x & bit) == 0 || closure(x & ~bit, fds) != full);
      }
      if (key) {
        keys.add(x);
      }
    }
    int prime = keys.stream().reduce(0, (a, b) -> a | b);
    if (fds.stream()
        .allMatch(fd -> (fd.rhs() & ~fd.lhs()) == 0 || closure(fd.lhs(), fds) == full)) {
      return "BCNF";
    }
    if (fds.stream()
        .allMatch(fd -> closure(fd.lhs(), fds) == full || (fd.rhs() & ~fd.lhs() & ~prime) == 0)) {
      return "3NF";
    }
    for (int key : keys) { // closures catch derived ones
      for (int part = 1; part < key; part++) {
        int extra = closure(part, fds) & ~part & ~prime;
        if ((part & key) == part && extra != 0) {
          return "1NF, as " + show(part) + " -> " + show(extra) + " breaks 2NF";
        }
      }
    }
    return "2NF";
  }

  public static void main(String[] args) {
    System.out.println("R1: " + highest(List.of(new Fd(A, B | C), new Fd(C, D))));
    System.out.println("R2: " + highest(List.of(new Fd(A | B, C), new Fd(C, B))));
    System.out.println(
        "R3: "
            + highest(
                List.of(new Fd(A, C), new Fd(A | C, E), new Fd(A | B, D), new Fd(C | D, A | B))));
  }
}

In Java: full is the OR of every dependency's attributes, computed in one line with mapToInt and reduce on a stream.

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <algorithm>
#include <print>
#include <string>
#include <utility>
#include <vector>

// Sets of attributes are bit masks.
enum : unsigned { A = 1, B = 2, C = 4, D = 8, E = 16 };
using Fds = std::vector<std::pair<unsigned, unsigned>>;

std::string show(unsigned x) {
  std::string out;
  for (int i = 0; i < 5; i++) {
    if (x >> i & 1) {
      out += "ABCDE"[i];
    }
  }
  return out;
}

unsigned closure(unsigned x, const Fds &fds) {
  for (bool grew = true; grew;) {
    grew = false;
    for (const auto &[lhs, rhs] : fds) {
      if ((lhs & x) == lhs && (rhs & ~x) != 0) {
        x |= rhs;
        grew = true;
      }
    }
  }
  return x;
}

std::string highest(const Fds &fds) {
  unsigned full = 0, prime = 0;
  std::vector<unsigned> keys;
  for (const auto &[lhs, rhs] : fds) {
    full |= lhs | rhs;
  }
  for (unsigned x = 1; x <= full; x++) {
    bool key = closure(x, fds) == full;
    for (unsigned bit = A; bit <= E; bit <<= 1) {
      key = key && ((x & bit) == 0 || closure(x & ~bit, fds) != full);
    }
    if (key) {
      keys.push_back(x);
      prime |= x;
    }
  }
  auto holds = [&](auto test) { return std::ranges::all_of(fds, test); };
  if (holds([&](auto fd) {
        return (fd.second & ~fd.first) == 0 || closure(fd.first, fds) == full;
      })) {
    return "BCNF";
  }
  if (holds([&](auto fd) {
        return closure(fd.first, fds) == full ||
               (fd.second & ~fd.first & ~prime) == 0;
      })) {
    return "3NF";
  }
  for (unsigned key : keys) { // closures catch derived ones
    for (unsigned part = 1; part < key; part++) {
      const unsigned extra = closure(part, fds) & ~part & ~prime;
      if ((part & key) == part && extra != 0) {
        return "1NF, as " + show(part) + " -> " + show(extra) + " breaks 2NF";
      }
    }
  }
  return "2NF";
}

int main() {
  std::println("R1: {}", highest({{A, B | C}, {C, D}}));
  std::println("R2: {}", highest({{A | B, C}, {C, B}}));
  std::println("R3: {}",
               highest({{A, C}, {A | C, E}, {A | B, D}, {C | D, A | B}}));
}

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

// Sets of attributes are bit masks.
const int A = 1;
const int B = 2;
const int C = 4;
const int D = 8;
const int E = 16;

Console.WriteLine("R1: " + Highest([(A, B | C), (C, D)]));
Console.WriteLine("R2: " + Highest([(A | B, C), (C, B)]));
Console.WriteLine("R3: " + Highest([(A, C), (A | C, E), (A | B, D), (C | D, A | B)]));

static string Show(int x) => string.Concat("ABCDE".Where((_, i) => (x >> i & 1) == 1));

static int Closure(int x, (int Lhs, int Rhs)[] fds)
{
    for (bool grew = true; grew;)
    {
        grew = false;
        foreach (var (lhs, rhs) in fds)
        {
            if ((lhs & x) == lhs && (rhs & ~x) != 0)
            {
                x |= rhs;
                grew = true;
            }
        }
    }
    return x;
}

static string Highest((int Lhs, int Rhs)[] fds)
{
    int full = fds.Aggregate(0, (all, fd) => all | fd.Lhs | fd.Rhs);
    var keys = new List<int>();
    for (int x = 1; x <= full; x++)
    {
        bool key = Closure(x, fds) == full;
        for (int bit = A; bit <= E; bit <<= 1)
        {
            key = key && ((x & bit) == 0 || Closure(x & ~bit, fds) != full);
        }
        if (key)
        {
            keys.Add(x);
        }
    }
    int prime = keys.Aggregate(0, (p, key) => p | key);
    if (fds.All(fd => (fd.Rhs & ~fd.Lhs) == 0 || Closure(fd.Lhs, fds) == full))
    {
        return "BCNF";
    }
    if (fds.All(fd => Closure(fd.Lhs, fds) == full || (fd.Rhs & ~fd.Lhs & ~prime) == 0))
    {
        return "3NF";
    }
    foreach (int key in keys) // closures catch derived ones
    {
        for (int part = 1; part < key; part++)
        {
            int extra = Closure(part, fds) & ~part & ~prime;
            if ((part & key) == part && extra != 0)
            {
                return $"1NF, as {Show(part)} -> {Show(extra)} breaks 2NF";
            }
        }
    }
    return "2NF";
}

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

#include <stdio.h>

// Sets of attributes are bit masks.
enum { A = 1, B = 2, C = 4, D = 8, E = 16 };
typedef struct {
  unsigned lhs, rhs;
} Fd;

static char *show(unsigned x, char *out) {
  char *p = out;
  for (int i = 0; i < 5; i++)
    if (x >> i & 1)
      *p++ = "ABCDE"[i];
  *p = '\0';
  return out;
}

static unsigned closure(unsigned x, const Fd *fds, int n) {
  for (int grew = 1; grew;) {
    grew = 0;
    for (int i = 0; i < n; i++)
      if ((fds[i].lhs & x) == fds[i].lhs && (fds[i].rhs & ~x)) {
        x |= fds[i].rhs;
        grew = 1;
      }
  }
  return x;
}

static void highest(const char *name, const Fd *fds, int n) {
  char a[6], b[6];
  unsigned full = 0, prime = 0, keys[32];
  int count = 0, bcnf = 1, nf3 = 1;
  for (int i = 0; i < n; i++)
    full |= fds[i].lhs | fds[i].rhs;
  for (unsigned x = 1; x <= full; x++) {
    int key = closure(x, fds, n) == full;
    for (unsigned bit = A; bit <= E; bit <<= 1)
      key = key && (!(x & bit) || closure(x & ~bit, fds, n) != full);
    if (key)
      prime |= keys[count++] = x;
  }
  for (int i = 0; i < n; i++) {
    unsigned extra = fds[i].rhs & ~fds[i].lhs;
    if (closure(fds[i].lhs, fds, n) != full) {
      bcnf = bcnf && !extra;
      nf3 = nf3 && !(extra & ~prime);
    }
  }
  if (bcnf || nf3) {
    printf("%s: %s\n", name, bcnf ? "BCNF" : "3NF");
    return;
  }
  for (int k = 0; k < count; k++) // closures catch derived ones
    for (unsigned part = 1; part < keys[k]; part++) {
      unsigned extra = closure(part, fds, n) & ~part & ~prime;
      if ((part & keys[k]) == part && extra) {
        printf("%s: 1NF, as %s -> %s breaks 2NF\n", name, show(part, a),
               show(extra, b));
        return;
      }
    }
  printf("%s: 2NF\n", name);
}

int main(void) {
  highest("R1", (const Fd[]){{A, B | C}, {C, D}}, 2);
  highest("R2", (const Fd[]){{A | B, C}, {C, B}}, 2);
  highest("R3", (const Fd[]){{A, C}, {A | C, E}, {A | B, D}, {C | D, A | B}},
          4);
  return 0;
}

Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main

PHP 8.5 · main.php

<?php

declare(strict_types=1);

// Sets of attributes are bit masks.
const A = 1;
const B = 2;
const C = 4;
const D = 8;
const E = 16;

function show(int $x): string
{
    $keep = fn(int $i): bool => ($x >> $i & 1) === 1;
    return implode('', array_filter(str_split('ABCDE'), $keep, ARRAY_FILTER_USE_KEY));
}

/** @param list<array{int, int}> $fds */
function closure(int $x, array $fds): int
{
    do {
        $grew = false;
        foreach ($fds as [$lhs, $rhs]) {
            if (($lhs & $x) === $lhs && ($rhs & ~$x) !== 0) {
                $x |= $rhs;
                $grew = true;
            }
        }
    } while ($grew);
    return $x;
}

/** @param list<array{int, int}> $fds */
function highest(array $fds): string
{
    $full = array_reduce($fds, fn(int $all, array $fd): int => $all | $fd[0] | $fd[1], 0);
    $keys = [];
    for ($x = 1; $x <= $full; $x++) {
        $key = closure($x, $fds) === $full;
        for ($bit = A; $bit <= E; $bit <<= 1) {
            $key = $key && (($x & $bit) === 0 || closure($x & ~$bit, $fds) !== $full);
        }
        if ($key) {
            $keys[] = $x;
        }
    }
    $prime = array_reduce($keys, fn(int $p, int $key): int => $p | $key, 0);
    $all = fn(callable $test): bool => count(array_filter($fds, $test)) === count($fds);
    $superkey = fn(int $lhs): bool => closure($lhs, $fds) === $full;
    if ($all(fn(array $fd): bool => ($fd[1] & ~$fd[0]) === 0 || $superkey($fd[0]))) {
        return 'BCNF';
    }
    if ($all(fn(array $fd): bool => $superkey($fd[0]) || ($fd[1] & ~$fd[0] & ~$prime) === 0)) {
        return '3NF';
    }
    foreach ($keys as $key) { // closures catch derived ones
        for ($part = 1; $part < $key; $part++) {
            $extra = closure($part, $fds) & ~$part & ~$prime;
            if (($part & $key) === $part && $extra !== 0) {
                return '1NF, as ' . show($part) . ' -> ' . show($extra) . ' breaks 2NF';
            }
        }
    }
    return '2NF';
}

echo 'R1: ' . highest([[A, B | C], [C, D]]) . "\n";
echo 'R2: ' . highest([[A | B, C], [C, B]]) . "\n";
echo 'R3: ' . highest([[A, C], [A | C, E], [A | B, D], [C | D, A | B]]) . "\n";

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

// Sets of attributes are bit masks.
const A: u32 = 1;
const B: u32 = 2;
const C: u32 = 4;
const D: u32 = 8;
const E: u32 = 16;

fn show(x: u32) -> String {
    "ABCDE"
        .chars()
        .enumerate()
        .filter(|(i, _)| x >> i & 1 == 1)
        .map(|(_, c)| c)
        .collect()
}

fn closure(mut x: u32, fds: &[(u32, u32)]) -> u32 {
    let mut grew = true;
    while grew {
        grew = false;
        for &(lhs, rhs) in fds {
            if lhs & x == lhs && rhs & !x != 0 {
                x |= rhs;
                grew = true;
            }
        }
    }
    x
}

fn highest(fds: &[(u32, u32)]) -> String {
    let full = fds.iter().fold(0, |all, &(lhs, rhs)| all | lhs | rhs);
    let keys: Vec<u32> = (1..=full)
        .filter(|&x| closure(x, fds) == full)
        .filter(|&x| {
            [A, B, C, D, E]
                .iter()
                .all(|&b| x & b == 0 || closure(x & !b, fds) != full)
        })
        .collect();
    let prime = keys.iter().fold(0, |p, key| p | key);
    if fds
        .iter()
        .all(|&(lhs, rhs)| rhs & !lhs == 0 || closure(lhs, fds) == full)
    {
        return "BCNF".to_string();
    }
    if fds
        .iter()
        .all(|&(lhs, rhs)| closure(lhs, fds) == full || rhs & !lhs & !prime == 0)
    {
        return "3NF".to_string();
    }
    for &key in &keys {
        // closures catch derived ones
        for part in 1..key {
            let extra = closure(part, fds) & !part & !prime;
            if part & key == part && extra != 0 {
                return format!("1NF, as {} -> {} breaks 2NF", show(part), show(extra));
            }
        }
    }
    "2NF".to_string()
}

fn main() {
    println!("R1: {}", highest(&[(A, B | C), (C, D)]));
    println!("R2: {}", highest(&[(A | B, C), (C, B)]));
    println!(
        "R3: {}",
        highest(&[(A, C), (A | C, E), (A | B, D), (C | D, A | B)])
    );
}

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

// Sets of attributes are bit masks.
const [A, B, C, D, E] = [1, 2, 4, 8, 16];
type Fd = [number, number];

const show = (x: number): string => [..."ABCDE"].filter((_, i) => (x >> i) & 1).join("");

function closure(x: number, fds: Fd[]): number {
  let grew = true;
  while (grew) {
    grew = false;
    for (const [lhs, rhs] of fds) {
      if ((lhs & x) === lhs && (rhs & ~x) !== 0) {
        x |= rhs;
        grew = true;
      }
    }
  }
  return x;
}

function highest(fds: Fd[]): string {
  const full = fds.reduce((all, [lhs, rhs]) => all | lhs | rhs, 0);
  const keys: number[] = [];
  for (let x = 1; x <= full; x++) {
    let key = closure(x, fds) === full;
    for (const b of [A, B, C, D, E]) key &&= (x & b) === 0 || closure(x & ~b, fds) !== full;
    if (key) keys.push(x);
  }
  const prime = keys.reduce((p, key) => p | key, 0);
  if (fds.every(([lhs, rhs]) => (rhs & ~lhs) === 0 || closure(lhs, fds) === full)) return "BCNF";
  if (fds.every(([lhs, rhs]) => closure(lhs, fds) === full || (rhs & ~lhs & ~prime) === 0)) {
    return "3NF";
  }
  for (const key of keys) {
    // closures catch derived ones
    for (let part = 1; part < key; part++) {
      const extra = closure(part, fds) & ~part & ~prime;
      if ((part & key) === part && extra !== 0) {
        return `1NF, as ${show(part)} -> ${show(extra)} breaks 2NF`;
      }
    }
  }
  return "2NF";
}

console.log(
  "R1:",
  highest([
    [A, B | C],
    [C, D],
  ]),
);
console.log(
  "R2:",
  highest([
    [A | B, C],
    [C, B],
  ]),
);
console.log(
  "R3:",
  highest([
    [A, C],
    [A | C, E],
    [A | B, D],
    [C | D, A | B],
  ]),
);

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import "fmt"

// Sets of attributes are bit masks.
const A, B, C, D, E = 1, 2, 4, 8, 16

func show(x int) string {
	out := ""
	for i, letter := range "ABCDE" {
		if x>>i&1 == 1 {
			out += string(letter)
		}
	}
	return out
}

func closure(x int, fds [][2]int) int {
	for grew := true; grew; {
		grew = false
		for _, fd := range fds {
			if fd[0]&x == fd[0] && fd[1]&^x != 0 {
				x |= fd[1]
				grew = true
			}
		}
	}
	return x
}

func highest(fds [][2]int) string {
	full, prime, bcnf, nf3 := 0, 0, true, true
	for _, fd := range fds {
		full |= fd[0] | fd[1]
	}
	var keys []int
	for x := 1; x <= full; x++ {
		key := closure(x, fds) == full
		for bit := A; bit <= E; bit <<= 1 {
			key = key && (x&bit == 0 || closure(x&^bit, fds) != full)
		}
		if key {
			keys = append(keys, x)
			prime |= x
		}
	}
	for _, fd := range fds {
		if closure(fd[0], fds) != full {
			bcnf = bcnf && fd[1]&^fd[0] == 0
			nf3 = nf3 && fd[1]&^fd[0]&^prime == 0
		}
	}
	if bcnf {
		return "BCNF"
	}
	if nf3 {
		return "3NF"
	}
	for _, key := range keys { // closures catch derived ones
		for part := 1; part < key; part++ {
			extra := closure(part, fds) &^ part &^ prime
			if part&key == part && extra != 0 {
				return fmt.Sprintf("1NF, as %s -> %s breaks 2NF", show(part), show(extra))
			}
		}
	}
	return "2NF"
}

func main() {
	fmt.Println("R1:", highest([][2]int{{A, B | C}, {C, D}}))
	fmt.Println("R2:", highest([][2]int{{A | B, C}, {C, B}}))
	fmt.Println("R3:", highest([][2]int{{A, C}, {A | C, E}, {A | B, D}, {C | D, A | B}}))
}

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

// Sets of attributes are bit masks.
let a = 1
let b = 2
let c = 4
let d = 8
let e = 16

func show(_ x: Int) -> String {
  String("ABCDE".enumerated().filter { x >> $0.offset & 1 == 1 }.map(\.element))
}

func closure(_ start: Int, _ fds: [(Int, Int)]) -> Int {
  var x = start
  var grew = true
  while grew {
    grew = false
    for (lhs, rhs) in fds where lhs & x == lhs && rhs & ~x != 0 {
      x |= rhs
      grew = true
    }
  }
  return x
}

func highest(_ fds: [(Int, Int)]) -> String {
  let full = fds.reduce(0) { $0 | $1.0 | $1.1 }
  let keys = (1...full).filter { x in
    closure(x, fds) == full
      && [a, b, c, d, e].allSatisfy { x & $0 == 0 || closure(x & ~$0, fds) != full }
  }
  let prime = keys.reduce(0, |)
  if fds.allSatisfy({ $0.1 & ~$0.0 == 0 || closure($0.0, fds) == full }) {
    return "BCNF"
  }
  if fds.allSatisfy({ closure($0.0, fds) == full || $0.1 & ~$0.0 & ~prime == 0 }) {
    return "3NF"
  }
  for key in keys {  // closures catch derived ones
    for part in 1..<key {
      let extra = closure(part, fds) & ~part & ~prime
      if part & key == part && extra != 0 {
        return "1NF, as \(show(part)) -> \(show(extra)) breaks 2NF"
      }
    }
  }
  return "2NF"
}

print("R1:", highest([(a, b | c), (c, d)]))
print("R2:", highest([(a | b, c), (c, b)]))
print("R3:", highest([(a, c), (a | c, e), (a | b, d), (c | d, a | b)]))

In Swift: keys.reduce(0, |) passes the | operator itself as the function that combines the keys into the prime attributes.

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

# Sets of attributes are bit masks.
A = 1
B = 2
C = 4
D = 8
E = 16

def show(set)
  'ABCDE'.chars.select.with_index { |_, i| set[i] == 1 }.join
end

def closure(start, fds)
  x = start
  loop do
    grown = fds.reduce(x) { |acc, (lhs, rhs)| lhs & acc == lhs ? acc | rhs : acc }
    return x if grown == x

    x = grown
  end
end

def keys_of(fds, full)
  (1..full).select do |x|
    closure(x, fds) == full &&
      [A, B, C, D, E].all? { |bit| (x & bit).zero? || closure(x & ~bit, fds) != full }
  end
end

# closures catch derived ones
def partial(fds, keys, prime)
  keys.each do |key|
    (1...key).each do |part|
      extra = closure(part, fds) & ~part & ~prime
      return "#{show(part)} -> #{show(extra)}" if part & key == part && extra != 0
    end
  end
  nil
end

def written_form(fds, full, prime)
  return 'BCNF' if fds.all? { |lhs, rhs| (rhs & ~lhs).zero? || closure(lhs, fds) == full }

  '3NF' if fds.all? { |lhs, rhs| closure(lhs, fds) == full || (rhs & ~lhs & ~prime).zero? }
end

def highest(fds)
  full = fds.reduce(0) { |all, (lhs, rhs)| all | lhs | rhs }
  keys = keys_of(fds, full)
  prime = keys.reduce(0, :|)
  form = written_form(fds, full, prime)
  return form if form

  found = partial(fds, keys, prime)
  found ? "1NF, as #{found} breaks 2NF" : '2NF'
end

puts "R1: #{highest([[A, B | C], [C, D]])}"
puts "R2: #{highest([[A | B, C], [C, B]])}"
puts "R3: #{highest([[A, C], [A | C, E], [A | B, D], [C | D, A | B]])}"

In Ruby: keys.reduce(0, :|) does the same with the symbol of the | method, so the prime attributes take one call.

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

// Sets of attributes are bit masks.
const val A = 1
const val B = 2
const val C = 4
const val D = 8
const val E = 16

fun show(x: Int) = "ABCDE".filterIndexed { i, _ -> x shr i and 1 == 1 }

fun closure(
    start: Int,
    fds: List<Pair<Int, Int>>,
): Int {
    var x = start
    var grew = true
    while (grew) {
        grew = false
        for ((lhs, rhs) in fds) {
            if (lhs and x == lhs && rhs and x.inv() != 0) {
                x = x or rhs
                grew = true
            }
        }
    }
    return x
}

fun highest(fds: List<Pair<Int, Int>>): String {
    val full = fds.fold(0) { all, (lhs, rhs) -> all or lhs or rhs }
    val keys =
        (1..full).filter { x ->
            closure(x, fds) == full &&
                listOf(A, B, C, D, E).all { x and it == 0 || closure(x and it.inv(), fds) != full }
        }
    val prime = keys.fold(0) { p, key -> p or key }
    if (fds.all { (lhs, rhs) -> rhs and lhs.inv() == 0 || closure(lhs, fds) == full }) return "BCNF"
    if (fds.all { (lhs, rhs) -> closure(lhs, fds) == full || rhs and (lhs or prime).inv() == 0 }) {
        return "3NF"
    }
    for (key in keys) { // closures catch derived ones
        for (part in 1 until key) {
            val extra = closure(part, fds) and part.inv() and prime.inv()
            if (part and key == part && extra != 0) {
                return "1NF, as ${show(part)} -> ${show(extra)} breaks 2NF"
            }
        }
    }
    return "2NF"
}

fun main() {
    println("R1: " + highest(listOf(A to (B or C), C to D)))
    println("R2: " + highest(listOf((A or B) to C, C to B)))
    println("R3: " + highest(listOf(A to C, (A or C) to E, (A or B) to D, (C or D) to (A or B))))
}

In Kotlin: "ABCDE".filterIndexed keeps the letters whose bit is set, so show needs no loop of its own.

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

// Sets of attributes are bit masks.
const a = 1, b = 2, c = 4, d = 8, e = 16;

String show(int x) => [
  for (var i = 0; i < 5; i++)
    if (x >> i & 1 == 1) 'ABCDE'[i],
].join();

int closure(int x, List<(int, int)> fds) {
  for (var grew = true; grew;) {
    grew = false;
    for (final (lhs, rhs) in fds) {
      if (lhs & x == lhs && rhs & ~x != 0) {
        x |= rhs;
        grew = true;
      }
    }
  }
  return x;
}

String highest(List<(int, int)> fds) {
  final full = fds.fold(0, (all, fd) => all | fd.$1 | fd.$2);
  final keys = [
    for (var x = 1; x <= full; x++)
      if (closure(x, fds) == full &&
          [
            a,
            b,
            c,
            d,
            e,
          ].every((bit) => x & bit == 0 || closure(x & ~bit, fds) != full))
        x,
  ];
  final prime = keys.fold(0, (p, key) => p | key);
  if (fds.every((fd) => fd.$2 & ~fd.$1 == 0 || closure(fd.$1, fds) == full)) {
    return 'BCNF';
  }
  if (fds.every(
    (fd) => closure(fd.$1, fds) == full || fd.$2 & ~fd.$1 & ~prime == 0,
  )) {
    return '3NF';
  }
  for (final key in keys) {
    // closures catch derived ones
    for (var part = 1; part < key; part++) {
      final extra = closure(part, fds) & ~part & ~prime;
      if (part & key == part && extra != 0) {
        return '1NF, as ${show(part)} -> ${show(extra)} breaks 2NF';
      }
    }
  }
  return '2NF';
}

void main() {
  print('R1: ${highest([(a, b | c), (c, d)])}');
  print('R2: ${highest([(a | b, c), (c, b)])}');
  print('R3: ${highest([(a, c), (a | c, e), (a | b, d), (c | d, a | b)])}');
}

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

R1: 2NF
R2: 3NF
R3: 1NF, as A -> E breaks 2NF

R1 reaches 2NF and stops there, because C -> D has a non-key on its left and a non-prime attribute on its right. R2 is in 3NF but not in BCNF: C -> B has a left side that is no superkey, but B is prime. R3 is the trap. No written dependency has part of a key on its left and a non-prime attribute on its right, yet A -> C and AC -> E together give A -> E, and A is part of the key AB. A check that reads only F would call R3 2NF; the closure of A shows that it is in 1NF.

GATE CS

The GATE 2027 syllabus for computer science lists normal forms among its database topics (GATE 2027 CS syllabus). An item that asks for the highest normal form of a relation is answered by exactly this procedure, and its second step, finding every candidate key, is the one to do with the most care.

To see a split as tables and their links, paste the CREATE TABLE statements of the four tables into an ER diagram.

SQL to ER Diagram Draw students, enrolments, courses and offices from their CREATE TABLE statements.

Checkpoint

Check: The highest normal form procedure

  1. Question 1 of 2 R3(A, B, C, D, E) has A -> C, AC -> E, AB -> D and CD -> AB, and its keys are AB, AD and CD. Which verdict does a 2NF check that reads only the written dependencies give?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 2NF, because no written dependency has part of a key on its left and E on its right

    The written dependencies show no partial one, so a check that reads only F would report 2NF. The closure of A is A, C and E, and A is part of the key AB, so A -> E is partial and R3 is in 1NF only, as the program printed.

  2. Question 2 of 2 Which step of the procedure decides the most verdicts when it is done carelessly?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Finding every candidate key and the prime attributes

    The 2NF and 3NF tests both read the prime attributes, so a missed candidate key can move a verdict down, as in the seating plan, or up, as in R2. The GATE note in the lesson makes the same point for exam questions.

Practise: name the highest normal form

The exercise lists three relations from a college: a hostel, exam results and sports squads. For each, give the highest normal form and a dependency that keeps it from the next one. The sample tests run the procedure of this lesson on your answers in the browser:

Exercise · Medium · Python

Name the highest normal form and the dependency that stops the next

answers.py lists three relations with their dependencies. For each one, replace the placeholder with a pair:

  1. the highest normal form of the relation: "1NF", "2NF", "3NF" or "BCNF";
  2. a dependency that keeps it from the next normal form, as a string such as "room -> block" (attribute names separated by spaces), or None when the relation is in BCNF.

Any dependency that follows from the listed ones and blocks the next form is accepted, so "room -> warden" counts as well as "room -> block" when it blocks the same step. Each relation is in first normal form: every value is atomic. The sample tests compute the answer with the method of the lesson and check both parts of each pair.

Starter code · answers.py

"""For each relation: its highest normal form ("1NF", "2NF", "3NF" or "BCNF"), and a dependency that keeps it from
the next normal form, written "left -> right" with attribute names separated by spaces (None for BCNF)."""

# hostel(roll_no, room, block, warden)
#   roll_no -> room;  room -> block;  block -> warden
HOSTEL = ("BCNF", None)

# results(roll_no, paper, attempt, marks, paper_title)
#   roll_no paper attempt -> marks;  paper -> paper_title
RESULTS = ("BCNF", None)

# squads(player, sport, coach)
#   player sport -> coach;  coach -> sport
SQUADS = ("BCNF", None)
The sample tests · test_answers.py
from itertools import combinations

from answers import HOSTEL, RESULTS, SQUADS

ORDER = ["1NF", "2NF", "3NF", "BCNF"]
NEXT = {"1NF": "2NF", "2NF": "3NF", "3NF": "BCNF"}


def closure(attrs, fds):
    result = set(attrs)
    grew = True
    while grew:
        grew = False
        for lhs, rhs in fds:
            if lhs <= result and not rhs <= result:
                result |= rhs
                grew = True
    return result


def parse(text):
    lhs, rhs = text.split("->")
    return set(lhs.split()), set(rhs.split())


def keys_of(relation, fds):
    keys = []
    for size in range(1, len(relation) + 1):
        for combo in combinations(relation, size):
            if not any(k <= set(combo) for k in keys) and closure(combo, fds) >= set(relation):
                keys.append(set(combo))
    return keys


def blocks(level, relation, fds, lhs, rhs):
    """Whether lhs -> rhs follows from fds and breaks the normal form after `level`."""
    keys = keys_of(relation, fds)
    prime = set().union(*keys)
    if not rhs or rhs & lhs or not rhs <= closure(lhs, fds):
        return False
    superkey = closure(lhs, fds) >= set(relation)
    if level == "3NF":                       # it must break BCNF
        return not superkey
    if level == "2NF":                       # it must break 3NF
        return not superkey and not rhs & prime
    if level == "1NF":                       # it must break 2NF: part of a key, non-prime right side
        return any(lhs < k for k in keys) and not rhs & prime
    return False


def highest(relation, fds):
    keys = keys_of(relation, fds)
    prime = set().union(*keys)
    single = [(lhs, {a}) for lhs, rhs in fds for a in rhs - lhs]
    if all(closure(lhs, fds) >= set(relation) for lhs, _ in single):
        return "BCNF"
    if all(closure(lhs, fds) >= set(relation) or rhs <= prime for lhs, rhs in single):
        return "3NF"
    for k in keys:
        for size in range(1, len(k)):
            for part in combinations(sorted(k), size):
                if closure(part, fds) - set(part) - prime:
                    return "1NF"
    return "2NF"


def check(answer, relation, texts):
    fds = [parse(t) for t in texts]
    level, witness = answer
    assert level in ORDER, f"{level!r} is not one of {ORDER}"
    want = highest(relation.split(), fds)
    assert level == want, f"{level} is not the highest normal form of this relation"
    if level == "BCNF":
        assert witness is None, "a relation in BCNF has no dependency that blocks the next form"
        return
    assert isinstance(witness, str) and "->" in witness, 'name the blocking dependency as "left -> right"'
    lhs, rhs = parse(witness)
    assert blocks(level, relation.split(), fds, lhs, rhs), f"{witness} does not keep the relation out of {NEXT[level]}"


def test_hostel():
    """hostel(roll_no, room, block, warden)"""
    check(HOSTEL, "roll_no room block warden", ["roll_no -> room", "room -> block", "block -> warden"])


def test_results():
    """results(roll_no, paper, attempt, marks, paper_title)"""
    check(RESULTS, "roll_no paper attempt marks paper_title", ["roll_no paper attempt -> marks", "paper -> paper_title"])


def test_squads():
    """squads(player, sport, coach)"""
    check(SQUADS, "player sport coach", ["player sport -> coach", "coach -> sport"])
A hint

Start every item by finding all its candidate keys, then mark the prime attributes. In squads, ask whether {player, coach} determines sport: if it does, it is a second key, and sport is prime. Then test BCNF, 3NF and 2NF in that order, and the first test that fails gives you both the answer and the dependency to name.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 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 covers the six ideas of the lesson, from atomic values to the procedure, and opens where your answers have been weakest so far:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 When does a date column count as atomic?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: As long as no query needs its parts, such as the month on its own

    1NF asks for one value from the domain in each cell, and what counts as one value depends on use. A date or a phone number is atomic as long as nothing queries its parts.

  2. Question 2 of 12 posts keeps each post's tags as one string such as 'go,golang'. What happens to a rule that a post may carry each tag only once?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: It cannot be declared, because a constraint sees one string per post, not the tags inside it

    Constraints work on whole values. With one tag per row, in post_tags(post_id, tag), the primary key on both columns states the rule; inside a list nothing can.

  3. Question 3 of 12 R(A, B, C, D) has the candidate keys AB and AD. Which attributes are prime?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: A, B and D

    Prime means part of some candidate key. The keys AB and AD contain A, B and D, so those three are prime and C is the only non-prime attribute.

  4. Question 4 of 12 orders(order_id, customer, city) has order_id -> customer and customer -> city. What kind of dependency is customer -> city?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: Transitive, because customer is no key and city depends on order_id only through it

    customer is neither a superkey nor part of a candidate key, and city is non-prime, so customer -> city is transitive. orders is in 2NF and breaks 3NF; customers(customer, city) is where the fact belongs.

  5. Question 5 of 12 Why can 2NF fail only when some candidate key has two or more attributes?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: Because a key of one attribute has no smaller part that could determine anything

    2NF forbids non-prime attributes that depend on part of a candidate key. A key of one attribute has no proper part with something in it, so the test can only fail when a key has two or more attributes.

  6. Question 6 of 12 The 2NF split of the seating plan moves seat -> hall out of seating(roll_no, session, seat, hall). Where does it go?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Into seats(seat, hall), keyed by seat

    A fact about part of a key goes into a table keyed by that part. seat determines hall, so seats(seat, hall) holds each hall once per seat, and seating(roll_no, session, seat) keeps the rest. Joining them on seat, the key of seats, gives back the original rows.

  7. Question 7 of 12 Which non-trivial dependencies X -> A does the 3NF test accept?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Those whose left side X is a superkey, or whose right side A is prime

    3NF accepts X -> A when X is a superkey or A is prime. Without the second clause the test is BCNF's, which is stricter.

  8. Question 8 of 12 lines(order_id, line_no, product_id, product_name, quantity) has the dependencies order_id line_no -> product_id quantity and product_id -> product_name. What is its highest normal form?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: 2NF

    The only candidate key is (order_id, line_no). Neither order_id nor line_no determines anything on its own, so there is no partial dependency and the table is in 2NF. product_id is not a key, and product_name is not prime, so product_id -> product_name breaks 3NF.

  9. Question 9 of 12 What can finding a second candidate key do to a relation's verdict?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: Lower it or raise it, because it changes which attributes are prime

    A second key adds prime attributes. That can turn a transitive dependency into a partial one, lowering the verdict, or make a right-hand side prime, raising it to 3NF.

  10. Question 10 of 12 Which of these statements are true for a relation in first normal form?

    Choose every answer that is right.

    Show the answer: Question 10 of 12

    Answer:

    • If every candidate key has a single attribute, the relation is in 2NF.
    • If every attribute is prime, the relation is in 3NF.

    A key of one attribute has no smaller part to depend on, so no partial dependency can exist. When every attribute is prime, the right-hand side of every dependency is prime, so the 3NF test passes. 3NF does not imply BCNF, and a second candidate key can turn a dependency that looks transitive into a partial one, as in the seating plan.

  11. Question 11 of 12 Which test of the procedure must use closures, not only the dependencies written in F?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: The 2NF test

    Steps 4 and 5 may read F as written, because a derived violation of BCNF or 3NF always passes through a written one. Step 6 may not: R3 shows a partial dependency, A -> E, that no written dependency states.

  12. Question 12 of 12 For R3(A, B, C, D, E) with A -> C, AC -> E, AB -> D and CD -> AB, why does a check that reads only the written dependencies wrongly call R3 2NF?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: Because A -> E is not written; it follows from A -> C and AC -> E, and A is part of the key AB

    No written dependency has part of a key on its left and a non-prime attribute on its right. The closure of A is A, C and E, so A -> E holds, A is part of the key AB, and E belongs to no key: a partial dependency. Computing the closure of every proper part of every key finds it.

Interview questions

Fresher to mid · What is the difference between a partial and a transitive dependency, and which normal form does each break?

Both are dependencies X -> A in which A is non-prime, that is, part of no candidate key, and X is not a superkey. They differ in X. In a partial dependency, X is a proper part of a candidate key: in enrolments keyed by (roll_no, course), roll_no -> student says the name depends on half the key. A partial dependency breaks 2NF. In a transitive dependency, X is contained in no candidate key: in courses keyed by course, instructor -> office says the office depends on the key only through the instructor. A transitive dependency breaks 3NF but not 2NF. Both are removed the same way, by moving A into a table keyed by X, and both must be judged against every candidate key, because an attribute that looks like a non-key may belong to a second key.

Two follow-ups an interviewer may add to that answer are below, each with four replies to weigh before you look at the explanation:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 An interviewer says that every relation whose candidate keys each have one attribute is in 2NF. Is that right?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Yes, because a key of one attribute has no proper part that could determine anything

    2NF fails only through a partial dependency, X -> A with X a proper part of a candidate key. With keys of one attribute there is no such X, so the relation is in 2NF, whatever else it breaks.

  2. Question 2 of 2 Every attribute of a relation belongs to some candidate key. Which normal form does it reach at least?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 3NF, because the right-hand side of every dependency is prime

    The clause "or A is prime" accepts every dependency when all attributes are prime, so the relation is in 3NF. Whether it reaches BCNF depends on whether a non-superkey still determines part of a key, as in R2.

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. Fresher to mid

    An exam seating table, seating(roll_no, session, seat, hall), is keyed by (roll_no, session) and also has session seat -> roll_no and seat -> hall. Your check says 2NF; a colleague says 1NF. Who is right, and which step decides it?

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

  2. Mid to senior

    A posts table of 5 million rows keeps each post's tags as one comma-separated string. A search for the tag "go" also returns posts tagged "golang", and counting posts per tag scans the whole table. What is wrong with the design, and how do you change it without stopping the service?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • 1NF means one atomic value in each cell; a list in a column hides its values from keys, foreign keys and equality tests.
  • Against every candidate key, a dependency X -> A with a non-prime A is partial when X is part of a key and transitive when X is in no key at all.
  • 2NF forbids a non-prime attribute that depends on part of a candidate key, so it can fail only when some key has two or more attributes.
  • 3NF asks, for every non-trivial X -> A, that X be a superkey or A be prime, and the sheet reaches it as four tables.
  • Find every candidate key first: with its second key found, the seating plan is in 1NF, not 2NF.
  • BCNF and 3NF can be tested on the written dependencies, but 2NF needs the closure of every proper part of every key.

Terms in this lesson

first normal form (1NF)
Every cell of the relation holds a single atomic value from its domain, never a list, a set or a nested table.
partial dependency
A dependency X -> A in which A is non-prime and X is a proper part of a candidate key. It breaks second normal form.
prime attribute
An attribute that belongs to at least one candidate key of the relation. An attribute in no candidate key is non-prime.
second normal form (2NF)
First normal form with no partial dependency, so every non-prime attribute depends on whole candidate keys.
third normal form (3NF)
For every non-trivial functional dependency X -> A that holds in the relation, X is a superkey or A is a prime attribute.
transitive dependency
A dependency X -> A in which A is non-prime and X is neither a superkey nor part of a candidate key, so A depends on a key only through X.

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.