目录
正在加载目录…
专栏文章
专栏文章
MySQL 系列
1. MySQL:核心概念、架构与使用场景 2. MySQL 数据类型与 Java 类型映射 3. MySQL 基础架构与执行流程 4. MySQL InnoDB 日志系统 5. MySQL 事务与 MVCC 6. MySQL 索引原理与优化 7. MySQL 锁机制 8. MySQL 性能问题排查 9. MySQL 主备复制与高可用 10. MySQL 实战技巧与常见陷阱 11. MySQL 数据库设计规范 12. MySQL SQL 函数与查询技巧 13. MySQL InnoDB Buffer Pool 原理 14. MySQL 排序与聚合原理

MySQL 数据库设计规范

发布于 2026-05-26 10:33 · 最后编辑于 2026-07-31 15:52 · 字数 2,274 👁 89 次阅读

从三大范式的定义出发,用进货单拆分案例讲清规范化设计的完整推导过程,并给出业务优先原则下的反范式权衡依据。

目录

章节说明
三大范式1NF/2NF/3NF 定义与推导案例
业务优先原则范式≠终点,适度冗余的场景
ER 模型实体-联系建模方法
字段类型选择规范各类型的选择原则与陷阱
主键设计原则自然主键 vs 代理主键
表结构优化技巧数据类型压缩、冗余字段、垂直拆表

mysql design principles

三大范式

第一范式(1NF):字段不可再拆分

要求:数据表中所有字段都是不可拆分的基本数据项。

反例:字段 "property" = "规格:500ml/单位:瓶"
正例:拆分为 specification="500ml" 和 unit="瓶" 两个字段

第二范式(2NF):非主键字段完全依赖主键

要求:在满足 1NF 的基础上,所有非主键字段必须完全依赖全部主键字段(消除部分依赖)。

推导案例:进货单表的拆分

原始表(主键为 listnumber + barcode):

listnumberbarcodesupplieridsuppliernamegoodsnameqtyprice
34780001S01供应商A105.00
34780002S01供应商A201.50

问题supplieridsuppliername 只依赖 listnumber(主键的一部分),goodsname 只依赖 barcode(主键的另一部分)→ 部分依赖 → 数据冗余。

拆分为三张表

-- 商品信息表(barcode 为主键或自增 itemnumber)
CREATE TABLE goodsmaster (
  itemnumber INT PRIMARY KEY AUTO_INCREMENT,
  barcode    TEXT NOT NULL,
  goodsname  TEXT NOT NULL,
  spec       TEXT NOT NULL,
  unit       TEXT NOT NULL
);

-- 进货单头表
CREATE TABLE importhead (
  listnumber   VARCHAR(20) PRIMARY KEY,
  supplierid   INT NOT NULL,
  suppliername TEXT NOT NULL,
  stock        TEXT NOT NULL
);

-- 进货单明细表(联合主键)
CREATE TABLE importdetail (
  listnumber  VARCHAR(20) NOT NULL,
  itemnumber  INT NOT NULL,
  quantity    DECIMAL(10,3) NOT NULL,
  importprice DECIMAL(10,2) NOT NULL,
  importvalue DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (listnumber, itemnumber)
);

第三范式(3NF):非主键字段不依赖其他非主键字段

要求:在满足 2NF 的基础上,不能存在依赖于非主键字段的字段(消除传递依赖)。

问题:进货单头表中 suppliername 依赖于 supplierid(非主键)→ 传递依赖。

再拆分

-- 供货商表
CREATE TABLE supplier (
  supplierid   INT PRIMARY KEY AUTO_INCREMENT,
  suppliername TEXT NOT NULL
);

-- 进货单头表(去掉 suppliername)
CREATE TABLE importhead (
  listnumber  VARCHAR(20) PRIMARY KEY,
  supplierid  INT NOT NULL,   -- 外键引用 supplier
  stock       TEXT NOT NULL
);

三范式总结

flowchart TD
    A["原始大表<br/>所有字段混在一起"] -->|"1NF<br/>字段不可再拆"| B["字段原子化"]
    B -->|"2NF<br/>完全依赖主键"| C["拆出部分依赖的字段<br/>形成新表"]
    C -->|"3NF<br/>无传递依赖"| D["拆出依赖非主键字段的字段<br/>形成新表"]
    style D fill:#cfc,stroke:#060
范式解决的问题判断标准
1NF字段粒度太粗每个字段是最小信息单元
2NF联合主键表中的部分依赖所有非主键字段依赖全部主键
3NF非主键字段间的传递依赖没有"字段A → 字段B → 字段C"的链条

业务优先原则

范式不是终点,业务需求优先。

适度冗余的场景

案例:进货单明细表中 qty × importprice = importvalue,看似 importvalue 是冗余字段,但:

  1. 供应商按金额促销时,只有 importvalueimportprice 是反算出来的
  2. 反算因四舍五入会累计误差,日积月累影响财务结果

保留三个字段,虽然违反 3NF,但满足业务可靠性要求。

冗余字段合理增加的两个条件

  1. 不需要经常修改(修改成本低)
  2. 查询时不可或缺(避免高频 join)
典型案例:高频查询的流水表 join 商品表获取商品名称
→ 在流水表冗余存储商品名称字段
→ 代价:商品改名时需同步更新流水表

ER 模型

三要素

要素说明ER 图表示
实体(Entity)可以独立存在的事物矩形
属性(Attribute)实体的特征椭圆
联系(Relationship)实体间的关系菱形

联系的三种类型

类型说明示例
1:1一对一员工 ↔ 员工档案
1:N一对多部门 → 员工(一个部门有多名员工)
M:N多对多学生 ↔ 课程(需要中间表)

M:N 关系的数据库实现

-- 学生表
CREATE TABLE student (id INT PRIMARY KEY, name VARCHAR(50));

-- 课程表
CREATE TABLE course (id INT PRIMARY KEY, name VARCHAR(100));

-- 选课中间表(M:N 拆解为两个 1:N)
CREATE TABLE enrollment (
  student_id INT NOT NULL,
  course_id  INT NOT NULL,
  score      DECIMAL(5,2),
  PRIMARY KEY (student_id, course_id),
  FOREIGN KEY (student_id) REFERENCES student(id),
  FOREIGN KEY (course_id)  REFERENCES course(id)
);

ER 建模流程

1. 确定系统的核心实体(名词 → 表)
2. 确定实体的属性(字段)
3. 确定实体间的联系(1:1 / 1:N / M:N)
4. 将联系转化为外键或中间表
5. 应用三大范式精简表结构
6. 根据业务优先原则做适度冗余

字段类型选择规范

整数类型

类型字节有符号范围无符号范围
TINYINT1-128 ~ 1270 ~ 255
SMALLINT2-32768 ~ 327670 ~ 65535
MEDIUMINT3-8388608 ~ 83886070 ~ 16777215
INT4-21 亿 ~ 21 亿0 ~ 42.9 亿
BIGINT8±9.2×10¹⁸0 ~ 1.8×10¹⁹

选择原则:先保证不溢出,再考虑节省空间。数据量小时用 INT,数据量大(百万级以上)时评估用 MEDIUMINT / SMALLINT 能否节省显著空间。

浮点数 vs 定点数

-- ❌ 不精确:FLOAT / DOUBLE 存在精度问题
CREATE TABLE t1 (price DOUBLE);
INSERT INTO t1 VALUES (0.1), (0.2);
SELECT 0.1 + 0.2 FROM t1;  -- 结果可能是 0.30000000000000004

-- ✅ 精确:DECIMAL(定点数)
CREATE TABLE t2 (price DECIMAL(10, 2));
-- DECIMAL(10, 2):总共 10 位数字,小数点后 2 位

规则金额、价格等精度敏感字段必须用 DECIMAL,禁止用 FLOAT/DOUBLE。

字符串类型

类型特点适用场景
CHAR(N)定长,不足补空格,查询速度快固定长度(手机号、邮编)
VARCHAR(N)变长,节省空间不定长文本(姓名、标题)
TEXT变长大文本长文章、JSON 内容

TEXT 字段无法建完整索引(只能前缀索引),且排序时使用临时表,尽量避免在高频查询字段上使用 TEXT。

时间类型

类型范围精度存储空间
DATE1000-01-01 ~ 9999-12-313 字节
DATETIME1000-01-01 00:00:00 ~ 9999-12-318 字节
TIMESTAMP1970-01-01 ~ 2038-01-194 字节

TIMESTAMP 有 2038 年问题,新系统建议用 DATETIMETIMESTAMP 会自动转换时区,跨时区系统慎用。

主键设计原则

自然主键 vs 代理主键

类型说明优缺点
自然主键用业务字段做主键(如身份证号)有实际含义,但业务变化时可能需要改主键
代理主键新增自增 ID 字段稳定,但无业务含义

推荐代理主键:业务主键可能变化(如手机号换号),代理主键永远稳定,也避免字符串比较开销。

InnoDB 主键选择的影响

InnoDB 的数据按主键顺序存储(聚簇索引):

  • 主键应尽量顺序递增(自增 INT/BIGINT),避免页分裂
  • UUID 做主键:随机写入导致频繁页分裂,性能差,不推荐
-- 推荐:自增主键
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY

-- 不推荐:UUID(字符串比较慢,随机写入导致页分裂)
id CHAR(36) DEFAULT (UUID()) PRIMARY KEY

表结构优化技巧

数据类型压缩

对于大数据量表,字段类型选择影响显著:

案例:400 万条流水数据
  字段 itemnumber:INT(4字节) → MEDIUMINT(3字节),节省 25% 空间
  字段 transuniqueid:TEXT(20字节) → BIGINT(8字节),节省 60% 空间
  
  导入速度:3分23秒 → 3分2秒
  查询速度:5.18s → 3.86s

垂直拆分(大宽表拆分)

将一张宽表中高频访问字段低频访问字段分开存储:

流水单头表(原始):
  会员编号、收款机、折扣(高频)+ 微信金额、支付宝金额、现金金额(低频)

拆分为:
  流水单头表1:会员编号、收款机、折扣(高频)
  流水单头表2:微信金额、支付宝金额、现金金额(低频)+ 外键

好处:每次查询读取的行数据更小,Buffer Pool 命中率更高。

非空约束

尽量给字段加 NOT NULL

  • 节省判断 NULL 的开销
  • 更容易创建索引(NULL 值会让索引统计不准确)
  • 节省少量存储空间(每个 NULL 字段需要 1 bit 标记位)
-- 推荐
name VARCHAR(50) NOT NULL DEFAULT ''

-- 不推荐(除非业务上确实需要区分 NULL 和空字符串)
name VARCHAR(50) DEFAULT NULL

参考资料

  • 《MySQL 必知必会》— 第 22 讲:范式:如何消除冗余,实现高效存取?
  • 《MySQL 必知必会》— 第 23 讲:ER 模型:如何理清数据库设计思路?
  • 《MySQL 必知必会》— 第 25 讲:表太大了,如何设计才能提高性能?
  • 《MySQL 必知必会》— 第 02 讲:字段:这么多字段类型,该怎么定义?
  • MySQL - Data Types
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论