Why go beyond 3NF?
3NF removes most redundancy, but not all. Take R(Student, Course, Teacher): each student takes a course from one teacher (SC → T), and each teacher teaches only one course (T → C).
| Student | Course | Teacher |
|---|---|---|
| Asha | DBMS | Rao |
| Ravi | DBMS | Rao |
| Meera | DBMS | Iyer |
| Asha | OS | Sen |
“Rao teaches DBMS” is stored once for every student Rao has. If Rao switches to OS, many rows must change (an update anomaly). If Meera leaves, we lose the fact that Iyer teaches DBMS (a deletion anomaly). R is in 3NF, because C is part of the key SC. But the root problem is that T → C holds while T is not a key.
The definition
A relation is in BCNF (Boyce–Codd normal form) if for every non-trivial functional dependency X → Y that holds in it, X is a superkey.
| 3NF | BCNF | |
|---|---|---|
| X → A is fine if… | X is a superkey or A is part of some key | X is a superkey, with no exception |
| Lossless decomposition always possible? | yes | yes |
| Dependency-preserving decomposition always possible? | yes | no |
The decomposition algorithm
- Find a relation Ri and a set X ⊂ Ri such that X determines more of Ri but isn’t a superkey of Ri. Use attribute closure: X⁺ ∩ Ri ≠ X and X⁺ ∩ Ri ≠ Ri.
- Split Ri into R1 = X⁺ ∩ Ri (X and everything it determines) and R2 = Ri − (X⁺ − X) (the rest, keeping X so the parts can be joined).
- Repeat until no part breaks BCNF.
Every split is lossless. R1 and R2 share exactly X, and X determines all of R1. By the binary test (R1 ∩ R2 → R1), joining them gives back exactly the original rows, with no extra “spurious” ones.
Worked examples
R(S, C, T), F = {SC → T, T → C}. Keys: SC and ST. T⁺ = TC, so T → C breaks BCNF.
- Split on T: (C, T) and (S, T). Both have two attributes, so both are in BCNF.
- But SC → T is lost. No single part contains S, C and T, so the database can’t check it without a join. This is the classic case where BCNF costs a dependency.
R(A, B, C, D, E), F = {A → B, B → C, C → D, D → E}. Key: A.
| Step | Relation | Violation | Split into |
|---|---|---|---|
| 1 | (A, B, C, D, E) | B⁺ = BCDE, but B is not a key | (B, C, D, E) and (A, B) |
| 2 | (B, C, D, E) | C → DE | (C, D, E) and (B, C) |
| 3 | (C, D, E) | D → E | (D, E) and (C, D) |
Result: (A, B), (B, C), (C, D), (D, E), and every FD is preserved.
Code
from itertools import combinations
def closure(X, F):
c = set(X)
changed = True
while changed:
changed = False
for L, R in F:
if L <= c and not R <= c:
c |= R
changed = True
return c
def violation(Ri, F):
"""Smallest X in Ri that determines more of Ri but is not a superkey of Ri."""
for size in range(1, len(Ri)):
for X in combinations(sorted(Ri), size):
got = closure(X, F) & Ri
if got != set(X) and got != Ri:
return set(X), got
return None
def bcnf(Ri, F):
v = violation(Ri, F)
if not v:
return [Ri]
X, got = v
return bcnf(got, F) + bcnf((Ri - got) | X, F) # lossless: the parts share X, and X -> got
def preserved(L, R, F, parts):
"""Can L -> R be checked using only the parts (closure through projections)?"""
got = set(L)
while True:
add = set().union(*(closure(got & P, F) & P for P in parts)) - got
if not add:
return R <= got
got |= add
F = [({'S', 'C'}, {'T'}), ({'T'}, {'C'})]
parts = bcnf({'S', 'C', 'T'}, F)
print([''.join(sorted(p)) for p in parts]) # ['CT', 'ST']
print([preserved(L, R, F, parts) for L, R in F]) # [False, True]
Choosing between 3NF and BCNF
- If BCNF keeps every dependency, use BCNF.
- If it would lose an important dependency (like SC → T above), many designers stop at 3NF, which always keeps every dependency, and accept a little redundancy.
- Different choices of violating FD can give different BCNF decompositions. All of them are correct, and some preserve more dependencies than others.
Common mistakes
- Checking only the given FDs inside the parts. A part can break BCNF through a derived FD. Use closures.
- Forgetting to keep X in both parts. Without the shared X the join is lossy and invents rows.
- Thinking BCNF always keeps every dependency. It doesn’t, as the S, C, T example shows.
- Mixing up superkey and candidate key. X only has to be a superkey (it may contain extra attributes).
Complexity at a glance
| Case / operation | Time | Why |
|---|---|---|
| Test one relation for BCNF (n attributes) | up to 2ⁿ closures | For the original relation it is enough to check the given FDs. For the parts, derived FDs count too. |
| Lossless test of a binary split | one closure | R1 ∩ R2 → R1 or R1 ∩ R2 → R2. |
| Dependency-preservation test | polynomial | A closure that is only allowed to move through the parts. |
Quick check
Test yourself — pick an answer to see if you got it.
1. When is a relation in BCNF?
3NF also allows X → Y when Y is part of some key. BCNF drops that exception.
2. R(S, C, T) has SC → T and T → C. Which FD violates BCNF?
T is not a superkey, since T⁺ = TC. But R is in 3NF, because C is part of the key SC.
3. Why is splitting R on X into R1 = X⁺ ∩ R and R2 = R − (X⁺ − X) lossless?
The binary test says a split is lossless if the common attributes determine one of the two parts.
4. R(S, C, T) is decomposed into (C, T) and (S, T). Which dependency is lost?
S, C and T never sit in one part, so checking SC → T would need a join.
5. Which statement is true?
3NF synthesis always manages both. BCNF is always lossless, but sometimes it can't preserve every dependency, as in the S, C, T example.