Skip to content

字段类型选错了,表就废了一半 —— 表设计与约束范式 ​

属于 S1 MySQL 深入 · 基础篇第 3 章 上一篇:SQL 语法详解 下一篇:事务与索引入门

建表是"一锤定音"的事:表结构上线后再改,代价极高(大表 ALTER 锁表、历史数据要迁移)。面试里"给你一个用户表,你怎么设计"是必考题。这一篇把字段类型选型、约束、主键设计、范式与反范式一次讲透,全是能直接用的经验。


字段类型选型:选错类型的三种代价 ​

类型选错会带来三种代价:浪费磁盘/内存(BIGINT 存年龄)、精度错误(用 FLOAT 存金额)、隐式转换导致索引失效(字符串列存数字)。逐类过一遍:

整数类型 ​

类型字节范围(有符号)适用
TINYINT1-128 ~ 127状态码、年龄、开关(0/1)
SMALLINT2-32768 ~ 32767小范围计数
INT4±21 亿常规 ID、计数
BIGINT8±9.2×10¹⁸主键、订单号等大 ID

经验:主键一律 BIGINT UNSIGNED(自增不会轻易撞上限);状态/枚举用 TINYINT 而不是字符串(省空间 + 比较快);INT(11) 里的 (11) 只是显示宽度,不限制取值范围,别被误导。

小数类型:金额必须用 DECIMAL ​

类型特点坑
FLOAT / DOUBLE浮点,快精度会丢(0.1+0.2 ≠ 0.3)
DECIMAL(p, s)定点数,精确慢一点但绝对精确
sql
-- 金额、价格、汇率一律 DECIMAL(10, 2)(10 位有效数字,2 位小数)
price DECIMAL(10, 2) NOT NULL COMMENT '价格,单位元,精确到分'

面试追问:为什么余额计算不能用 FLOAT?—— 浮点数用二进制近似表示十进制小数,0.1 + 0.2 = 0.30000000000000004,累计运算后金额会错;DECIMAL 是字符串式定点存储,精确但占用空间更大、运算更慢。钱的精度 > 性能。

字符串:VARCHAR vs CHAR vs TEXT ​

类型特点适用
VARCHAR(n)变长,n 是字符数,占 n+1~2 字节绝大多数文本:姓名、邮箱、地址
CHAR(n)定长,不足补空格定长编码:手机号、订单号(长度固定)
TEXT / BLOB大文本/二进制文章正文(但通常建议拆表或走对象存储)
sql
name VARCHAR(64)  NOT NULL COMMENT '姓名'   -- 64 个字符,不是 64 字节
phone CHAR(11)    NOT NULL COMMENT '手机号' -- 定长,无碎片,检索快

坑 1:VARCHAR(n) 的 n 是字符数,utf8mb4 下最多 65535/4 ≈ 16383 字符,别超。 坑 2:TEXT 列不能有默认值、建索引要指定前缀长度(INDEX (content(100))),大文本列混在主表里会拖慢全表扫描——正文、日志这类数据建议单独拆表或直接上对象存储。

日期时间:DATETIME vs TIMESTAMP ​

类型范围存储时区适用
DATETIME1000~9999 年8 字节与时区无关业务时间,首选
TIMESTAMP1970~2038 年4 字节跟随会话时区有跨时区显示需求时

经验:默认用 DATETIME(范围大、不受 2038 问题影响);要"UTC 存储、本地展示"的用 TIMESTAMP。存"创建/更新时间"统一 DEFAULT CURRENT_TIMESTAMP。

其他值得知道的类型 ​

  • JSON(8.0+):存结构化但格式不固定的数据(如埋点、扩展属性);可以 JSON_EXTRACT 查询,但别拿它当主查询条件(无法走常规索引,要配合虚拟列/多值索引)。
  • BOOL:本质是 TINYINT(1)。
  • ENUM:有顺序的枚举,能用但扩展要 ALTER,加枚举值锁表,慎用;状态码建议 TINYINT + 注释/字典表。

约束:数据正确的最后防线 ​

约束作用写法
NOT NULL非空name VARCHAR(64) NOT NULL
UNIQUE唯一UNIQUE KEY uk_email (email)
PRIMARY KEY主键(唯一+非空,聚簇索引)PRIMARY KEY (id)
FOREIGN KEY外键,保证引用完整性FOREIGN KEY (dept_id) REFERENCES departments(id)
CHECK值范围校验(8.0 真正生效)CHECK (age >= 0 AND age <= 150)
DEFAULT默认值DEFAULT 0

两个高频观点:

  1. 外键:互联网大厂基本禁用。外键让数据库在每次插入/删除时做完整性检查,高并发下是性能杀手;而且分库分表后外键直接失效。正确做法:业务层保证引用关系(代码里先查父表存在再插入),数据库只保留逻辑关联(普通索引)。
  2. CHECK 约束:5.7 及以前 CHECK 只解析不生效(形同虚设),8.0 才真正执行。依赖版本,别把业务校验全押在它身上。

主键设计:自增 vs UUID 的世纪之争 ​

主键在 InnoDB 里就是聚簇索引,它的选择直接影响整张表的写入性能(聚簇索引原理见深入篇《存储引擎与 B+ 树》):

方案优点缺点结论
BIGINT AUTO_INCREMENT顺序插入、页不分裂、性能最好可被遍历猜测(可配合业务做混淆)、跨库合并会冲突单库首选
UUID(字符串)全局唯一、适合分布式合并随机顺序 → 频繁页分裂 → 写放大;32 字符占空间分布式场景考虑
雪花 ID / 雪花变体(BIGINT)全局唯一 + 趋势递增 + 数字型需要 ID 生成服务(见高并发场景题)分布式首选

一句话:单机用自增 BIGINT;分布式用趋势递增的雪花 ID(BIGINT 存储);纯 UUID 字符串做主键是最差选择(随机写 + 大索引)。订单号、流水号这类"业务可见 ID"应单独设唯一索引列,与自增主键分离。

三范式:要守,但更要会"反" ​

范式是"消除冗余"的设计规范,三个级别:

范式要求通俗理解
1NF列不可再分一个字段只存一个值(别用逗号拼多个手机号)
2NF消除部分依赖联合主键下,非主键列不能只依赖主键的一部分
3NF消除传递依赖非主键列不能依赖其他非主键列(如 city 依赖 province,不该都放同一张订单表)

但互联网业务讲究反范式:为了查询性能,故意冗余。经典案例:订单表里冗余一份"商品名称 + 商品快照价"——因为商品信息会变,订单里必须存下单那一刻的快照;用户信息大表拆"热表/冷表";统计字段(如帖子回复数)冗余存储而不是每次 COUNT。

面试观点:范式是"逻辑正确"的底线(至少守到 3NF 的语义),反范式是"性能优化"的手段。设计顺序是:先按 3NF 建模,再针对高频查询路径做有意识的冗余/拆表,而不是一上来就到处冗余。

命名规范与设计陷阱清单 ​

命名规范(大厂风格,面试写出来加分):

  • 库名/表名/字段名:小写 + 下划线(user_profile),不用驼峰、不用保留字。
  • 表名复数或单数统一一种;前缀区分模块(order_, user_)。
  • 索引命名:idx_字段名、uk_字段名(唯一)、idx_字段1_字段2(联合)。
  • 必备字段:id、created_at、updated_at,可选 is_deleted(逻辑删除)、version(乐观锁)。

设计陷阱清单(每一条都是真实事故):

陷阱说明
预留字段field1, field2 预留列——永远不知道类型,且没法建索引;要扩展直接 ALTER 加列(8.0 INSTANT 很快)
用字符串存日期/数字浪费空间 + 无法用日期/数值函数 + 排序错误('10' < '9')
大字段混主表TEXT 大文本拖慢全表扫描,拆表或走对象存储
冗余索引idx_a 和 idx_a_b 同时存在:前者完全没用(最左前缀已被覆盖),白占写放大
索引过多每个索引都是写放大 + 内存占用;单表索引数建议 < 5
无注释三个月后没人知道 status=3 是什么意思
无 created_at/updated_at排查问题、对账时抓瞎

串起来 ​

表设计这一关的核心判断力是:类型选对(金额 DECIMAL、主键 BIGINT、中文 utf8mb4)、约束用对(外键交给业务层)、冗余是策略不是失误(先 3NF 建模、再按查询反范式)。设计完先自问:这张表的高频查询是什么?索引够不够?会不会写放大?

下一篇进入 事务与索引入门:ACID 是什么、事务怎么用、索引到底是什么东西,为深入篇的 B+ 树、隔离级别、慢查询优化做铺垫。

持续学习,持续构建。