深蓝黑背景封面:右侧大标题「主键和唯一索引,区别不止 NULL」,副标题「空值、数量、聚簇,三个维度分清」;左侧是两张对照卡片,中间夹一个 VS 圆标——上卡青色描边「主键 PRIMARY KEY」:每表至多一个、不允许 NULL、默认聚簇索引;下卡紫色描边「唯一索引 UNIQUE KEY」:一张表可以有多个、允许多个 NULL、二级索引需回表;卡片下方追问「NULL 能不能存?能存几个?这是笔试最常追问的点」。左上标注「笔试复盘 · 数据库」,右上胶囊标注「Day 07 · 11-14」 图 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 上。所以「不建主键等于没有聚簇索引」是误解——聚簇索引永远存在,区别只在用谁的键。这也解释了为什么主键该选短列:每个二级索引的叶子都要带一份主键值,主键越长,所有索引越膨胀。

深蓝黑背景概念图(以 MySQL InnoDB 为准)。左侧对比表四行:数量——主键每表至多一个,唯一索引可创建多个;NULL——主键不允许,唯一索引允许且多个 NULL 互不冲突(NULL 互不相同,不参与判等);索引结构——主键是聚簇索引、叶子节点存整行,唯一索引是二级索引、叶子节点存主键值;查询整行——主键直接命中,唯一索引先查到主键值、再按主键「回表」查一次。右侧叶子节点示意:聚簇索引叶子单元格为「主键 id | 整行数据」,二级索引叶子单元格为「唯一键 | 主键值」,一支标着「回表」的箭头从二级索引叶子指向聚簇索引叶子;下方说明 InnoDB 用主键组织聚簇索引,未建主键时选第一个非空唯一索引,否则生成隐藏 row id。底部规则条:外键列引用另一张表的主键或唯一键(被引用值必须唯一),两列类型需兼容,插入时校验引用存在、删除与更新受约束 图 2:NULL、数量、聚簇与回表机制全景。

维度三:外键到底引用谁。

选项二「外键必定是另一表的主键」把充分条件说成了必要条件。SQL 标准里,外键引用的是父表的候选键——主键或唯一键都行,核心要求是被引用值必须唯一,否则子表一行无法确定对应父表哪一行。引用唯一键完全合法。MySQL 的 InnoDB 历史上曾允许外键引用非唯一索引(对标准 SQL 的扩展),但官方文档已将其标记为废弃、默认拒绝,并建议尽快迁离——工程上也不要依赖这个行为。

配套的工程细节还有两条:外键列与被引用列的数据类型必须兼容(定长数值类型的大小和符号要一致,字符串的字符集与排序规则要一致);插入或更新子表行时,数据库会校验父表存在匹配的候选键值,父表的删除与更新则受 ON DELETE、ON UPDATE 子句约束。

维度四:索引不是免费的。

选项四「索引只影响查询、对插入影响不大」错得最「隐蔽」。每次插入或更新,数据库都要在索引的 B+ 树里定位、插入键值,可能引发页分裂;唯一索引还多一次唯一性检查。索引的本质是用写入开销换查询加速,索引越多写入越慢——这也是「该不该给每个查询列都建索引」的答案来源。

回到答题现场,建议的结构:先一句话总览——主键是特殊的唯一索引,特殊在「每表一个、不允许 NULL、默认承担聚簇索引」;再按 NULL 语义、数量限制、聚簇与回表、外键引用四个维度展开;最后补写入代价收尾。聚簇和回表是区分度的来源,多数人止步于 NULL。

留个实务问题给你:用户表的邮箱列要求不允许重复,你会把它设成主键还是唯一索引?如果这张表已经有一个自增主键,答案会变吗?为什么?