MySQL 主键类型选型指南:自增、UUID、雪花算法怎么选
主键(Primary Key, PK)不仅是“唯一标识一行数据”的约束,更是 InnoDB 的物理组织方式:InnoDB 的表数据按主键构建聚簇索引(Clustered Index),数据行存放在主键 B+Tree 的叶子节点上。
这意味着:
- 主键的长度会放大所有二级索引(Secondary Index)的体积与维护成本(二级索引叶子节点存的是“二级索引键 + 主键值”)。
- 主键的有序性决定插入是否“尾插”还是频繁“页分裂”(page split),直接影响写入吞吐、锁竞争和碎片。
本文从工程实践出发,比较自增主键、UUID 及常见分布式 ID,并给出可落地的选型建议与 DDL 示例。
1. 选主键的核心目标
把主键选型拆成 6 个维度,更容易做“最优方案”的工程权衡:
- 写入性能:是否顺序递增、是否导致页分裂、是否放大二级索引维护。
- 存储体积:主键越大,所有二级索引越大,Buffer Pool 命中率越低。
- 分布式唯一性:多机房/多写入节点/离线导入是否需要“无中心生成”。
- 可读性与调试:是否方便排查、是否能按时间粗略排序。
- 安全性:ID 是否可枚举(自增 ID 在外部暴露时容易被遍历)。
- 迁移与合并:跨库合并、主从切换、双写、历史数据回灌是否容易冲突。
2. 常见主键方案对比
下面按“工程上最常见的几类主键”进行对比。
2.1 自增主键(BIGINT UNSIGNED AUTO_INCREMENT)
特点
- 单库单写(或主从复制)环境下最简单、性能最好。
- 值递增,插入高度“尾插”,对 InnoDB 友好。
- 整数短(通常 8 字节),二级索引额外负担小。
优点
- 写入吞吐高,页分裂少。
- 索引紧凑,Buffer Pool 利用率高。
- 业务实现简单,生态支持最好。
缺点
- 多写入节点(多主/多活/分库分表)下会有冲突,需要统一发号器或分段策略。
- 对外暴露时可枚举,可能泄露业务规模与数据存在性。
推荐场景
- 单主写、多从读、绝大多数 OLTP 系统。
- 主键不需要在应用侧提前生成。
DDL 示例
CREATETABLEorders(idBIGINTUNSIGNEDNOTNULLAUTO_INCREMENT,user_idBIGINTUNSIGNEDNOTNULL,statusTINYINTNOTNULL,created_atDATETIMENOTNULL,PRIMARYKEY(id),KEYidx_user_created(user_id,created_at))ENGINE=InnoDB;2.2 UUID(文本形态:CHAR(36)/VARCHAR(36))
特点
- 典型值形态如
550e8400-e29b-41d4-a716-446655440000。 - 字符存储大(36 字符 + 字符集开销),对索引极不友好。
- UUID 近似随机,插入在 B+Tree 上“到处落点”,页分裂多。
优点
- 去中心化生成,天然适合分布式唯一性。
- 对外暴露不易枚举。
缺点(工程上很关键)
- 索引膨胀严重:主键变大后,所有二级索引都跟着变大。
- 写入放大明显:随机插入导致页分裂与碎片,可能引发吞吐下降与延迟抖动。
推荐场景
- 非高写入、或数据量不大,且强依赖跨系统唯一性的场景。
- 更推荐使用二进制形态或可排序 UUID(见后文)。
2.3 UUID(二进制形态:BINARY(16))
特点
- 存储 16 字节,比
CHAR(36)紧凑很多。 - 仍可能是“随机插入”,但索引体积显著降低。
MySQL 8.0 推荐用法(减少随机写)
MySQL 8.0 提供UUID_TO_BIN()/BIN_TO_UUID(),并支持“交换时间字段”的优化,使 UUID 更接近按时间递增,减少页分裂:
CREATETABLEevents(idBINARY(16)NOTNULL,created_atDATETIMENOTNULL,payload JSONNOTNULL,PRIMARYKEY(id))ENGINE=InnoDB;INSERTINTOevents(id,created_at,payload)VALUES(UUID_TO_BIN(UUID(),1),NOW(),JSON_OBJECT('k','v'));SELECTBIN_TO_UUID(id,1)ASid_textFROMeventsLIMIT10;其中第二个参数1会对 UUID 的部分字节做重排,让新生成的 UUID 在索引上更“顺序”。
优缺点总结
- 相比
CHAR(36):空间与索引体积大幅改善。 - 相比自增:仍更复杂,写入性能通常略弱(尤其在高并发写入、热点二级索引多时)。
2.4 分布式递增 ID(Snowflake/号段/时间序)
这类方案的目标是兼得:
- 全局唯一(分布式可生成)
- 大体递增(减少页分裂,接近自增的写入局部性)
- 整数紧凑(通常 64-bit)
常见实现:
- 雪花算法(Snowflake):时间戳 + 机器号 + 序列。
- 号段模式(Segment/Leaf):集中服务批量发号,应用侧内存递增。
- ULID/KSUID:带时间前缀的可排序标识(常用 128-bit)。
优点
- 分布式唯一且近似递增,写入更平滑。
- 使用
BIGINT(8 字节)时索引紧凑。
缺点
- 实现复杂度更高,需要校时、机器号管理、容灾策略。
- 如果强依赖严格递增顺序,需要额外保证(多数方案仅“趋势递增”)。
推荐场景
- 分库分表、多活写入、跨系统合并。
- 高写入 OLTP,希望兼顾性能与全局唯一。
DDL 示例(以BIGINT存储分布式 ID)
CREATETABLEtrade(idBIGINTUNSIGNEDNOTNULL,buyer_idBIGINTUNSIGNEDNOTNULL,amountDECIMAL(18,2)NOTNULL,created_atDATETIMENOTNULL,PRIMARYKEY(id),KEYidx_buyer_created(buyer_id,created_at))ENGINE=InnoDB;2.5 业务主键 / 联合主键(Natural Key / Composite PK)
例如用(tenant_id, order_no)作为主键。
优点
- 语义强,天然防重(不需要额外唯一索引)。
- 某些查询天然命中主键。
缺点
- 主键更长,二级索引被放大。
- 业务字段一旦变更成本高(主键更新在 InnoDB 中代价大)。
- 多字段比较更慢,Join 成本更高。
推荐实践
- 更常见做法:使用**短的代理主键(Surrogate Key)**作为 PK,再对业务唯一字段建
UNIQUE约束。
CREATETABLEtenant_order(idBIGINTUNSIGNEDNOTNULLAUTO_INCREMENT,tenant_idBIGINTUNSIGNEDNOTNULL,order_noVARCHAR(64)NOTNULL,created_atDATETIMENOTNULL,PRIMARYKEY(id),UNIQUEKEYuk_tenant_orderno(tenant_id,order_no))ENGINE=InnoDB;3. 为什么“主键越短、越顺序”通常越好
3.1 二级索引会“携带”主键
InnoDB 的二级索引叶子节点存储的是主键值而不是行指针,所以:
- 主键从 8 字节变为 16 字节,所有二级索引都会变“更胖”。
- 主键从
BIGINT变为CHAR(36),二级索引体积可能成倍增长。
二级索引变胖会带来:更高的磁盘与内存消耗、更低的缓存命中、更高的写放大。
3.2 随机主键更容易页分裂
- 递增主键:新数据大多插在右侧,B+Tree 追加为主。
- 随机主键:插入点分布在整棵树,容易触发页分裂、页面搬迁,造成碎片与抖动。
这就是为什么“UUID 文本主键”在写入压力上去后,往往比自增慢很多。
4. 选型建议:按场景给结论
4.1 单库单写(绝大多数业务)
- 首选:
BIGINT UNSIGNED AUTO_INCREMENT - 对外暴露 ID 的安全诉求:
- 外部展示用单独的“展示 ID”(例如短链/编码),内部仍用自增主键。
- 或增加一列
public_id(UUID/ULID)并加唯一索引,作为对外标识。
4.2 分布式多写 / 分库分表 / 多活
- 首选:趋势递增的分布式
BIGINT(雪花/号段)作为主键。 - 备选:
BINARY(16)的可排序 UUID/ULID(写入与索引体积通常优于CHAR(36))。
4.3 写入极高 + 二级索引较多
- 强烈倾向:8 字节整数主键(自增或分布式
BIGINT)。 - 尽量避免:
CHAR(36)UUID 主键。
4.4 数据迁移、跨库合并频繁
- 若需要无冲突合并:分布式 ID(雪花/号段)或
BINARY(16)UUID。 - 若只在离线任务合并:也可以保留自增主键,但需要设计“映射表/重写主键”的迁移流程。
4.5 业务天然有稳定且短的唯一键
- 可以选择业务键做
UNIQUE,主键仍用短代理键。 - 只有在确认“字段稳定、不会变更、且长度非常短”时才考虑业务键做 PK。
5. 推荐的工程落地模板
模板 A:性能优先(内部主键自增,外部 ID 随机)
CREATETABLEuser_profile(idBIGINTUNSIGNEDNOTNULLAUTO_INCREMENT,public_idBINARY(16)NOTNULL,nicknameVARCHAR(64)NOTNULL,created_atDATETIMENOTNULL,PRIMARYKEY(id),UNIQUEKEYuk_public_id(public_id))ENGINE=InnoDB;写入时:
INSERTINTOuser_profile(public_id,nickname,created_at)VALUES(UUID_TO_BIN(UUID(),1),'alice',NOW());模板 B:分布式优先(分布式 BIGINT 主键)
CREATETABLEmessage(idBIGINTUNSIGNEDNOTNULL,room_idBIGINTUNSIGNEDNOTNULL,sender_idBIGINTUNSIGNEDNOTNULL,contentTEXTNOTNULL,created_atDATETIMENOTNULL,PRIMARYKEY(id),KEYidx_room_created(room_id,created_at))ENGINE=InnoDB;6. 常见坑与细节
- 不要用
INT省空间:大多数长期业务更建议从一开始就用BIGINT UNSIGNED,避免未来扩容与迁移。 - 不要把 UUID 文本当主键:
CHAR(36)主键通常是最差的组合(大 + 随机)。 - 主键尽量不要有业务含义:业务变化会让“带语义主键”变更成本极高。
- 二级索引越多,主键越要短:因为每个二级索引都要携带主键。
- 可排序 UUID 也不是完全顺序:高并发下仍可能产生一定随机性,但一般显著优于完全随机 UUID。
7. 一句话决策表
| 你的约束/目标 | 更优选择 |
|---|---|
| 单库单写、追求性能、实现简单 | BIGINT UNSIGNED AUTO_INCREMENT |
| 多写入节点、全局唯一、写入仍要稳 | 分布式趋势递增BIGINT(雪花/号段) |
| 必须应用侧生成、且希望索引别太大 | BINARY(16)+UUID_TO_BIN(UUID(), 1) |
| 要对外不可枚举,但内部仍要高性能 | 内部自增 PK + 对外public_id唯一索引 |
| 业务唯一键稳定且短 | PK 用代理键,业务键加UNIQUE(更常见) |
8. 结论
- 默认最优:
BIGINT UNSIGNED AUTO_INCREMENT(单写场景),简单、快、省。 - 分布式最优:趋势递增的分布式
BIGINT(雪花/号段),兼顾唯一与写入性能。 - UUID 的正确打开方式:尽量用
BINARY(16),并使用可排序/重排策略,避免CHAR(36)做主键。