Skip to content
← Database Systems

/knowledge/notes/functional-dependencies

Concept note · Database Systems

Functional Dependencies

A dependency says which columns fix which. Keys, closures and normal forms all follow from taking those promises seriously.

Studied
Database SystemsINFO20003 · COMP90050
When
2020 S1
Applied in
Louvre Ops DB
Read / Refreshed
~6 min read2026-10-21

A functional dependency is a promise about data: whenever two rows agree on these columns, they agree on that one too. A good part of database design is writing those promises down and then arranging the tables so that each fact is stored in exactly one place. This note covers what a dependency says, how to reason from a few of them to everything they imply, and why a table that ignores them turns every update into a chance to contradict itself.

01

The idea

Write X→YX \to Y, read "XX determines YY", when knowing the values in columns XX fixes the value in column YY. In a ticketing table, the ticket number determines the ticket type, and the type determines the price. The dependency is a claim about every table the system could ever hold, not only the rows in it today. That makes it a business rule, and it comes from what the columns mean.

A key is a set of columns that determines every other column, with nothing spare in it. Declaring a primary key is declaring a dependency from the key to the whole row, which is why a key chosen carelessly can quietly stop the table storing what actually happens.

Trouble starts when a dependency's left side is not a key. Picture one wide table of wing scans with the ticket type and price copied into every row. The Adult price now sits in every Adult scan. Change it and you must update every one of those rows, and if one is missed, the table holds two Adult prices at once. That is an update anomaly. You also cannot record the price of a new ticket type until someone is scanned with one, and deleting the last Member scan forgets what a Member ticket costs. Normalisation fixes all three by splitting the table so each dependency's left side becomes the key of its own table. The Database Systems topic walks through the normal forms that make this precise.

02

The maths

Formally, a relation satisfies X→YX \to Y when no two of its rows agree on XX and disagree on YY.

X→Y  ⟺  for all rows t1,t2:  t1[X]=t2[X]  ⇒  t1[Y]=t2[Y]X \to Y \iff \text{for all rows } t_1, t_2:\; t_1[X] = t_2[X] \;\Rightarrow\; t_1[Y] = t_2[Y]

One pair of rows is enough to break a dependency. No number of rows is enough to prove one.

From a declared set of dependencies FF, Armstrong's three rules derive every dependency that must also hold: reflexivity, augmentation and transitivity.

Y⊆XX→YX→YXZ→YZX→YY→ZX→Z\frac{Y \subseteq X}{X \to Y} \qquad\qquad \frac{X \to Y}{XZ \to YZ} \qquad\qquad \frac{X \to Y \quad Y \to Z}{X \to Z}

The rules are sound, so they never derive a false dependency, and complete, so they derive every true one.

In practice you rarely apply the rules by hand. You compute a closure instead: start with XX, and whenever a declared dependency's left side sits inside the set, add its right side, until nothing changes.

X+={ A:X→A follows from F },X is a superkey  ⟺  X+=RX^{+} = \{\, A : X \to A \text{ follows from } F \,\}, \qquad X \text{ is a superkey} \iff X^{+} = R

A key is a superkey that stops being one if any column is removed.

Boyce–Codd normal form asks that every dependency that holds has a superkey on its left. Third normal form relaxes this slightly, allowing the right side to be part of some key, which keeps every dependency checkable inside a single table when BCNF cannot.

R is in BCNF  ⟺  every non-trivial X→Y that holds has X+=RR \text{ is in BCNF} \iff \text{every non-trivial } X \to Y \text{ that holds has } X^{+} = R

Trivial means Y is already inside X, as in ticket, time determines time.

A split must not lose information. Joining the pieces back has to give exactly the original rows, no more, and that is guaranteed when the shared columns are a key of one of the pieces.

R→R1,R2 is lossless  ⟺  (R1∩R2)→R1   or   (R1∩R2)→R2R \to R_1, R_2 \text{ is lossless} \iff (R_1 \cap R_2) \to R_1 \;\text{ or }\; (R_1 \cap R_2) \to R_2

Split on a dependency X to Y by making one table of X and Y, and keeping X in the other.

03

Try it

The widget holds a log of wing scans at a made-up museum, one row per scan, and the four rules its designer declared. Build a dependency with the buttons. The widget answers two separate questions about it: does this sample of rows break it, and does it follow from the declared rules?

Rules the designer declared

  • ticket → type
  • type → price
  • wing → level
  • ticket, time → wing

Left side, X (pick one or more)

16
Seed 1

ticket → wing

In this sample

Broken. Rows 1 and 4 agree on ticket (T08) but differ on wing: East against West.

From the declared rules

{ticket}⁺ = {ticket, type, price}. The closure does not contain wing, so the rules do not promise this. The left side is not a key.

Via ticket → type, then type → price.

The first 16 wing scans, in time order. X columns are marked (X), Y is (Y).
#ticket (X)typepricewing (Y)leveltime
1clashT08Member0EastL209:10
2T03Adult22EastL209:14
3T01Adult22WestL009:17
4clashT08Member0WestL009:18
5T01Adult22EastL209:57
6T08Member0NorthL109:59
7T01Adult22WestL010:02
8T02Adult22WestL010:25
9T05Adult22WestL010:41
10T04Member0WestL010:53
11T02Adult22WestL011:08
12T04Member0EastL211:12
13T04Member0NorthL111:54
14T06Adult22EastL212:10
15T07Adult22NorthL112:38
16T06Adult22WestL012:43

Broken. Rows 1 and 4 agree on ticket (T08) but differ on wing: East against West. The closure does not contain wing, so the rules do not promise this.

Scans come from a seeded generator that obeys the four declared rules, so any break you find is a dependency nobody promised.
  • The default asks whether ticket determines wing. The data breaks it, because a ticket visits several wings, and the closure of ticket holds only ticket, type and price.
  • Add time to the left side. Ticket and time together determine the wing, the closure reaches every column, and the pair is a key.
  • Try price determines type. At 16 rows it holds. Drag the rows to 24, and a Child ticket and a Member ticket turn out to cost the same 0. Type determines price, and the reverse was never a rule.
  • Try time determines ticket. Up to 32 rows every scan falls on a different minute, so the sample cannot refute it. At 40 rows several minutes are shared. A sample can break a dependency and never prove one.
  • Tick the split. Three declared rules have a left side that is not a key, and splitting on them stores fewer cells and keeps the Adult price in one row.

04

Where I used it

05

Easy to get wrong

06

Sources