跳到主要内容

表设计与范式

三大范式​

范式要求违反的例子
1NF每列都是不可再分的原子值用 地址 一列存「省市区街道」;用逗号分隔存多个标签
2NF满足 1NF,且非主键列完全依赖于整个主键(消除部分依赖)主键是 (订单号, 商品号),表里却有 订单日期(只依赖订单号)
3NF满足 2NF,且非主键列之间不存在传递依赖表里有 部门ID 又有 部门名称(部门名称依赖部门ID,不直接依赖主键)
BCNF3NF 加强版,要求所有决定因素都是候选键面试很少深究

反范式化:故意冗余字段来避免 JOIN。典型场景是订单表里冗余 商品名称、商品价格 的快照 —— 这不只是性能优化,更是业务正确性要求:商品改价后,历史订单必须显示下单时的价格。

反范式的代价是数据一致性维护成本(冗余字段要同步更新)和存储空间。判断标准:读远多于写、且 JOIN 已成为瓶颈时才做。

数据类型选择​

整数类型:

类型字节无符号上限
tinyint1255
smallint265535
int4约 42.9 亿
bigint8约 1844 亿亿
  • 能用小的不用大的 —— 不只是省磁盘:类型越小,单页放的行越多,同样 IO 读到更多数据,B+ 树更矮
  • int(11) 的 11 不是长度限制,只是 ZEROFILL 时的显示宽度,int(1) 和 int(11) 能存的范围完全相同(8.0.17 起已废弃该语法)
  • 状态、类型等枚举值用 tinyint 够了

小数类型:

  • 金额必须用 decimal,禁用 float / double。浮点数是二进制近似,0.1 + 0.2 != 0.3,财务对账会错
  • 或者用 bigint 存最小货币单位(分),业务层除以 100。性能更好,且规避了所有精度问题

字符串类型:

  • char(n) 定长,适合长度确定的值(MD5、手机号、国家码),不足补空格,取值时右侧空格被截断
  • varchar(n) 变长,额外用 1~2 字节存长度
  • text / blob 不能有默认值,且会导致隐式临时表落盘。大文本建议拆到单独的表里(主表只存 ID),避免拖慢主表的全字段查询

时间类型(高频):

datetimetimestamp
字节8(5.6 起 5 字节 + 小数秒)4
范围1000 ~ 9999 年1970 ~ 2038-01-19
时区原样存取,不做转换存 UTC,读取时按会话时区转换

timestamp 有 2038 年问题,且时区转换行为容易引发跨地区业务的隐性 bug。推荐用 datetime,或者直接用 bigint 存毫秒时间戳(跨语言跨时区最省心,代价是可读性差、不能用 SQL 的日期函数)。

慎用 enum —— 加一个枚举值要 ALTER TABLE(大表是 DDL 风险),且底层存的是序号,顺序变更会导致数据错乱。用 tinyint + 代码里的常量定义更可控。

索引设计规范​

  • 联合索引的列顺序:区分度高的放前面;等值条件放前面、范围条件放最后(见索引与 B+ 树的最左前缀)

  • 优先建联合索引而非多个单列索引 —— (a,b,c) 一个索引能顶 (a)、(a,b)、(a,b,c) 三种查询。MySQL 的索引合并(index merge)效果远不如联合索引

  • 避免冗余索引 —— 已有 (a,b) 就不需要再建 (a)

  • 长字符串用前缀索引 —— KEY idx_url (url(20))。要先算区分度确定长度:

    SELECT COUNT(DISTINCT LEFT(url, 20)) / COUNT(*) FROM t;

    取接近 1 的最短长度。注意前缀索引无法用于覆盖索引和排序

  • 区分度太低的列别建索引 —— 性别、是否删除这类只有几个值的列,单独建索引没用(优化器会走全表扫),但可以作为联合索引的一部分

  • 单表索引不超过 5~6 个,每个索引都要在写入时维护

主键设计​

自增主键是默认最优解:顺序写入、只在 B+ 树右侧追加、页分裂少、8 字节紧凑(所有二级索引的叶子都存主键,主键越大所有索引越大)。

分布式场景不能用自增(多库会冲突),候选方案:

方案有序问题
UUID v4❌ 完全随机页分裂严重、36 字节太大,最差选择
雪花算法 Snowflake✅ 趋势递增依赖机器 ID 分配,有时钟回拨问题
号段模式(美团 Leaf)✅要额外的发号服务
UUID v7 / ULID✅ 时间前缀仍是 16 字节
Redis INCR✅依赖 Redis 可用性

务必显式指定主键 —— 不指定时 InnoDB 会生成 6 字节隐藏的 row_id,且这是一个全局的计数器(所有无主键表共享),有并发竞争和溢出覆盖的风险。

其他约定​

字段尽量 NOT NULL DEFAULT —— NULL 需要额外的 NULL 标记位;NULL 参与任何比较都返回 NULL(WHERE col != 1 查不出 col IS NULL 的行,容易漏数据);COUNT(col) 不统计 NULL 行;索引列为 NULL 时统计和比较行为更复杂。

不用外键 —— 大厂规范基本都禁用物理外键。理由:每次写入都要检查关联表,锁的范围变大、并发下容易死锁;分库分表后外键无法跨库;DDL 和数据迁移受约束。改为在应用层保证引用完整性。

垂直拆分大表 —— 一张表字段过多(超过几十个)或含大字段时,按「访问频率」拆成主表 + 扩展表。主表只放高频小字段,让单页装更多行。

软删除 vs 物理删除 —— 加 is_deleted 标记保留数据,代价是所有查询都要带这个条件、且该列区分度低。数据量大时考虑归档到历史表而非永久保留。