深蓝黑背景封面:右侧大标题「一张表里,到底有几种键」,副标题「超键 ⊃ 候选键 ⊃ 主键,外键管表间」;左侧原创示意图——三个同心嵌套椭圆从外到内标注超键、候选键、主键,旁边另一张表的椭圆通过一条外键箭头指向主键;左上标注「面试经典 · 数据库」,右上标注「Day 01 · 10-29」 图 1:本文封面,四种键的包含关系一目了然。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。「一张表里有几种键」这道题考的不是背定义,而是四种键之间的包含关系——很多人能分别说出定义,却画不出它们谁套谁。

先看包含链:超键 ⊃ 候选键 ⊃ 主键。

超键是能唯一标识一行的任意属性组合。注意是「任意」:工号能唯一标识一名员工,那么(工号,姓名)、(工号,部门,姓名)也都是超键——只要组合里含一个能唯一标识的部分,整个组合就是超键。超键允许冗余,它是这个家族里最外层的圈。

候选键是「极小的」超键:去掉任何一个属性,就不再能唯一标识一行。工号是候选键,(工号,姓名)不是——因为姓名是冗余的,去掉它剩下工号依然唯一。一张表可以有多个候选键:员工表里工号和身份证号各自唯一,它们是两个并列的候选键。

主键是从候选键里选定的那一个。候选键是「有资格」,主键是「已上任」。MySQL 官方文档对主键的要求很明确:它带唯一索引,且不允许包含 NULL 值——InnoDB 里表数据物理上就按主键组织,主键的选择直接决定查找和排序的物理效率。

再看外键:它不属于包含链,它管表与表之间。

外键是本表里指向另一张表主键(或唯一键)的属性,它要的不是唯一性,而是参照完整性:外键的值必须在被引用的表里真实存在,或者被置空。订单表的用户 ID 引用用户表的主键,数据库就会拦下「给不存在的用户下单」这种写入。三种键负责表内的唯一性,外键负责表间的引用合法——这是两套正交的职责。

选主键看三件事:唯一、非空、稳定。

唯一和非空是硬约束,前面已经说过;「稳定」是工程经验:主键的值最好不要随业务变化。用邮箱做主键,用户改一次邮箱,所有引用它的外键都得跟着级联更新,一次变更变成全表迁徙。这也是为什么多数表会引入与业务无关的自增 ID 或 UUID 当主键(代理键),把业务上唯一的列降为唯一约束(自然键)——MySQL 文档也建议:大表没有天然合适的主键列时,单独建一个自增列。

用代码把「极小性」跑一遍。

概念容易混,判定逻辑却可以写得明明白白。下面这段代码在样本行上检验「超键」与「候选键」的判定规则,用 Node.js 22.15.0 直接运行(node keys.js):

'use strict';

// 员工表样本:工号与身份证号各自唯一,姓名不唯一,(姓名,部门) 组合也不唯一
const rows = [
  { empNo: 'E001', idCard: 'IC-1001', name: '林一', dept: '平台' },
  { empNo: 'E002', idCard: 'IC-1002', name: '林一', dept: '数据' },
  { empNo: 'E003', idCard: 'IC-1003', name: '王二', dept: '平台' },
  { empNo: 'E004', idCard: 'IC-1004', name: '林一', dept: '平台' },
];

// 超键:该属性组合在所有行上的取值组合互不相同(允许含冗余属性)
function isSuperKey(attrs, rows) {
  const seen = new Set();
  for (const row of rows) {
    const signature = attrs.map((a) => row[a]).join(' ');
    if (seen.has(signature)) return false;
    seen.add(signature);
  }
  return true;
}

// 候选键:是超键,且去掉任何一个属性就不再是超键(极小性)
function isCandidateKey(attrs, rows) {
  if (!isSuperKey(attrs, rows)) return false;
  return attrs.every((drop) => !isSuperKey(attrs.filter((a) => a !== drop), rows));
}

const candidates = [
  ['empNo'],
  ['idCard'],
  ['name'],
  ['empNo', 'name'],   // 能唯一标识,但含冗余
  ['name', 'dept'],
  ['empNo', 'idCard'],
];
for (const attrs of candidates) {
  const label = `(${attrs.join(', ')})`;
  console.log(
    `${label.padEnd(18)} 超键=${isSuperKey(attrs, rows) ? '是' : '否'} 候选键=${isCandidateKey(attrs, rows) ? '是' : '否'}`,
  );
}

实测输出:

(empNo)            超键=是 候选键=是
(idCard)           超键=是 候选键=是
(name)             超键=否 候选键=否
(empNo, name)      超键=是 候选键=否
(name, dept)       超键=否 候选键=否
(empNo, idCard)    超键=是 候选键=否

六行输出把包含链演示得很清楚:(empNo, name)能唯一标识每一行,是超键;但去掉 name 后 empNo 依然唯一,所以它不是候选键。这就是「超键允许冗余、候选键要求极小」的机械判定。

要提醒一点:代码只能在给定样本上验证,而候选键本质上是业务语义的承诺——「身份证号唯一」是对所有可能数据的断言,不是对当前四行数据的观察。样本验证用来理解概念,真实建模要回到业务规则。

左侧包含关系图:外层椭圆「超键」——任意能唯一标识一行的属性组合(含冗余);中层「候选键」——去掉冗余属性后仍能唯一标识的最小超键,一张表可以有多个;内层「主键」——从候选键里选定的那个,要求非空且稳定。右侧两张迷你表:订单表的用户 ID 列作为外键,箭头指向用户表的主键,标注「外键的值必须在被引用表里存在,或被置空」。底部总结卡:选主键看三件事——唯一、非空、不轻易变;外键把表间引用写成数据库能强制检查的约束 图 2:包含链加外键参照完整性,四种键各就各位。

这套概念在工程里真的有用。

做数据管道时,样本表、标注表的主键设计直接决定写入幂等和去重策略:搞不清「候选键」与「自然键」的区别,用了一个会变的业务字段当去重键,数据一回流就埋下一堆重复记录。反过来,把幂等键设计成稳定的代理键加唯一约束的组合,重试、回放、补数全都安全。

回到面试现场,建议的答题顺序:先给四个定义,再画包含链(超键⊃候选键⊃主键,外键在链外管表间),然后补一句主键选择的三标准,最后落到代理键与自然键的工程取舍。概念题答出工程味,分差就在这里。你在建表时用过业务字段当主键吗?后来踩坑了吗?欢迎分享。