4、数据库设计与建模:ER图绘制、范式化设计、主键与外键策略、索引优化原则、常用字段类型选择、数据库迁移工具

数据库设计,说白了就是给ERP系统搭骨架。骨架歪了,后面业务逻辑写得再漂亮也白搭。我见过太多项目,上线半年后开始频繁死锁、查询慢得像蜗牛,一查根因,全是数据库设计阶段埋的雷。今天咱们就把这块硬骨头啃下来。

4.1 ER图绘制:先画图,再写代码

我个人习惯,拿到需求第一件事不是建表,而是画ER图。实体-关系图就像建筑图纸,能让你一眼看清数据之间的脉络。

举个例子,ERP里最常见的“订单-客户-商品”关系:

  • 实体:客户、订单、商品、仓库
  • 关系:一个客户可以下多个订单(1:N),一个订单包含多个商品(M:N)
  • 属性:客户有姓名、电话;订单有下单时间、总金额

画图时我建议用矩形表示实体,菱形表示关系,椭圆表示属性。工具嘛,Draw.io、Visio或者PDManer都行。我在项目中遇到过最坑的情况——团队里有人跳过ER图直接建表,结果上线后发现“商品分类”和“商品”的关系搞反了,改起来那叫一个痛苦。

小技巧:ER图不要画得太细。属性列到10个以上就该考虑拆实体了,否则图会乱成一团。

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又依赖于订单主键。这就形成了传递依赖。正确的做法是把客户等级放到客户表里。

核心原则:3NF通常够用了。过度范式化(比如BCNF、4NF)反而会导致查询时关联太多表,性能下降。我在实际项目中,80%的表只做到3NF,剩下的20%会刻意反范式化来提升查询性能。

4.3 主键与外键策略

主键怎么选?这是个老生常谈的问题。我个人习惯:

  • 自增整数主键:适合单库单表,性能最好。但分布式场景下会冲突。
  • UUID/GUID:适合分布式,但占用空间大,索引性能差。我曾经在一个千万级表上用UUID做主键,插入性能直接掉了30%。
  • 雪花ID:我现在的首选。全局唯一、趋势递增、性能不错。很多框架都内置了雪花算法。
  • 业务主键:比如用身份证号做主键。嗯,这里要注意——业务主键一旦变更,关联表全得改,慎用。

外键策略呢?我的建议是:逻辑外键优于物理外键

物理外键(数据库层面的FOREIGN KEY约束)虽然能保证数据一致性,但在高并发场景下,插入和删除时数据库要检查关联表,性能损耗不小。而且,分布式数据库很多不支持物理外键。

我现在的做法:表里保留关联ID,但不在数据库层面加外键约束。数据一致性由应用层保证。这样既灵活,性能也好。

注意:如果团队水平参差不齐,或者业务对数据一致性要求极高(比如财务系统),物理外键还是必要的。别为了性能牺牲数据准确性。

4.4 索引优化原则

索引这东西,用好了是神器,用不好是累赘。我见过有人一张表建了20个索引,结果写入时慢得像乌龟爬。

几个核心原则:

  1. 区分度高的列优先建索引:比如性别这种只有两个值的列,建索引意义不大。但身份证号这种几乎唯一的列,索引效果极好。
  2. 联合索引遵循最左前缀原则:比如索引(A, B, C),查询条件必须从A开始才能用到索引。跳过A只用B,索引就废了。
  3. 不要在小表上建索引:一张表只有几百行,全表扫描比走索引还快。
  4. 索引不是越多越好:每个索引都会增加写入开销。我一般建议单表索引不超过5个。
  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更合适。但学习成本也高一些。

我的建议:小团队用Flyway,大团队或跨数据库项目用Liquibase。不管用哪个,一定要在项目第一天就引入,别等到上线前才想起来。

嗯,数据库设计这块内容确实不少。但只要你把ER图画清楚、范式化做到3NF、主键选对、索引建好、字段类型选准、迁移工具用上,ERP系统的数据层基本就稳了。下一章咱们聊聊业务逻辑层的设计,到时候见。