主键索引与唯一索引的区别

主键索引(PRIMARY KEY)和唯一索引(UNIQUE)在 MySQL 中都属于约束型索引——既有唯一性约束作用,也具备索引加速查询的能力。但两者存在本质区别。

核心区别对比

对比维度主键索引(PRIMARY KEY)唯一索引(UNIQUE)
=NULL 值=不允许 NULL(任何列)允许 NULL,且允许多个 NULL
每表数量最多 1 个多个
聚簇性质InnoDB 中 主键即聚簇索引二级索引(非聚簇索引)
约束层级实体完整性约束(Entity Integrity)唯一性约束(Unique Constraint)
是否可以由外键引用✅ 可以❌ 不能直接引用
索引类型按数据存储方式分类按功能逻辑分类

1. NULL 值处理:最根本的差异

CREATE TABLE test (
    id INT PRIMARY KEY,
    email VARCHAR(100) UNIQUE
);
 
INSERT INTO test(id, email) VALUES (1, NULL);  -- ✅ 唯一索引允许 NULL
INSERT INTO test(id, email) VALUES (2, NULL);  -- ✅ 允许第二个 NULL(NULL ≠ NULL)
INSERT INTO test(id, email) VALUES (3, 'a@b.com');  -- ✅
INSERT INTO test(id, email) VALUES (4, 'a@b.com');  -- ❌ ERROR: Duplicate entry

SQL 标准中 NULL 的含义

NULL 不等于任何值,甚至不等于另一个 NULL。 因此在唯一索引中,多个 NULL 值并不违反唯一性约束。而在主键中,NULL 表示”未知”,违背了”每一行必须能被唯一标识”的实体完整性原则。

2. 聚簇性质:InnoDB 的特有关联

在 InnoDB 存储引擎中:

  • 主键索引 = 聚簇索引(Clustered Index)

    • 叶子节点直接存储完整行数据,数据按主键顺序物理组织
    • 决定了表数据的物理存储排列,因此一个表只能有一个主键
  • 唯一索引 = 二级索引(Secondary Index)

    • 叶子节点存储主键值,而非完整行数据
    • 查询需要两步:先通过唯一索引找到主键值,再回聚簇索引取数据(除非被覆盖索引覆盖)
graph TB
    subgraph 聚簇索引(主键)
        PK["id: int (PK) → 整行数据"]
    end
    subgraph 二级索引(唯一索引)
        UK["email: varchar (UNIQUE) → id: int (PK)"]
    end

    UK -->|"回表(如果必要)"| PK

性能影响

因为唯一索引是二级索引,每次通过唯一索引查询时,可能涉及回表操作。而主键索引直接定位到数据行,通常更快。使用覆盖索引可以消除唯一索引的回表开销。

3. 主键选择策略

推荐使用自增/序列主键

CREATE TABLE user (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,  -- ✅ 推荐的聚簇索引
    email VARCHAR(100) UNIQUE,
    name VARCHAR(50)
);
  • B+Tree 的新插入操作只在当前最右侧叶子节点追加页分裂概率低
  • 逻辑递增,保持聚簇索引的物理顺序连续性

不推荐使用业务字段做主键

-- ❌ 不推荐:用身份证号、手机号作为主键
CREATE TABLE user (
    id_card VARCHAR(18) PRIMARY KEY,
    name VARCHAR(50)
);
  • 业务字段长度长,导致二级索引体积大(二级索引存主键值)
  • 业务字段可能变更,但主键不应变更
  • UUID 做主键尤其不推荐:离散插入导致频繁页分裂、空间碎片

UUID 主键问题

UUID 字符串长(36 字节)、无序插入,会造成 B+Tree 频繁页分裂、数据碎片化、二级索引臃肿。若业务需要唯一标识,可考虑 UUID 作为唯一索引 + AUTO_INCREMENT 整数作主键的分离策略。

4. 语义和使用场景

场景应使用
唯一标识每行记录主键
保证业务字段不重复(如手机号、邮箱)唯一索引
需要外键引用主键
允许 NULL 的唯一性约束唯一索引
接口幂等性中的去重表唯一索引 + 业务流水号

相关笔记