学完这篇你能拿到一套可以直接落地的数据库设计规范:命名怎么定、字段类型怎么选、什么时候老实按范式拆表、什么时候可以放心反范式冗余。
第一步:先把命名规则定死,别边写边想
命名混乱是后期维护成本最高的地方,因为它不会报错,只会让人猜。建议一开始就定死这几条:
- 表名:全小写 + 下划线,统一单数或统一复数(选一个,别混),带业务前缀,如
bbs_topic、bbs_reply
- 字段名:全小写 + 下划线,主键统一
id,外键统一 xxx_id,布尔统一 is_xxx,时间统一 xxx_at(created_at/updated_at)
- 索引名:普通索引
idx_字段、唯一索引 uk_字段、外键 fk_表_字段,让人看名字就知道查什么
- 全部小写:Linux 上 MySQL 表名默认大小写敏感,
User 和 user 是两个表,迟早出事
CREATE TABLE bbs_topic (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
board_id INT UNSIGNED NOT NULL DEFAULT 0,
user_id INT UNSIGNED NOT NULL DEFAULT 0,
title VARCHAR(120) NOT NULL DEFAULT '',
reply_count INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_board_id (board_id),
KEY idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意:order、group、key、desc、status 里有一部分是保留字,字段名撞上就得每次反引号包起来。建表前先查一遍保留字清单,能绕就绕。
第二步:字段类型按用途选,不要一律 VARCHAR(255)
类型选错的代价是隐性的:精度丢失、索引变长、排序变慢。按这张对照表来:
| 用途 | 选什么 | 别选什么 |
|---|
| 主键/外键 | INT UNSIGNED,全库统一,超大表用 BIGINT UNSIGNED | 混合用 INT 和 BIGINT |
| 金额 | DECIMAL(10,2) | FLOAT/DOUBLE(0.1+0.2 不等于 0.3) |
| 时间 | DATETIME,统一存一个时区 | TIMESTAMP(2038 上限)、VARCHAR 存时间串 |
| 布尔 | TINYINT UNSIGNED,0/1 | ENUM('Y','N') |
| 短文本 | VARCHAR(N),N 按真实上限给 | 一律 255(索引长度白浪费) |
| 长文本 | TEXT,或拆到副表 | 硬塞进 VARCHAR(20000) |
| 枚举状态 | TINYINT + 代码常量 | ENUM(加一个值就要 ALTER TABLE) |
| IP | VARCHAR(45) | VARCHAR(15)(存不下 IPv6) |
字符集统一 utf8mb4 + utf8mb4_unicode_ci。
注意:MySQL 里的 utf8 其实是 utf8mb3,最多 3 字节,存不了 emoji 和部分生僻字,用户一发 emoji 就报错或变问号。建库建表全部写 utf8mb4。
第三步:先按范式拆,把「重复」当信号
- 第一范式(原子性):一个字段只存一个值。别把多个标签塞进
tags VARCHAR(255) 用逗号拼,要建 bbs_topic_tag 关联表。
- 第二范式(完全依赖):联合主键的表里,非主属性必须依赖整个主键,不能只依赖其中一部分。
- 第三范式(无传递依赖):非主属性不能依赖其他非主属性。典型反例是帖子表里同时存
board_id 和 board_name,版块一改名,几百万行帖子全要跟着改。
范式的本质是「一份事实只存一处」。表设计阶段先按 3NF 拆干净,后面才有资格谈反范式。
第四步:该反范式时,只反「读多写少、可重算」的地方
反范式不是偷懒的借口,判断标准有两条:这个冗余能从上位数据重算出来;读写比足够悬殊。
典型场景:
- 计数冗余:
bbs_topic.reply_count、用户发帖数。写入时同事务更新,同时挂一个定时校对任务定期全量重算,防止某次异常导致数字飘掉。Clara BBS 这类系统的计划任务体系(Cron::register 注册、前台懒触发或外部 /cron 精准触发)就是干这个的。
- 快照冗余:订单、交易流水里存下单时的用户名、价格、商品标题快照。历史记录本来就该是「当时的样子」,改了反而不对。
- 榜单热点:排行榜别在主库上实时跑聚合查询,交给 Redis 缓存层(
Cache::remember 一行式调用),过期自动重算。
注意:反范式字段一旦写进表,就必须配两样东西——写入事务保证同步、定时任务保证校准。只写不校对,线上数据迟早对不上账。另外开了读写分离时,写后立刻读要注意走主库,否则从库延迟会读出旧值。
第五步:把规范变成可执行的检查项
- 维护一份建表模板,新表直接复制,不留自由发挥空间
- 业务唯一约束交给唯一索引(用户名唯一、邮箱唯一),别用「先 SELECT 再 INSERT」,并发下必然重复
- 上线前对核心查询跑一遍
EXPLAIN,确认走索引、没有 type: ALL
- 代码评审时把「字段类型是否合理」「冗余字段是否有校对任务」列为固定检查项
小结
- 命名先行:全小写下划线、前缀统一、索引按
idx_/uk_/fk_ 规范起名
- 类型按用途选:金额用 DECIMAL、时间用 DATETIME、状态用 TINYINT、字符集一律 utf8mb4
- 设计阶段先按 3NF 拆干净,把重复字段当信号处理
- 反范式只用在计数、快照、榜单三类场景,且必须「可重算 + 有校对」
- 唯一索引承载业务约束,核心查询上线前必须 EXPLAIN
- 读写分离环境下,写后立即读要确保走主库