SQL 主键选择自增 ID vs UUID vs 雪花 ID vs 自然键的优缺点权衡 2026

15次阅读

分布式场景下必须禁用数据库自增 id,改用雪花 id 等全局唯一方案;uuid 因无序性导致页分裂,性能差 3–5 倍;自然键作主键易引发级联更新和性能问题。

SQL 主键选择自增 ID vs UUID vs 雪花 ID vs 自然键的优缺点权衡 2026

自增 ID 在分布式写入时会直接卡死

单库单表用 auto_increment 最省心,但一旦分库分表或接入多个写节点,ID 生成就变成中心瓶颈。MySQL 的 auto_increment_offsetauto_increment_increment 只能做简单轮询,无法应对动态扩缩容;PostgreSQL 的 serial 更是完全不支持跨实例协调。

常见错误现象:Duplicate entry 'X' for key 'PRIMARY' 或写入延迟飙升,本质是应用层抢锁或代理层(如 ProxySQL、ShardingSphere)没正确透传自增策略。

  • 只在单主 MySQL + 无分片场景下放心用 auto_increment
  • 分库后必须禁用所有节点的 auto_increment,改由中间件或应用层统一分配
  • PostgreSQL 若用 IDENTITY 列,务必确认是否启用了 GENERATED ALWAYS —— 否则 COPY 或批量插入可能绕过生成逻辑

UUID v4 写性能差不是因为长度,而是页分裂

UUID 字符串本身占 36 字节(含连字符),但真正拖慢的是无序性:新记录的 UUID 值随机散落在 B+ 树各处,导致频繁页分裂、缓存命中率骤降。实测 MySQL 下,同等数据量插入吞吐比自增 ID 低 3–5 倍。

使用场景:适合只读多、写入少,且需要客户端离线生成 ID 的场景(比如移动端离线草稿同步)。

  • 务必用 BINARY(16) 存储,而不是 VARCHAR(36) —— 能省一半空间,索引效率提升明显
  • 避免用 UUID_SHORT():它依赖服务器启动时间戳,集群中多实例部署极易重复
  • PostgreSQL 的 gen_random_uuid() 需要 pgcrypto 扩展,别漏装

雪花 ID 必须自己校验时间回拨,数据库不帮你兜底

雪花 ID 的 64 位结构里,时间戳占 41 位,精度为毫秒。一旦机器时钟回拨(NTP 同步异常、运维误操作),worker_id 相同的节点就会生成重复 ID。MySQL 和 PostgreSQL 都不会拦截这种重复,只会抛出 Duplicate entry 错误。

参数差异:epoch 起始时间选错会导致 ID 过早溢出(如用 Unix epoch 1970 年,41 位只能撑到 2039 年);worker_id 分配若依赖 ZooKeeper 或 etcd,要额外处理节点上下线时的 ID 段回收。

  • 应用层生成 ID 后,必须先查一次数据库确认唯一性,再执行 INSERT —— 尤其在金融类强一致性场景
  • 别把 worker_id 硬编码进配置,应从服务发现组件动态获取,否则扩容时容易冲突
  • MySQL 8.0+ 支持函数索引,可用 CREATE INDEX idx_snowflake_ts ON t1 ((CAST((id >> 22) AS UNSIGNED))) 加速按时间范围查询

自然键当主键?先看更新频率和业务耦合度

用邮箱、身份证号、订单号这类业务字段做主键,表面省了 ID 列,实际埋雷:一旦邮箱改绑、身份证升位、订单号规则变更,整个外键链路全得跟着级联更新。而且这些字段通常带语义,索引体积大、比较慢。

性能影响:MySQL 的二级索引叶子节点存的是主键值,自然键越长,二级索引越大;PostgreSQL 的 TOAST 机制虽能压缩大字段,但主键仍需全程参与排序和连接。

  • 仅当该字段绝对不可变、全局唯一、且长度 ≤ 32 字节时才考虑(比如 ISO 国家代码 CHAR(2)
  • 订单号若含日期前缀(如 20260415-XXXX),时间局部性好,但仍是字符串比较,不如整型快
  • 千万别用 JSON 字段或逗号分隔字符串当自然键 —— 即使当前唯一,后续扩展性归零

最麻烦的其实是迁移成本:现有系统想从自然键切到雪花 ID,不只是改主键,所有关联表的外键、索引、应用层 DTO、缓存 key 设计都得动。这事没有银弹,得一行行代码核对。

text=ZqhQzanResources