在MySQL表结构设计中,主键的选择直接影响数据库的读写性能和索引维护成本。UUID作为全局唯一的标识符,常被用于分布式场景下的主键生成,但MySQL官方并不推荐将其作为InnoDB引擎表的主键,这背后的核心原因和UUID的随机特性、B+树索引的工作机制密切相关。

UUID作为主键的核心问题
UUID是128位的无序字符串,常见的版本4 UUID由随机数字生成,本身没有顺序性。当使用UUID作为InnoDB表的主键时,会带来两个核心问题:随机IO和B+树索引页分裂。
随机IO的影响
InnoDB的主键索引是聚簇索引,数据行直接存储在主键索引的叶子节点中。如果主键是有序的,新插入的数据会追加到索引的末尾,只需要访问最新的索引页即可,属于顺序IO,性能很高。
但UUID是无序的,新插入的主键值可能远小于之前插入的值,也可能远大于,插入位置是随机的。这意味着每次插入都需要随机访问不同的索引页,产生大量随机IO。随机IO的寻道成本远高于顺序IO,会直接降低写入性能。
B+树索引页分裂的影响
InnoDB的B+树索引页默认大小是16KB,每个索引页存储一定数量的索引记录。当索引页满了之后,再插入新的记录就会触发页分裂:将一个页的数据拆分成两个页,同时上层索引节点需要新增记录指向新的页。
由于UUID是无序的,新插入的记录很可能插入到已经写满的索引页中间,频繁触发页分裂。页分裂会带来三个负面影响:
- 分裂过程需要移动大量数据,消耗额外的CPU和IO资源
- 分裂后会产生碎片空间,降低索引页的存储空间利用率
- 上层索引节点也可能因为新增记录触发分裂,甚至引发连锁分裂,进一步增加开销
对比自增主键的优势
自增主键是有序递增的,插入时永远追加到索引末尾,几乎不会触发页分裂,同时写入是顺序IO,性能远高于UUID主键。我们可以通过简单的测试对比两者的性能差异。
测试表结构
创建两个结构相同的表,一个使用自增主键,一个使用UUID主键:
-- 自增主键表 CREATE TABLE `user_auto` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL, `age` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- UUID主键表 CREATE TABLE `user_uuid` ( `id` varchar(36) NOT NULL, `name` varchar(50) DEFAULT NULL, `age` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
批量插入测试
向两个表分别插入10万条测试数据,观察插入耗时:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.UUID;
public class InsertTest {
public static void main(String[] args) throws Exception {
// 自增主键插入测试
long start1 = System.currentTimeMillis();
insertAutoIncrement();
long end1 = System.currentTimeMillis();
System.out.println("自增主键插入10万条耗时:" + (end1 - start1) + "ms");
// UUID主键插入测试
long start2 = System.currentTimeMillis();
insertUUID();
long end2 = System.currentTimeMillis();
System.out.println("UUID主键插入10万条耗时:" + (end2 - start2) + "ms");
}
private static void insertAutoIncrement() throws Exception {
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8", "root", "123456");
PreparedStatement ps = conn.prepareStatement("INSERT INTO user_auto (name, age) VALUES (?, ?)");
for (int i = 0; i < 100000; i++) {
ps.setString(1, "user" + i);
ps.setInt(2, i % 100);
ps.executeUpdate();
}
ps.close();
conn.close();
}
private static void insertUUID() throws Exception {
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8", "root", "123456");
PreparedStatement ps = conn.prepareStatement("INSERT INTO user_uuid (id, name, age) VALUES (?, ?, ?)");
for (int i = 0; i < 100000; i++) {
ps.setString(1, UUID.randomUUID().toString());
ps.setString(2, "user" + i);
ps.setInt(3, i % 100);
ps.executeUpdate();
}
ps.close();
conn.close();
}
}
实际测试中,自增主键的插入耗时通常在1-2秒,而UUID主键的插入耗时可能达到5-10秒,性能差距非常明显。同时查看UUID主键表的索引碎片率,也会远高于自增主键表。
特殊场景的替代方案
如果是分布式场景,确实需要全局唯一的主键,也不建议直接使用随机UUID,可以考虑以下替代方案:
- 使用有序的UUID,比如版本7的UUID,基于时间戳生成,具备有序性,减少随机IO和页分裂
- 使用雪花算法生成有序的分布式ID,兼顾唯一性和顺序性
- 使用自增主键作为物理主键,同时新增一个UUID字段作为业务唯一标识,给业务字段加唯一索引即可
总之,在MySQL InnoDB引擎下,除非有特殊业务需求,否则优先选择自增主键或者有序的分布式ID作为主键,避免使用无序的UUID,才能最大程度发挥B+树索引的性能优势。