InnoDB 随机 UUID 主键:聚簇写入与二级索引成本
把主键从自增 BIGINT 换成随机 UUID,通常不只是“8 字节变 16 字节”。在 InnoDB 中,主键决定聚簇记录的组织,二级索引还会携带主键值。表上二级索引越多,主键宽度的影响越容易被放大。
这不意味着 UUID 一定不能用。真正要比较的是:编号在哪里生成,写入位置是否集中,外部系统是否需要全局标识,以及你愿意承担多少索引维护成本。
先比较相同业务字段的两种结构
以下是 MySQL 8.4、InnoDB 的独立实验结构,不包含任何性能测量结论:
CREATE DATABASE primary_key_lab;
USE primary_key_lab;
CREATE TABLE event_integer (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
external_id BINARY(16) NOT NULL,
tenant_id INT NOT NULL,
created_at DATETIME(6) NOT NULL,
payload VARCHAR(200) NOT NULL,
UNIQUE KEY uk_external (external_id),
KEY idx_tenant_time (tenant_id, created_at)
) ENGINE=InnoDB;
CREATE TABLE event_uuid (
id BINARY(16) NOT NULL PRIMARY KEY,
tenant_id INT NOT NULL,
created_at DATETIME(6) NOT NULL,
payload VARCHAR(200) NOT NULL,
KEY idx_tenant_time (tenant_id, created_at)
) ENGINE=InnoDB;
SET @u = UUID_TO_BIN('7865acf2-80fa-4b35-b201-cd9ca7b09fa8');
INSERT INTO event_integer(external_id, tenant_id, created_at, payload)
VALUES (@u, 7, '2026-10-01 12:00:00', 'sample');
INSERT INTO event_uuid(id, tenant_id, created_at, payload)
VALUES (@u, 7, '2026-10-01 12:00:00', 'sample');
第一种保留短的内部聚簇主键,UUID 作为外部唯一标识;第二种直接用 UUID 聚簇。第一种多维护了一棵 UUID 唯一索引,所以不能仅比较主键字段长度,就宣布它的总空间一定更小。必须把全部索引一起算。
两个结构的二级索引 idx_tenant_time 都需要能定位聚簇行,因此主键值也进入它的记录。选宽主键的成本不只出现在主键索引里,尤其不能用业务列宽相同就推断所有索引页大小相同。
随机写入与顺序写入的差异发生在页上
递增整数通常把新记录推向索引的一端,随机 UUID 则可能把插入分散到许多叶子页。需要访问的页不在缓冲池时,读入、修改及后续刷盘的范围可能扩大;页空间不足时还可能发生页分裂。
这条机制不等于一个固定倍数的吞吐差异。小表整体驻留内存、写入频率低或业务瓶颈在别处时,差异可能不显著。反过来,持续写入、大索引和有限缓冲池,更值得用相同数据量与并发模型做对照。
顺序主键也有自己的竞争热点,不能把“顺序”直接翻译成无限扩展。不同存储、连接并发和事务大小下,实际等待点可能不同。选择应依赖可复现测试,而不是复制一张未注明环境的性能图。
UUID 的版本与编码不能混讲
MySQL 的 UUID() 生成的值与应用生成的随机 UUID v4 不是同一种排序特征。拿 UUID_TO_BIN(UUID()) 做插入样本,却把结果解释为随机 v4 的成本,实验前提就错了。
UUID_TO_BIN 的交换参数针对特定时间布局,不能被当作“给任何 UUID 排序”的通用开关。若应用采用另一种有序标识方案,必须核对该方案字节顺序、时间信息和实现的实际排序规则,再决定怎么存。
对于固定 16 字节的 UUID,BINARY(16) 可以避免把连字符文本作为聚簇键存储。展示时再转换:
SELECT id, BIN_TO_UUID(external_id) AS external_uuid
FROM event_integer;
SELECT BIN_TO_UUID(id) AS uuid_value FROM event_uuid;
但二进制存储也要求接口、驱动、导出和排障工具对编码保持一致。一个地方交换了字节顺序、另一个地方没有交换,可能导致看似同一个字符串在数据库中无法命中。
用查询路径选择,而不是让内部主键承担所有职责
如果大多数查询由租户和时间范围进入,短内部主键加外部 UUID 是合理候选;如果多个节点必须离线生成标识并直接合并写入,使用全局标识作主键也有充分理由。两者之间还需要比较额外唯一索引、回表、外部引用和迁移成本。
验证时保留相同业务数据、相同二级索引和相同事务批次。分别观察总索引空间、写入延迟分布、缓冲池读取及查询路径,而不是只跑一轮单线程插入。填充表后还应执行典型读取,避免把“写得快”误当整套访问协议更好。
现有表迁移主键更不能只改一列。外键、CDC 主键识别、缓存键和应用对象映射都可能依赖原值。通常需要明确新旧标识映射和兼容窗口,再做回填及切换;更换聚簇主键涉及数据组织变化,也要预算重建空间。
自增 BIGINT 与随机 UUID 的取舍,没有一个脱离访问模式的正确答案。值得坚持的判断是:先区分外部身份和内部存储路径,再把主键对每一棵二级索引的影响算进去。