表设计与范式
三大范式
| 范式 | 要求 | 违反的例子 |
|---|---|---|
| 1NF | 每列都是不可再分的原子值 | 用 地址 一列存「省市区街道」;用逗号分隔存多个标签 |
| 2NF | 满足 1NF,且非主键列完全依赖于整个主键(消除部分依赖) | 主键是 (订单号, 商品号),表里却有 订单日期(只依赖订单号) |
| 3NF | 满足 2NF,且非主键列之间不存在传递依赖 | 表里有 部门ID 又有 部门名称(部门名称依赖部门ID,不直接依赖主键) |
| BCNF | 3NF 加强版,要求所有决定因素都是候选键 | 面试很少深究 |
反范式化:故意冗余字段来避免 JOIN。典型场景是订单表里冗余 商品名称、商品价格 的快照 —— 这不只是性能优化,更是业务正确性要求:商品改价后,历史订单必须显示下单时的价格。
反范式的代价是数据一致性维护成本(冗余字段要同步更新)和存储空间。判断标准:读远多于写、且 JOIN 已成为瓶颈时才做。
数据类型选择
整数类型:
| 类型 | 字节 | 无符号上限 |
|---|---|---|
tinyint | 1 | 255 |
smallint | 2 | 65535 |
int | 4 | 约 42.9 亿 |
bigint | 8 | 约 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),避免拖慢主表的全字段查询
时间类型(高频):
datetime | timestamp | |
|---|---|---|
| 字节 | 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 标记保留数据,代价是所有查询都要带这个条件、且该列区分度低。数据量大时考虑归档到历史表而非永久保留。