二级索引叶子节点只存索引列值和对应行的主键值,不存整行数据;主键长度直接影响二级索引大小与性能,如uuid主键会导致索引膨胀、页分裂、缓存效率下降。

二级索引叶子节点里存了什么
二级索引(比如 KEY idx_email (email))的每个叶子节点,实际只存两样东西:email 值 + 对应行的主键值。它不存整行数据,也不存其他字段——这是和聚簇索引最根本的区别。
也就是说,只要主键变长,所有二级索引的每个叶子项就跟着变胖。比如:
- 主键是
INT(4 字节):一个email VARCHAR(255)索引项 ≈ 30 字节(email 平均长度) + 4 字节 = 34 字节左右 - 主键是
CHAR(36)UUID:同样索引项 ≈ 30 + 36 = 66 字节,翻倍还不止
16KB 的数据页能塞下的索引项数量,直接被主键长度“掐着脖子”往下压。页数一多,扫描、范围查询、缓存命中率全受影响。
主键长度浮动会让缓冲池“喘不过气”
InnoDB 缓冲池(innodb_buffer_pool_size)按页(16KB)为单位缓存索引数据。如果主键是 VARCHAR(64) 这类可变长字段,实际存储可能是 1 字节也可能是 64 字节——那同样一个索引页,在不同时间点缓存的“有效数据量”波动很大。
这种不稳定性会拖累 LRU 淘汰策略:缓存里混着大量“半空页”,看起来占了内存,但真正能服务查询的有效索引项却不多。结果就是看似缓存够大,Buffer pool hit rate 却上不去。
自增 INT 或 BIGINT 主键则完全规避这个问题:长度固定、可预测、页填充率稳定。
UUID 做主键时二级索引空间膨胀的真实代价
用 CHAR(36) 存 UUID,不只是多占 32 字节那么简单。真实代价包括:
-
UUID()生成的字符串是无序的 → 插入必然引发页分裂 → 新页写入位置随机 → 更多物理 IO,更多碎片页 - 每个二级索引都重复存一遍这个 36 字节主键 → 表有 3 个二级索引?那主键值就被冗余存储 3 次
- 若开启
innodb_file_per_table=ON,SHOW TABLE STATUS里的Data_length可能不大,但Index_length会远超Data_length—— 这是典型的索引设计失衡信号
实测中,同一批数据,CHAR(36) 主键表的 Index_length 常比 INT 主键表高出 80%~120%,尤其在二级索引多、字段本身也大的场景下。
不是不能用字符串主键,但得知道怎么“减负”
业务强依赖全局唯一 ID(比如分布式发号),又不想放弃性能,可以绕开主键膨胀问题:
- 主键仍用
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT - 另建
uuid_bin BINARY(16) NOT NULL字段,用UUID_TO_BIN(UUID(), TRUE)写入(打乱时间位,提升局部性) - 在
uuid_bin上建唯一索引:UNIQUE KEY uk_uuid_bin (uuid_bin) - 查询时走该二级索引,回表成本只多一次
BIGINT主键查找,而非 36 字节字符串比较
这样既保留了业务所需的唯一标识能力,又没把二级索引拖进空间和性能的双重泥潭。最容易被忽略的是:主键一旦定型,后续改造成本极高——选型阶段多花十分钟想清楚,比线上扛着慢查询调优三个月更划算。











