图 1:本文封面,主键与唯一索引的三维度对照卡。
先说明属性:这是一篇笔试真题复盘,题目出自本地历史笔试资料,与任何公司的当前招聘无关。原题是一道选择题,问关于主键和唯一索引的哪些说法是错误的,四个选项的大意是:唯一索引的列允许为 NULL;一个表的外键必定是另一张表的主键;一张表中只能有一个唯一性索引;索引主要影响查询过程,对数据的插入影响不大。能秒选靠记忆——后三个都是错的;但面试追问的是「为什么」,下面按四个维度把这组易混点钉死,语义以 MySQL InnoDB 为准。
维度一:数量与空值。
主键每张表至多一个,主键列隐含 NOT NULL 约束,想存空值直接报错。唯一索引一张表可以建任意多个,而且允许 NULL——MySQL 官方手册在 CREATE INDEX 一章写得很明确:唯一索引允许可空列存入多个 NULL 值,因为 NULL 与 NULL 不相等,不参与唯一性判重。所以「唯一索引能存 NULL 吗」的完整答案是:能,而且能存任意多个。选项三「一张表只能有一个唯一索引」混淆了主键和唯一索引的数量限制,错误。顺带一提,CREATE INDEX 语句本身不能创建主键,主键要在建表或 ALTER TABLE 时定义,这也暗示了它俩在系统里的地位不同。
维度二:聚簇与回表。
这是最容易被忽略、却最影响性能的一层。InnoDB 用主键组织聚簇索引:叶子节点直接存整行数据,按主键查一次就命中。唯一索引是二级索引:叶子节点存的是「唯一键值 + 该行的主键值」,按唯一键查到主键后,还要拿着主键去聚簇索引再查一次才能拿到整行——这就是回表。
没有主键时 InnoDB 怎么办?官方文档给出了完整的退路链:选第一个所有列都 NOT NULL 的唯一索引当聚簇索引;连这都没有,就生成一个隐藏的聚簇索引,挂在 InnoDB 自维护的 6 字节单调递增 row id 上。所以「不建主键等于没有聚簇索引」是误解——聚簇索引永远存在,区别只在用谁的键。这也解释了为什么主键该选短列:每个二级索引的叶子都要带一份主键值,主键越长,所有索引越膨胀。
图 2:NULL、数量、聚簇与回表机制全景。
维度三:外键到底引用谁。
选项二「外键必定是另一表的主键」把充分条件说成了必要条件。SQL 标准里,外键引用的是父表的候选键——主键或唯一键都行,核心要求是被引用值必须唯一,否则子表一行无法确定对应父表哪一行。引用唯一键完全合法。MySQL 的 InnoDB 历史上曾允许外键引用非唯一索引(对标准 SQL 的扩展),但官方文档已将其标记为废弃、默认拒绝,并建议尽快迁离——工程上也不要依赖这个行为。
配套的工程细节还有两条:外键列与被引用列的数据类型必须兼容(定长数值类型的大小和符号要一致,字符串的字符集与排序规则要一致);插入或更新子表行时,数据库会校验父表存在匹配的候选键值,父表的删除与更新则受 ON DELETE、ON UPDATE 子句约束。
维度四:索引不是免费的。
选项四「索引只影响查询、对插入影响不大」错得最「隐蔽」。每次插入或更新,数据库都要在索引的 B+ 树里定位、插入键值,可能引发页分裂;唯一索引还多一次唯一性检查。索引的本质是用写入开销换查询加速,索引越多写入越慢——这也是「该不该给每个查询列都建索引」的答案来源。
回到答题现场,建议的结构:先一句话总览——主键是特殊的唯一索引,特殊在「每表一个、不允许 NULL、默认承担聚簇索引」;再按 NULL 语义、数量限制、聚簇与回表、外键引用四个维度展开;最后补写入代价收尾。聚簇和回表是区分度的来源,多数人止步于 NULL。
留个实务问题给你:用户表的邮箱列要求不允许重复,你会把它设成主键还是唯一索引?如果这张表已经有一个自增主键,答案会变吗?为什么?