Relations, Attributes, and Keys

Imagine a university database storing thousands of students. Two different students happen to share the same name. If the system relied on names to find "the" student, an advisor's grade update could silently land on the wrong record. This everyday scenario is exactly the problem that keys exist to solve. They give us a principled way to guarantee something important. Every row in a table can be pinned down uniquely and unambiguously. This article builds the vocabulary that the entire relational model rests on. Key terms include attributes, tuples, relations, and a hierarchy of key types. That hierarchy includes superkeys, candidate keys, and primary keys. These are not just naming conventions. They are precise mathematical objects with well-defined properties. The choices a designer makes about them ripple through every later stage of database design. This includes constraint enforcement, normalization, and physical indexing. The big picture is this: a relation is a set of tuples. Sets by definition cannot contain duplicate elements. Keys are the mechanism that translates this abstract "no duplicates" property into something concrete. They give a designer a minimal set of attributes whose values determine an entire row. Once you can identify keys correctly, you can enforce entity integrity. You can also design efficient indexes. And you can reason rigorously about Functional Dependencies, which in turn drive normalization. This article delivers a precise definition of relations and attributes. It offers a step-by-step algorithm for discovering superkeys and candidate keys from functional dependencies. It includes a worked trace on a concrete schema, and a diagram of the key hierarchy. Finally, it discusses pitfalls that trip up almost every beginner. These especially include confusion between "candidate key" and "primary key." Another common trap is declaring a key without checking all attributes it must determine.

Key takeaways
  • A relation is a set of tuples over a fixed schema of named, typed attributes; row order and duplicates are meaningless.
  • A superkey is any attribute set that uniquely identifies every tuple; a candidate key is a minimal superkey (no attribute can be dropped).
  • A relation can have several candidate keys; the designer picks one as the primary key, and the rest become alternate keys.
  • Prime attributes belong to some candidate key; all others are non-prime, a distinction that matters later for normalization.
  • Finding candidate keys from functional dependencies is computable via attribute closure, an efficient, provably correct algorithm.
  • Picking the wrong key (or none) breaks entity integrity and can silently corrupt data through duplicate or ambiguous rows.
The core insight

A key is the minimal set of attributes whose values are guaranteed to determine one, and only one, tuple in a relation.

The idea

A relation schema is a name paired with a fixed set of attributes, each drawn from a domain — the set of legal values for that attribute (integers, strings of bounded length, dates, and so on). Formally, a schema R(A1,A2,…,An)R(A_1, A_2, \dots, A_n) names attributes A1,…,AnA_1,\dots,A_n, and a relation instance rr on RR is a finite set of tuples, where each tuple t=(v1,…,vn)t = (v_1, \dots, v_n) assigns a value vi∈dom(Ai)v_i \in \text{dom}(A_i) to each attribute. Because rr is a mathematical set, two consequences follow immediately: tuples have no inherent order, and — crucially — no two tuples in rr can be identical. This second property is what forces the existence of keys: if the model guarantees no duplicate rows, there must be some subset of attributes whose values are always sufficient to tell any two distinct tuples apart. This is where keys enter. Let K⊆{A1,…,An}K \subseteq \{A_1,\dots,A_n\} be a subset of attributes. KK is a superkey of RR if, for every legal instance rr of RR, no two distinct tuples t1≠t2t_1 \ne t_2 in rr agree on all attributes of KK. In symbols, t1[K]=t2[K]  ⟹  t1=t2t_1[K] = t_2[K] \implies t_1 = t_2. Every relation trivially has at least one superkey: the set of all its attributes, since no two tuples can be entirely identical. The interesting question is how much smaller that set can be made while preserving uniqueness. A superkey KK is a candidate key if it is minimal: no proper subset of KK is also a superkey. Minimality is the whole point — it says every single attribute in KK is pulling its weight in guaranteeing uniqueness, and removing any one of them breaks the guarantee. A relation may have more than one candidate key (think of a `Student` relation identified equally well by a university-issued ID or by a national ID number). Among the candidate keys, the designer designates exactly one as the primary key, chosen by convention for stability, brevity, and non-null guarantees; the others are called alternate keys (or secondary/candidate keys, terminology varies). Attributes that participate in at least one candidate key are called prime attributes; all others are non-prime attributes — a classification that becomes essential later when discussing Functional Dependencies and normal forms, since normalization rules are stated in terms of dependencies on candidate keys and the primeness of attributes. Finally, a foreign key is a different (though related) concept — an attribute set in one relation that references the primary key of another, enforcing referential integrity across relations, a topic developed under Integrity Constraints.

How it works

In practice, keys are not guessed by inspection alone — for schemas of any real size, they are derived from a declared set of functional dependencies (FDs). An FD X→YX \rightarrow Y states that whenever two tuples agree on all attributes in XX, they must also agree on all attributes in YY. Given a schema RR and a set of FDs FF, the standard algorithm for testing "is KK a superkey?" and for discovering all candidate keys relies on computing the attribute closure K+K^+: the set of all attributes functionally determined by KK under FF. The closure algorithm is the computational workhorse here. Its correctness rests on Armstrong's axioms (reflexivity, augmentation, transitivity), which are sound and complete for the theory of functional dependencies — meaning the closure computed this way captures exactly the FDs logically implied by FF, no more and no less. ``` ATTRIBUTE-CLOSURE(K, F): // K: a set of attributes; F: a set of functional dependencies X -> Y result <- K changed <- true while changed: changed <- false for each FD (X -> Y) in F: if X is a subset of result and Y is not a subset of result: result <- result union Y changed <- true return result ``` ``` IS-SUPERKEY(K, R, F): return ATTRIBUTE-CLOSURE(K, F) == all attributes of R IS-CANDIDATE-KEY(K, R, F): if not IS-SUPERKEY(K, R, F): return false for each attribute A in K: if IS-SUPERKEY(K - {A}, R, F): return false // some proper subset is already a superkey -> K not minimal return true ``` ``` FIND-ALL-CANDIDATE-KEYS(R, F): // Practical (exponential worst case, fine for typical schema sizes) candidates <- empty set for each subset S of attributes(R), in increasing order of size: if S is a superset of some already-found candidate key: continue // cannot be minimal if IS-SUPERKEY(S, R, F): candidates <- candidates union {S} return candidates ``` The three procedures compose cleanly. `ATTRIBUTE-CLOSURE` answers the atomic question "what does this attribute set determine?" `IS-SUPERKEY` reduces to checking whether that closure reaches every attribute in RR. `IS-CANDIDATE-KEY` adds the minimality check by trying to remove each attribute one at a time and confirming the superkey property breaks. `FIND-ALL-CANDIDATE-KEYS` enumerates subsets by increasing size — smallest first — so that once a candidate key is found, any larger set containing it is automatically skipped, since it cannot be minimal. A practical shortcut speeds this up enormously: partition attributes into three classes before enumerating anything. 1. An attribute that never appears on the right-hand side of any FD must appear in every candidate key (nothing else can ever produce it). 2. An attribute that never appears on the left-hand side of any FD is useless to test as part of a candidate key's "core" (it can only ever be a passenger, determined by others). 3. Everything else needs to be tested combinatorially, but usually this remaining set is small.

Figure
The key hierarchy inside a relation schema
All attributes of R (trivial superkey) Superkeys (uniquely identify every tuple) Candidate keys (minimal superkeys) Primary key Alternate key e.g. {StudentID} e.g. {NationalID} {StudentID, Name} — superkey, not minimal Nesting shows: Primary/Alternate ⊂ Candidate keys ⊂ Superkeys ⊂ All attributes

Complexity & correctness

Correctness of `ATTRIBUTE-CLOSURE`. The loop only ever adds attributes to `result`, never removes any, so it is monotone; since the attribute universe is finite, the loop must terminate. Termination happens exactly when a full pass over FF adds nothing new, at which point `result` is closed under one application of every FD — and by the transitivity/reflexivity/augmentation axioms (Armstrong's axioms, known to be sound and complete), this fixed point equals the true semantic closure K+K^+: the exact set of attributes functionally determined by KK given FF. Soundness means every attribute added is genuinely implied; completeness means no implied attribute is missed. This is the standard guarantee that makes the closure test exact rather than heuristic.

Complexity of closure. Each outer iteration of the `while` loop either adds at least one attribute to `result` or terminates; since there are at most nn attributes, the loop body runs at most nn times before nothing changes. Each pass scans all ∣F∣|F| dependencies and does a subset check costing O(n)O(n), giving O(n⋅∣F∣⋅n)=O(n2∣F∣)O(n \cdot |F| \cdot n) = O(n^2 |F|) per closure computation in a naive implementation (this is commonly tightened to near-linear in the total size of FF with hashing/counter tricks, but the simple bound already suffices for correctness reasoning and is what's typically taught).

Correctness of superkey/candidate-key tests. `IS-SUPERKEY` is correct by definition: KK functionally determines every attribute of RR if and only if K+=RK^+ = R, which is precisely what distinguishes a set that pins down a unique tuple from one that doesn't. `IS-CANDIDATE-KEY` is correct because minimality is, by definition, "no proper subset is a superkey" — and it suffices to check subsets obtained by removing one attribute at a time: if some larger deficient subset failed to be minimal for a different reason, that failure would already have been detected at the single-attribute-removal level, since superkey-ness is monotone (any superset of a superkey is also a superkey). This monotonicity is exactly what justifies stopping at singleton removals rather than checking every proper subset explicitly.

Complexity of full enumeration. In the worst case, a relation can have exponentially many candidate keys relative to the number of attributes — a classical fact from FD theory — so `FIND-ALL-CANDIDATE-KEYS` is worst-case exponential, O(2n)O(2^n) subsets tested, each costing a closure computation. This is not a flaw in the algorithm; it reflects the genuine combinatorial richness of the candidate-key structure. In practice, real schemas have a handful of attributes with well-understood dependency structure (often driven directly from an ER Model design), so the exponential blowup rarely bites; the "attributes never on a right-hand side must be in every key" pruning rule alone eliminates most of the search space in typical textbook and industrial schemas.

Tradeoffs. Choosing a primary key is not purely mathematical — a composite natural key (e.g., `{CourseID, Semester, Section}`) may be semantically meaningful but bulky to index and awkward as a foreign key target in other relations; a surrogate key (an auto-incremented integer) is compact and stable but carries no business meaning and must still be checked against candidate keys to preserve entity integrity. The relational model itself is agnostic on this choice — it only requires that some candidate key be honored as primary, enforced via a `NOT NULL` and uniqueness constraint under Integrity Constraints.

Worked example

Consider a relation for university enrollment records:

Enroll(RollNo,Name,CourseID,CourseName,Semester,Grade)\text{Enroll}(\text{RollNo}, \text{Name}, \text{CourseID}, \text{CourseName}, \text{Semester}, \text{Grade})

with the following functional dependencies declared by the designer:

F={  RollNo→Name,CourseID→CourseName,RollNo, CourseID, Semester→Grade  }F = \{\; \text{RollNo} \rightarrow \text{Name}, \quad \text{CourseID} \rightarrow \text{CourseName}, \quad \text{RollNo, CourseID, Semester} \rightarrow \text{Grade} \;\}

We trace `FIND-ALL-CANDIDATE-KEYS`.

Step 1 — classify attributes. Scan the right-hand sides of all FDs: `Name`, `CourseName`, and `Grade` each appear on some right-hand side, so none of them is forced into every key. `RollNo`, `CourseID`, and `Semester` never appear on any right-hand side — by the pruning rule, all three must belong to every candidate key. Call this forced core C={RollNo,CourseID,Semester}C = \{\text{RollNo}, \text{CourseID}, \text{Semester}\}.

Step 2 — test the forced core as a candidate. Compute C+C^+ using `ATTRIBUTE-CLOSURE`:

Pass`result` beforeFD appliedattributes added
start{RollNo, CourseID, Semester}——
1{RollNo, CourseID, Semester}RollNo → Name+Name
1 (cont.){RollNo, CourseID, Semester, Name}CourseID → CourseName+CourseName
1 (cont.){…, CourseName}RollNo,CourseID,Semester → Grade+Grade
2{RollNo, CourseID, Semester, Name, CourseName, Grade}no FD adds anything new— (fixed point)

C+={RollNo,CourseID,Semester,Name,CourseName,Grade}C^+ = \{\text{RollNo}, \text{CourseID}, \text{Semester}, \text{Name}, \text{CourseName}, \text{Grade}\}, which is all six attributes of `Enroll`. So CC is a superkey.

Step 3 — check minimality. Try dropping each attribute of CC one at a time:

  • Drop `Semester`: closure of {RollNo, CourseID} adds `Name` and `CourseName` via the first two FDs, but the third FD needs `Semester` too, so `Grade` is never added. Closure stalls at 4 of 6 attributes — not a superkey. Good, `Semester` is needed.
  • Drop `CourseID`: closure of {RollNo, Semester} adds only `Name`; `CourseName` needs `CourseID` and `Grade` needs `CourseID` too. Stalls at 2 of 6 — not a superkey. `CourseID` is needed.
  • Drop `RollNo`: closure of {CourseID, Semester} adds only `CourseName`. Stalls at 3 of 6 — not a superkey. `RollNo` is needed.

No attribute can be removed, so C={RollNo,CourseID,Semester}C = \{\text{RollNo}, \text{CourseID}, \text{Semester}\} is confirmed minimal, and hence a candidate key.

Step 4 — are there other candidate keys? Since CC is the unique forced core and every candidate key must contain it (Step 1), and CC itself is already a superkey, no strictly larger set can also be minimal (any superset of a superkey is a superkey, but not minimal unless it equals CC). So {RollNo,CourseID,Semester}\{\text{RollNo}, \text{CourseID}, \text{Semester}\} is the only candidate key of `Enroll`.

Conclusion. `{RollNo, CourseID, Semester}` becomes the primary key of `Enroll`. `Name` and `CourseName` are non-prime attributes (each functionally dependent on a proper part of the key, in fact — a partial dependency that would flag this schema as violating 2NF, foreshadowing the normalization discussion built on exactly this kind of key/dependency analysis).

Common pitfalls & traps

PitfallWhy it happensConsequence
Treating "candidate key" and "primary key" as synonymsTextbooks often pick one candidate key as canonical, obscuring that others existDesigner forgets to add a `UNIQUE` constraint on the alternate key(s), losing entity integrity on those columns
Declaring a key without checking minimalityTesting only that a set determines everything, skipping the removal stepA non-minimal set (e.g., `{RollNo, CourseID, Semester, Name}`) gets used as a key, wasting index space and hiding partial dependencies
Assuming a single "obvious" attribute (like `Name`) is a keySmall sample data has no duplicate names, so it "looks" uniqueTwo students named "Alex Kim" enroll later; uniqueness silently breaks in production
Forgetting attributes that appear on neither side of any FDOverlooking attributes with no declared dependency at allSuch attributes must also be forced into every candidate key (they determine nothing and are determined by nothing), and omitting them yields an incorrect "key" that isn't actually a superkey
Computing closure by hand and stopping too earlyForgetting to re-scan all FDs after new attributes are added in a passUnderestimates K+K^+, wrongly concludes a true superkey is not one

Pioneers & history

The theory of keys is inseparable from the invention of the relational model itself, since keys are the mechanical device that turns "a relation is a set" into an enforceable, checkable design discipline.

Origins · 1 minEdgar F. Codd and how this was discovered
EF
Edgar F. Codd
1923–2003 · IBM San Jose Research Laboratory, birth of the relational model

Codd, a mathematician by training who worked at IBM, proposed representing data as mathematical relations rather than as hierarchical or navigational pointer structures. His 1970 paper introduced relations, tuples, domains, and — critically — the requirement that every relation have a way to uniquely address a tuple, the seed of the key concept formalized soon after by Codd himself and by researchers extending his normalization program.

History
1970
— Codd publishes "A Relational Model of Data for Large Shared Data Banks," introducing relations and the uniqueness requirement that necessitates keys.
1971–1972
— Codd formalizes functional dependencies and the first normal forms, tying key structure directly to dependency theory.
1974
— Codd and Chamberlin's SEQUEL (later SQL) gives keys a concrete syntactic home via `PRIMARY KEY` and `UNIQUE` declarations.
1976
— Peter Chen's Entity-Relationship model gives designers a diagrammatic way to identify candidate keys before ever writing a schema, feeding directly into Mapping ER to Relations.
1980
s — commercial RDBMSs (Oracle, DB2, Ingres-derived systems) implement primary/foreign key constraints as enforced, indexed structures rather than mere documentation.

Where this matters

Every production relational database — PostgreSQL, MySQL, Oracle, SQL Server, SQLite — enforces primary keys as an internally indexed, uniqueness-checked structure, typically backed by a B+-tree index that both accelerates lookups and rejects duplicate insertions at write time. Object-relational mapping frameworks (Hibernate, Django ORM, ActiveRecord) generate surrogate primary keys automatically precisely because application-level natural keys are so often discovered, too late, not to be true candidate keys. Data warehouse star-schema design leans on the prime/non-prime distinction when deciding which dimension attributes belong in a dimension table's key versus its descriptive payload. Distributed databases (Cassandra, DynamoDB, CockroachDB) generalize the primary key into a partition key that determines physical data placement across nodes, meaning a poor key choice there causes not just correctness bugs but severe load-balancing hotspots. Every foreign-key relationship, every join optimization plan, and every normalization proof taught under Functional Dependencies ultimately reduces to the closure-based reasoning introduced here.

Ask the tutor: How do candidate keys and functional dependencies combine to prove a relation is in Boyce-Codd Normal Form?

What exactly distinguishes a superkey from a candidate key?

A superkey is any attribute set that uniquely identifies every tuple in a relation — including redundant ones, like all attributes together. A candidate key adds the requirement of minimality: no attribute can be removed from it without losing the uniqueness guarantee. Every candidate key is a superkey, but not every superkey is a candidate key. Concretely, if `{RollNo}` alone determines every other attribute, then `{RollNo, Name}` is still technically a superkey (it still uniquely identifies rows), but it fails minimality because `Name` is extraneous — so it is not a candidate key. The candidate key is the "tightest" superkey.

Can a relation have zero candidate keys?

No, as long as the relation is finite (which every relation instance in practice is), the full attribute set is always a superkey, since no two tuples can be entirely identical by the set semantics of a relation. Given any superkey, you can always shrink it by repeatedly removing attributes while the shrunk set remains a superkey, and this process must terminate at a minimal superkey — a candidate key. So every relation has at least one candidate key, though possibly only the trivial "all attributes" one if no smaller subset determines the rest.

Why do we bother distinguishing primary key from alternate/candidate keys at all?

Because a database can only designate one attribute set as the structural identity used for foreign-key references, clustering, and default indexing, even when several equally valid uniqueness guarantees exist. Choosing a primary key is a design decision layered on top of the mathematics: candidate keys are objective facts derivable from functional dependencies, but which one becomes primary depends on practical concerns like stability (does the value ever change?), size (shorter keys index faster), and nullability. Alternate keys still get enforced via `UNIQUE` constraints so their uniqueness guarantee is not lost, just demoted from the "official" identity role.

How does attribute closure relate to normalization?

Attribute closure is the computational engine behind almost all classical normalization theory. To check whether a relation is in a given normal form, you need to know its candidate keys and which attributes are prime — both derived via closure. For example, Second Normal Form asks whether any non-prime attribute depends on only part of a composite candidate key (a partial dependency), and Third Normal Form asks about non-prime attributes depending on other non-prime attributes (a transitive dependency). Both checks presuppose you have already correctly computed candidate keys using exactly the closure algorithm described here.

Is a foreign key the same kind of object as a candidate key?

No. A candidate key is a property internal to a single relation — it identifies rows within that relation. A foreign key is a reference mechanism between two relations: an attribute set in one relation (the referencing relation) whose values must match the primary key values in another relation (the referenced relation), or be null. Foreign keys are not required to be unique in the referencing relation at all — many rows can reference the same primary key value, which is exactly how one-to-many relationships are represented. The concepts are related but serve entirely different structural roles, covered separately under Integrity Constraints.

Why is the worst-case number of candidate keys exponential, and should I worry about this in practice?

It is a known combinatorial fact in dependency theory that carefully constructed sets of functional dependencies can force a relation to have exponentially many minimal candidate keys relative to the attribute count — essentially because minimality proofs can interact combinatorially across independent "either-or" dependency structures. In practice you should not worry: real-world schemas derived from careful ER modeling almost always have one to three candidate keys, because natural business identifiers are limited and dependencies are usually simple and hierarchical rather than adversarially constructed. The exponential bound is a worst-case theoretical ceiling, not a typical operating regime.

Does a primary key have to be a single attribute?

No — a primary key can be composite, meaning it consists of multiple attributes acting together, such as `{RollNo, CourseID, Semester}` in the enrollment example. What matters is only that the combination is a minimal superkey; there is no rule limiting primary keys to single columns. Composite keys are common in associative/junction tables representing many-to-many relationships, and in time-series or versioned data where uniqueness naturally depends on a combination like `(SensorID, Timestamp)`.

Before this

Where this leads