4、数据库设计与建模:ER图绘制、范式化设计、主键与外键策略、索引优化原则、常用字段类型选择、数据库迁移工具
数据库设计,说白了就是给ERP系统搭骨架。骨架歪了,后面业务逻辑写得再漂亮也白搭。我见过太多项目,上线半年后开始频繁死锁、查询慢得像蜗牛,一查根因,全是数据库设计阶段埋的雷。今天咱们就把这块硬骨头啃下来。
4.1 ER图绘制:先画图,再写代码
我个人习惯,拿到需求第一件事不是建表,而是画ER图。实体-关系图就像建筑图纸,能让你一眼看清数据之间的脉络。
举个例子,ERP里最常见的“订单-客户-商品”关系:
- 实体:客户、订单、商品、仓库
- 关系:一个客户可以下多个订单(1:N),一个订单包含多个商品(M:N)
- 属性:客户有姓名、电话;订单有下单时间、总金额
画图时我建议用矩形表示实体,菱形表示关系,椭圆表示属性。工具嘛,Draw.io、Visio或者PDManer都行。我在项目中遇到过最坑的情况——团队里有人跳过ER图直接建表,结果上线后发现“商品分类”和“商品”的关系搞反了,改起来那叫一个痛苦。
4.2 范式化设计:1NF、2NF、3NF
范式化,说白了就是一套“如何避免数据冗余”的规则。你想想看,如果一张表里既存了客户姓名,又存了客户地址,还存了客户的所有订单——那客户改个地址,你得改多少行?
4.2.1 第一范式(1NF)
要求:每个字段都是不可再分的原子值。说白了,一个字段里不能塞一个列表。
❌ 错误示例:订单表里有个字段叫“商品列表”,存的是“商品A,商品B,商品C”。
✅ 正确做法:拆成订单明细表,一行一个商品。
4.2.2 第二范式(2NF)
要求:在1NF基础上,消除部分函数依赖。也就是非主键字段必须完全依赖于主键,不能只依赖主键的一部分。
我记得有个ERP项目,把“订单明细表”的主键设成了(订单ID, 商品ID),结果把“商品名称”也塞进了这张表。这就出问题了——商品名称只依赖于商品ID,跟订单ID没关系。这就是部分依赖,违反了2NF。
4.2.3 第三范式(3NF)
要求:在2NF基础上,消除传递函数依赖。也就是非主键字段不能依赖于另一个非主键字段。
举个例子:订单表里有“客户ID”和“客户等级”。客户等级依赖于客户ID,而客户ID又依赖于订单主键。这就形成了传递依赖。正确的做法是把客户等级放到客户表里。
4.3 主键与外键策略
主键怎么选?这是个老生常谈的问题。我个人习惯:
- 自增整数主键:适合单库单表,性能最好。但分布式场景下会冲突。
- UUID/GUID:适合分布式,但占用空间大,索引性能差。我曾经在一个千万级表上用UUID做主键,插入性能直接掉了30%。
- 雪花ID:我现在的首选。全局唯一、趋势递增、性能不错。很多框架都内置了雪花算法。
- 业务主键:比如用身份证号做主键。嗯,这里要注意——业务主键一旦变更,关联表全得改,慎用。
外键策略呢?我的建议是:逻辑外键优于物理外键。
物理外键(数据库层面的FOREIGN KEY约束)虽然能保证数据一致性,但在高并发场景下,插入和删除时数据库要检查关联表,性能损耗不小。而且,分布式数据库很多不支持物理外键。
我现在的做法:表里保留关联ID,但不在数据库层面加外键约束。数据一致性由应用层保证。这样既灵活,性能也好。
4.4 索引优化原则
索引这东西,用好了是神器,用不好是累赘。我见过有人一张表建了20个索引,结果写入时慢得像乌龟爬。
几个核心原则:
- 区分度高的列优先建索引:比如性别这种只有两个值的列,建索引意义不大。但身份证号这种几乎唯一的列,索引效果极好。
- 联合索引遵循最左前缀原则:比如索引(A, B, C),查询条件必须从A开始才能用到索引。跳过A只用B,索引就废了。
- 不要在小表上建索引:一张表只有几百行,全表扫描比走索引还快。
- 索引不是越多越好:每个索引都会增加写入开销。我一般建议单表索引不超过5个。
- 覆盖索引:如果查询的字段都在索引里,数据库就不需要回表查数据了,性能翻倍。
我曾经接手过一个ERP项目,订单查询页面要等10秒。一查,发现查询条件里的“下单时间”字段没建索引。加上索引后,查询时间降到了0.2秒。你想想看,一个索引就能解决的事,何必折腾服务器?
4.5 常用字段类型选择
字段类型选错了,后果很严重。我见过有人用VARCHAR存日期,结果排序时按字符串排,2023-01排到了2022-12前面。
| 数据类型 | 适用场景 | 避坑指南 |
|---|---|---|
| INT/BIGINT | 主键、数量、金额(分单位) | 金额不要用FLOAT,精度会丢。用DECIMAL(18,2)或存分为单位的INT |
| VARCHAR | 名称、描述、编码 | 长度不要随意设255。比如手机号11位,VARCHAR(20)就够了 |
| DATETIME/TIMESTAMP | 创建时间、更新时间 | TIMESTAMP有2038年问题,新项目建议用DATETIME |
| TEXT | 长文本、备注 | 尽量避免,查询性能差。非要存的话,单独放一张表 |
| JSON | 动态属性、配置信息 | MySQL 5.7+支持,但不要滥用。能拆成字段就别用JSON |
4.6 数据库迁移工具:Flyway vs Liquibase
项目开发过程中,数据库结构肯定会变。加个字段、改个索引、调整表结构——这些变更怎么管理?手动执行SQL?那等着上线时出乱子吧。
我强烈建议用数据库迁移工具。目前主流的两款:
4.6.1 Flyway
轻量、简单、约定大于配置。你只需要写SQL文件,按命名规范放在指定目录下,Flyway会自动按顺序执行。
-- 示例:V1__create_user_table.sql
CREATE TABLE `user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- V2__add_email_column.sql
ALTER TABLE `user` ADD COLUMN `email` VARCHAR(100);
Flyway会记录哪些脚本已经执行过,不会重复执行。我个人比较喜欢Flyway,因为它够简单,团队成员上手快。
4.6.2 Liquibase
功能更强大,支持XML、YAML、JSON多种格式。可以回滚、可以生成变更日志、可以跨数据库。
<!-- 示例:dbchangelog.xml -->
<databaseChangeLog>
<changeSet id="1" author="author">
<createTable tableName="user">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true"/>
</column>
<column name="name" type="VARCHAR(50)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
如果你需要支持多种数据库(比如开发用MySQL,生产用PostgreSQL),Liquibase更合适。但学习成本也高一些。
嗯,数据库设计这块内容确实不少。但只要你把ER图画清楚、范式化做到3NF、主键选对、索引建好、字段类型选准、迁移工具用上,ERP系统的数据层基本就稳了。下一章咱们聊聊业务逻辑层的设计,到时候见。