跳到主要内容

MySQL主键选择

MySQL 主键选择,常用自增主键,分布式服务可能会选择 uuid、雪花 ID。

一、MySQL 主键选择

MySQL 自增主键(AUTO_INCREMENT)的最大大小取决于其数据类型,不同整数类型有不同的取值范围。实际使用中,自增主键通常采用整数类型(如  INTBIGINT  等),且推荐使用  UNSIGNED(无符号)  类型以获得更大的取值范围。

1、常见自增主键类型的最大取值

以下是 MySQL 中支持自增的主要整数类型及其最大大小(UNSIGNED  无符号类型为推荐,取值范围更大):

数据类型有符号(SIGNED)最大取值无符号(UNSIGNED)最大取值适用场景
TINYINT127255极小表(如配置表,行数 ≤255)
SMALLINT3276765535小表(行数 ≤6 万)
MEDIUMINT838860716777215中等表(行数 ≤1600 万)
INT(最常用基础类型)21474836474294967295常规表(行数 ≤42 亿)
BIGINT(大数据量表)922337203685477580718446744073709551615超大表(行数 ≤18 万亿)

2、类型选择需提前规划

  • 小表(如配置表、字典表):SMALLINT UNSIGNED  足够;
  • 常规业务表(如用户表、订单表):若预估行数 ≤42 亿,用  INT UNSIGNED;一般推荐这个,分库分表
  • 超大表(如日志表、流水表,行数可能超百亿):必须用  BIGINT UNSIGNED,避免后期溢出。

二、MySQL 主键三个核心要求

1、唯一性

不管是自增 ID、雪花 ID 还是 UUID,首先得保证 “不重复”。

2、有序性 - 性能的关键

聚簇索引,要是主键是有序的,比如自增 ID 1、2、3、4……InnoDB 插入数据的时候,就知道直接往最后面加就行,跟排队似的,顺着来,效率特别高。

但要是主键是无序的,例如 UUID 那种长得乱七八糟的字符串,插入的时候就麻烦了,InnoDB 得先找这个 ID 该插在哪个位置,可能插在中间某个地方,这时候就需要 “页分裂”。啥是页分裂?你可以想象成书架上的书排满了,你要把一本新书插进中间,就得先把后面的书都往后挪一挪,腾出地方。数据量小的时候还好,数据量大了,这挪来挪去的功夫可就多了,插入速度会越来越慢,索引还会变得特别臃肿。

3、足够小

主键是要存在索引里的,而且二级索引(比如你建的 name 索引、age 索引)里存的也是主键的值。要是主键占用空间大,比如 UUID 是 36 个字符,那索引文件就会变得特别大,不仅占磁盘空间,还会影响查询速度。

三、UUID 适合做主键嘛?

uuid 信息

全称:通用唯一识别码,大小 36 个字符。mysql 和 后端服务都可以生成。

如果把 uuid 当作 mysql 主键,主要有以下三个问题:

问题 1:无序性直接触发"页分裂地狱"

在 MySQL 中,页分裂(Page Split)  是 InnoDB 存储引擎在管理 B+ 树索引时,当一个数据页(Page)无法容纳新插入的数据时,将该页拆分为两个新页的过程。它是维持 B+ 树结构平衡和有序性的重要机制。

1. InnoDB 的页结构

InnoDB 中,数据和索引都以 “页” 为基本存储单位(默认页大小为 16KB)。B+ 树的每个节点(包括叶子节点和非叶子节点)都对应一个页,页内存储多条有序记录(按索引键排序)。

2. 页分裂的触发

当插入新记录时,InnoDB 会先找到该记录应存放的目标页:

  • 如果目标页有足够空间,直接插入即可;
  • 如果目标页已满(或剩余空间不足以容纳新记录),则触发页分裂
    • 将原页拆分为两个新页,把原页中的部分记录(通常是后半部分)迁移到新页;
      • 新记录插入到合适的页中(原页或新页);
      • 同时更新父节点(B+ 树的上层节点)的指针,使其分别指向两个新页,维持树的结构。

3. 页分裂的影响

  • 正面影响:保证 B+ 树始终有序且相对平衡,避免单页过大导致查询效率下降,维持索引的高效查询能力。
  • 负面影响
    • 分裂过程需要消耗额外的 IO 和 CPU 资源,可能短暂影响插入性能(尤其是高频插入场景);
    • 分裂后两个页的空间利用率可能降低(例如原页满时分裂,两个页可能各用 50% 空间),长期可能产生索引碎片,影响整体存储效率。

4. 索引碎片率

一个测试:用 UUID 当主键,往 MySQL 里插入 100 万条数据,前 10 万条的时候还挺顺畅,插入速度大概每秒 1 万条;但到了 50 万条之后,速度就掉到每秒 3000 条了;到 100 万条的时候,每秒只能插 1000 多条,而且磁盘 IO 占用率直接飙到 90% 以上。

反观用自增 ID 做主键,插入 100 万条数据,速度一直稳定在每秒 1.5 万条左右,索引碎片率只有 5% 不到。

问题 2:uuid 36 个字符,浪费资源

UUID 是 36 个字符,要是用 VARCHAR (36) 存储,每个 UUID 要占用 36 个字节。

假设你有一张用户表,有 1000 万条数据,主键是 UUID,那光主键索引就要占用 1000 万 × 36 字节 = 360MB。要是再建几个二级索引,比如 name、phone、email,每个二级索引里都要存主键的值,那每个二级索引又要多占 360MB,几个索引加起来,光索引文件就好几 GB 了。

换成自增 ID,用 BIGINT 类型(8 个字节),同样 1000 万条数据,主键索引只需要 1000 万 × 8 字节 = 80MB,二级索引也跟着变小。同样的磁盘空间,能装下更多的数据和索引。如果用 uint 4 字节,则只需 40MB 即可。

如果把 uuid 转成 二进制存储,占有空间确实小了。但可读性极差,后续不好维护。

问题 3:查询性能差,尤其是范围查询

主键查询时:

  • 使用自增 ID,由于 ID 有序,范围查询更方便、更快。
  • 如果主键是 uuid,uuid 无序,按照主键范围查询时,会比较慢。

四、雪花 ID 适合做主键嘛?

雪花 ID。雪花 ID 是 Twitter 搞出来的一种分布式 ID 生成算法,结构是 64 位的长整型(BIGINT),格式大概是这样的:

  • 1 位符号位:固定 0,因为 ID 是正数
  • 41 位时间戳:能表示大概 69 年的时间(从某个起始时间开始算)
  • 10 位机器 ID:能表示 1024 台机器
  • 12 位序列号:每台机器每秒能生成 4096 个 ID(12 位最多 4095)

雪花 ID 的优点很明显:是有序的(因为有时间戳)、占用空间小(8 字节,和自增 ID 一样)、能保证分布式环境下唯一;但其实并不是,坑比较多。

问题 1:时钟回拨是 “致命伤”

雪花 ID 的有序性,全靠前面的 41 位时间戳。但要是生成 ID 的机器出现 “时钟回拨”,麻烦就大了。

时钟回拨:就是机器的系统时间突然往后跳了,比如本来是 2025 年 8 月 27 日,突然变成 2025 年 8 月 25 日了。这可能是因为机器同步了 NTP 服务器时间,也可能是系统出了故障。例如:时钟回拨的时间比较长,还可能生成重复的 ID。

问题 2:机器 ID 配置不当,分分钟重复

雪花 ID 的 10 位机器 ID,能表示 1024 台机器。但要是你配置机器 ID 的时候不小心,把两台机器配置成了同一个 ID,那这两台机器生成的雪花 ID 就会重复,插入 MySQL 的时候就会报主键冲突。

问题 3:在某些场景下,有序性也会出问题

雪花 ID 的有序性,是 “相对有序”,不是 “绝对有序”。因为它的排序优先级是:时间戳 > 机器 ID > 序列号。 也就是说,在同一毫秒内,不同机器生成的 ID,会按机器 ID 排序;同一机器同一毫秒内生成的 ID,会按序列号排序。

这在大部分场景下没问题,但要是你有 “严格按生成时间排序” 的需求,就可能出问题。比如你有一个订单表,要求订单 ID 严格按下单时间排序,要是两台机器在同一毫秒内生成订单 ID,机器 ID 大的那个,就算下单时间稍晚,ID 也会更大,导致订单 ID 的顺序和实际下单时间的顺序不一致。

虽然这种情况出现的概率不高,但要是你的业务对 ID 的时间顺序要求特别严格(比如金融场景),那雪花 ID 就不太合适了。

问题 4:迁移数据非常麻烦

要是你用雪花 ID 当主键,后续迁移数据的时候,比如把数据从旧库迁到新库,或者分库分表,就会遇到一个麻烦:雪花 ID 是在应用层生成的,不是数据库生成的,所以迁移的时候,你得保证新库的 ID 和旧库一致,不能重复,也不能漏。

而要是用自增 ID,数据库会自动生成唯一的 ID,迁移的时候只需要把数据导过去就行,不用管 ID 的问题。

五、mysql 主键如何选择

方案一、小项目/单机项目:使用自增 ID 即可

自增 ID 的优点:

  • 完全符合 MySQL 主键的三个要求:唯一(数据库保证)、有序(每次 + 1)、占用空间小(8 字节)。
  • 零配置:不用自己写代码生成 ID,数据库自动搞定,省事儿。
  • 性能好:插入速度快,查询速度快,索引碎片少。
  • 方便迁移:迁移数据的时候不用管 ID,数据库自动生成。

自增 ID 缺点:

  • 分布式环境下不唯一:要是你有多个 MySQL 实例,每个实例都自增,就会出现重复的 ID。
  • 容易被猜到:比如你的用户 ID 是自增的,别人很容易猜到你有多少用户,也容易通过 ID 遍历数据(比如从 1 开始,依次访问 /user/1、/user/2)。

方案 2、分布式项目,数据库分段自增 ID

项目是分布式的,需要多台 MySQL 服务器(比如分库分表),自增 ID 就不够用了,这时候可以用 “数据库分段自增 ID”。

啥是数据库分段自增 ID?简单说就是:专门建一个 “ID 生成器” 数据库,里面有一张表,记录每个业务表的 ID 当前最大值和步长,每次应用需要生成 ID 的时候,就去这个表拿一段 ID(比如拿 1000 个),然后在应用里自己慢慢用,用完了再去拿下一段。

方案的优点:

  • 有序性:ID 是连续的,符合 MySQL 主键的要求,不会出现页分裂。
  • 分布式唯一:因为所有应用都从同一个 ID 生成器拿 ID,所以不会重复。
  • 性能好:每次拿一段 ID,不用每次生成 ID 都访问数据库,减少数据库压力。
  • 配置简单:不用依赖 ZooKeeper、Etcd 这些分布式协调工具,只需要一个数据库就行。

缺点也有,就是 ID 生成器数据库是单点,要是这个数据库挂了,所有需要生成 ID 的业务都得停。不过可以搞主从复制,主库挂了就切从库,解决单点问题。

方案 3:高并发场景,Redis 生成 ID

项目并发特别高,比如秒杀活动,每秒要生成几万甚至几十万的 ID,数据库分段自增 ID 可能会有点吃力(因为每次拿段 ID 都要访问数据库),这时候可以用 Redis 生成 ID。

Redis 生成 ID 的原理很简单:利用 Redis 的 INCR 命令(原子性递增),每次生成 ID 的时候,就调用 INCR 命令,让某个键的值加 1,这个值就是新的 ID。

比如生成用户 ID:

  1. 先在 Redis 里设置一个键,初始值为 0: SET user_id 0
  2. 每次需要生成用户 ID 的时候,调用 INCR 命令: INCR user_id

这样每次调用 INCR,都会返回一个唯一的、有序的 ID。为了提高性能,也可以像数据库分段自增那样,一次性从 Redis 拿一段 ID,比如拿 1000 个: INCRBY user_id 1000

这样就能拿到一段 ID(比如从 1001 到 2000),然后在应用里自己慢慢用。Redis 生成 ID 的优点:

  • 性能极高:Redis 是内存数据库,INCR 命令的性能特别好,每秒能处理几十万次请求,完全能满足高并发场景。
  • 有序性:ID 是连续递增的,符合 MySQL 主键要求。
  • 分布式唯一:所有应用都访问同一个 Redis,不会出现 ID 重复。

缺点:

  • 需要依赖 Redis:要是 Redis 挂了,ID 生成就会出问题,所以得搞 Redis 集群,保证高可用。
  • 数据持久化问题:要是 Redis 没有持久化,或者持久化失败,Redis 重启后,ID 会从之前的值开始,可能会重复。所以需要开启 Redis 的 AOF 持久化,并且配置合适的持久化策略。

这种方案适合高并发场景,比如秒杀、直播带货这些需要快速生成大量 ID 的业务。

方案 4、分表时,可以根据业务进行分

如果不强依赖主键 ID,不要求主键唯一性。例如:分库分表,可直接用自增 ID

六、总结

  • 小项目 / 单机项目:直接用自增 ID,简单高效,不用瞎折腾。
  • 分布式项目(中低并发):用数据库分段自增 ID,稳定可靠,配置简单。
  • 分布式项目(高并发):用 Redis 生成 ID,性能极高,能扛住大流量。
  • 要是你实在想用雪花 ID:那一定要做好时钟回拨处理和机器 ID 配置,并且接受它可能带来的迁移麻烦和排序问题。

所以:

1、优先使用自增 ID 作为主键,可以利用数据库自增 ID 或者 redis INCRBY key 自增。 2、放弃使用 uuid 作为自增主键, 3、可以考虑使用雪花 ID 作为主键。