图 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 依然唯一,所以它不是候选键。这就是「超键允许冗余、候选键要求极小」的机械判定。
要提醒一点:代码只能在给定样本上验证,而候选键本质上是业务语义的承诺——「身份证号唯一」是对所有可能数据的断言,不是对当前四行数据的观察。样本验证用来理解概念,真实建模要回到业务规则。
图 2:包含链加外键参照完整性,四种键各就各位。
这套概念在工程里真的有用。
做数据管道时,样本表、标注表的主键设计直接决定写入幂等和去重策略:搞不清「候选键」与「自然键」的区别,用了一个会变的业务字段当去重键,数据一回流就埋下一堆重复记录。反过来,把幂等键设计成稳定的代理键加唯一约束的组合,重试、回放、补数全都安全。
回到面试现场,建议的答题顺序:先给四个定义,再画包含链(超键⊃候选键⊃主键,外键在链外管表间),然后补一句主键选择的三标准,最后落到代理键与自然键的工程取舍。概念题答出工程味,分差就在这里。你在建表时用过业务字段当主键吗?后来踩坑了吗?欢迎分享。