Finding Candidate Keys from FDs
Handed nothing but a tangle of functional dependencies, how do you find every key a table has? One short procedure does it every time — and GATE asks it almost every year.
What you'll learn
- A candidate key is a minimal attribute set whose closure is the whole relation
- Sort attributes into LHS-only, RHS-only, and both — the split that shortcuts the search
- The procedure that turns a set of FDs into every candidate key
- Why LHS-only attributes sit in every key and RHS-only ones sit in none
Before you start
Last lesson closed on a reversal. Computing X⁺ confirms a key you already suspect — you start with X, watch its closure grow, and check whether it swallows every attribute. That is the easy direction. The hard one ran the other way: handed nothing but a tangle of functional dependencies, how do you find all the keys, not merely test a guess? That hunt is what this lesson is about.
Picture a school’s attendance register. To point at exactly one pupil, what do you need to know? The roll number alone will do it. So will a full name together with a date of birth and a home town, if the school is small enough that no two such pupils collide. Each of these is a little bundle of facts that pins down one row and no other.
A single table can carry several such bundles at once. Before you crown one of them the primary key, you would like the honest list of all of them — and guessing is not a method. The functional dependencies already hold the answer; we only need a procedure that reads it off. GATE leans on exactly this almost every year — how many candidate keys does R have, or which of these is one — so the procedure is worth owning cold.
Here is the picture to carry. Treat each attribute as a room, and each FD X → Y as a one-way door: once you are standing in all the rooms of X, that door swings open and lets you into Y. A bundle that pins down a row is then a set of rooms you can start in and, walking through doors, reach every room in the building. The smallest such starting set is the thing we are hunting.
A candidate key, defined
That smallest reach-everything starting set has a name: a candidate key. Precisely, it is a set of attributes K such that:
K⁺(its attribute closure) is the full set of attributes —Kdetermines everything.- No proper subset of
Kalready determines everything —Kis minimal.
Condition 1 makes K a superkey: enough to reach every room. Condition 2 trims that superkey down until nothing can be dropped. A relation can own more than one candidate key, and a key with n attributes does not promise that some (n-1)-attribute key also exists — minimality is checked subset by subset, never assumed.
The shortcut: where each attribute appears
Before computing a single closure, look at where each attribute lives in the FDs. That one glance answers the question the last lesson left hanging — which attributes are forced into every key, and which can never appear in one.
- LHS-only — appears on some left-hand side, never on any right-hand side. Must sit in every candidate key. Nothing in the FDs produces it, so the only way to have it in your closure is to start holding it.
- RHS-only — appears only on right-hand sides. Cannot sit in any candidate key. It is always producible from something else, so including it can never be minimal.
- Both / neither — the optional middle. You may need some of these, and only here does any real searching happen.
The procedure
The classification turns into four mechanical steps.
- Classify every attribute as LHS-only, RHS-only, or both.
- Let
K =the set of all LHS-only attributes. ComputeK⁺. - If
K⁺is every attribute, thenKis the one and only candidate key. Stop — nothing smaller can work (you cannot drop a mandatory attribute), and nothing else can be minimal. - Otherwise, add ONE “both” attribute at a time to
Kand recompute. If a single addition completes the closure, that set is a candidate key. If none does, try pairs, then triples — always keeping minimality by skipping any superset of a key you already found.
Most exam problems halt at step 3, or after a single one-attribute extension. The cost lives entirely in the “both” bucket; the smaller that bucket, the shorter the hunt.
How GATE asks this
- NAT — “How many candidate keys does
Rhave?” Run the procedure; count. - MCQ — “Which of the following is a candidate key of
R?” For each option, ask two questions: is it a superkey, and is it minimal (drop each attribute, recompute the closure)? - MSQ — properties of candidate keys themselves: LHS-only inclusion, RHS-only exclusion, minimality, multiplicity.
Worked example
R(A, B, C, D, E)withF = {A → B, BC → D, D → E}. Find all candidate keys.
Step 1 — classify. Scan each attribute against the three FDs.
A: left ofA → B. Never on any RHS. → LHS-only.C: left ofBC → D. Never on any RHS. → LHS-only.B: left ofBC → D; right ofA → B. → both.D: left ofD → E; right ofBC → D. → both.E: only right ofD → E. → RHS-only.
Step 2 — start from the mandatory set. The LHS-only attributes are A and C, so K = {A, C}.
Step 3 — compute K⁺, one firing at a time.
- Start
closure = {A, C}. A → B:Ais in, so addB→{A, B, C}.BC → D:BandCare both in, so addD→{A, B, C, D}.D → E:Dis in, so addE→{A, B, C, D, E}. Every attribute is in; stop.
{A, C}⁺ reaches everything, so {A, C} is a superkey. It holds both LHS-only attributes, and those are mandatory in any candidate key — so it cannot be shrunk. Therefore {A, C} is the unique candidate key.
No extensions are worth trying. Any other superkey would also have to contain {A, C}, which makes it a superset of the key we found — and a superset is never minimal.
In one breath
A candidate key is a minimal superkey: its closure spans the whole relation, and no proper subset already does. Sort the attributes by where they live in the FDs — LHS-only ones belong to every key, RHS-only ones to none, and the rest are the only attributes you ever search over. Start from the LHS-only set, grow its closure, and extend through the “both” bucket only if you must.
Practice
Quick check
A question to carry forward
You can now hand back, for any table, the full list of its candidate keys. That list quietly sorts every attribute into two camps: the prime attributes, which belong to at least one candidate key, and the non-prime ones, which belong to none. Hold onto that split — it is about to do real work.
Because a table can be technically correct and still feel badly built: the same department head copied onto every employee row, a customer’s whole order crammed into one cell, a fact you cannot record until some unrelated fact exists. Here is the thread onward: given the keys you just found and the prime/non-prime labels they imply, is there a precise ladder of “how clean is this design” that you can test a table against — and what exactly does each rung forbid?
Practice this in an interview
All questionsUse backtracking with a running total. At each step, try adding a candidate to the current path. If the total equals the target, record the path. If it exceeds the target, prune. Passing the same start index (not i+1) back into the recursion allows unlimited reuse of the same element.
You combine conditions in the ON clause with AND to join on multiple columns, which is necessary when no single column is a unique identifier across both tables. For index performance, the most selective column — or the column used in equality predicates — should come first in a composite index.
1NF eliminates repeating groups and requires atomic column values. 2NF further removes partial dependencies on a composite key. 3NF removes transitive dependencies — every non-key column must depend on the key, the whole key, and nothing but the key. Denormalization trades update anomalies for read performance, and is appropriate when the read path dominates and write correctness can be enforced at the application layer or with materialized views.
A primary key uniquely identifies each row in a table and implicitly creates a unique index; it cannot be NULL. A foreign key in a child table references the primary key of a parent table and enforces referential integrity — the database rejects inserts or updates that reference a non-existent parent row, and rejects parent deletes that would orphan child rows unless a cascade rule is defined.