数据库建模不是简单地把字段堆进表里,而是在写任何DDL之前先确定数据如何组织、关系如何表达。很多性能问题和数据不一致,根源都在建模阶段就埋下了。一个系统化的建模过程通常分为三个阶段:概念建模、逻辑建模和物理建模。概念建模回答业务中有哪些对象,逻辑建模回答数据应该如何拆分才不冗余,物理建模回答在具体数据库里怎样落地才高效。先抓住这条主线,再逐步展开细节。

一、概念建模:把业务语言翻译成实体与关系
概念建模阶段不需要考虑具体数据库类型。核心是从需求文档、用户访谈、原型图中识别出实体。一个实用的方法是名词提取法:把业务描述中的名词圈出来,比如客户提交订单,订单包含商品,商品属于分类,这里客户、订单、商品、分类都是候选实体。动词往往表示关系,例如提交、包含、属于。这一步容易犯的错是把所有名词都当成实体,比如订单金额是属性而不是实体,因为它不能独立存在,只能用某个值来描述订单。判断依据是:它有没有自己的唯一标识,需不需要独立维护生命周期。
识别出实体后,要明确关系基数。常见关系包括一对一、一对多、多对多。例如一个客户可以下多个订单,就是一对多;一个订单可以包含多个商品,一个商品也可以出现在多个订单里,就是多对多,需要引入订单明细这样的关联实体。画不画ER图并不重要,重要的是用文字或表格把每个实体、属性、主键候选以及关系基数写清楚,作为后续建表的依据。这张清单越具体,后面逻辑建模越轻松。
属性分配也要在这一步基本确定。只把直接描述实体的属性放进去,不要急着放外键,因为外键是逻辑建模阶段根据关系推导出来的。比如客户实体有客户编号、姓名、手机号;订单实体有订单号、下单时间、状态。如果把客户姓名直接放在订单实体里,虽然查询方便,但已经属于提前反范式,应该等逻辑建模时再评估。
二、逻辑建模:范式不是目的,而是拆表的工具
逻辑建模的任务是把概念模型转换成关系模式,并尽量减少数据冗余。第一范式要求每个字段都是不可再分的原子值,比如不要把收货地址存成一个包含省市区的大字符串,如果业务需要按省筛选,就拆成多个字段或者单独地址表。第二范式要求非主键字段完全依赖于主键,不能只依赖主键的一部分。典型的反例是选课表,如果主键是学号加课程号,而表中还存了学生姓名,那么学生姓名只依赖学号,不依赖课程号,这就不符合第二范式。
第三范式进一步要求非主键字段之间不能存在传递依赖。例如订单表中如果既有客户编号又有客户姓名、客户电话,那么客户姓名和电话依赖于客户编号,而客户编号又依赖于订单主键,形成传递依赖。正确做法是拆出客户表,订单表只保留客户编号作为外键。下面是一组规范化后的建表示例:
CREATE TABLE customer (
customer_id INT PRIMARY KEY,
customer_name VARCHAR(50) NOT NULL,
phone VARCHAR(20)
);
CREATE TABLE product (
product_id INT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
category_id INT
);
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT NOT NULL,
order_time DATETIME NOT NULL,
status VARCHAR(20) NOT NULL,
CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customer(customer_id)
);
CREATE TABLE order_item (
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (order_id, product_id),
CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(order_id),
CONSTRAINT fk_item_product FOREIGN KEY (product_id) REFERENCES product(product_id)
);
但范式并不是越高越好。到了第三范式甚至BCNF,表会拆得很细,查询时经常要多次连接。如果系统读取压力远大于写入压力,可以主动做反范式设计,比如在订单表中冗余客户名称,避免每次查询订单列表都要关联客户表。反范式的前提是:明确冗余字段由哪个表负责维护,并且通过应用层事务保证更新一致性。没有这个前提,冗余字段很快就会变成数据不一致的来源。
三、物理建模:建表语句中的约束与索引选择
物理建模是在具体数据库上落地,重点包括数据类型、主键策略、约束和索引。主键选择最影响后续扩展。自增整数主键写入性能好、占用空间小,适合大多数内部系统;UUID主键适合分布式环境,但作为聚簇索引时会导致页分裂和索引膨胀。业务主键如订单号,如果业务上唯一且查询频繁,可以加唯一约束,但不一定非要作为物理主键。许多团队采用自增ID作为主键、业务编号作为唯一键的混合方案。
外键约束在开发阶段非常有用,能阻止非法关联数据的写入。生产环境是否保留外键有两种做法:保留外键可以保证数据完整性,但会带来一定的写入开销,并且在分库分表场景下无法强制使用。如果去掉外键,就必须在应用层做严格校验,并且定期清理孤儿数据。无论哪种选择,只要一开始就明确,后续维护成本都可控。CHECK约束可用于限制取值范围,例如状态只能取某些值,不过MySQL在8.0之前对CHECK支持有限,需要根据实际版本确认。
索引设计是物理建模中最容易出问题的部分。不是索引越多越好,每个索引都会增加存储和写入成本。应该优先为WHERE条件、JOIN连接列、ORDER BY列建立索引。联合索引遵循最左前缀原则,例如为customer_id和order_time建立联合索引,可以支撑只按customer_id查询的场景,但不能单独支撑按order_time查询。写建表语句时要把索引也一并定义,避免上线后才发现慢查询。例如:
CREATE INDEX idx_orders_customer_time ON orders(customer_id, order_time); CREATE UNIQUE INDEX uk_orders_order_no ON orders(order_no);
四、建模评审清单与常见误区
模型完成之后,不要马上进入开发,先用一份清单做评审。检查是否每个表都有主键;是否所有外键都有对应索引;是否存在重复存储同一含义的字段;是否使用了合适的数据类型而不是一律VARCHAR;是否对可能为空的字段明确允许NULL;命名是否统一,比如都用下划线小写、保留字是否避开。这些细节看似琐碎,但会在长期维护中持续产生影响。
常见误区还包括:把所有字段都设成可空,导致业务逻辑不断判空;用VARCHAR存储日期,导致范围查询无法走索引;主键使用业务可变字段,一旦业务变更就要级联修改所有引用;忽略默认值设计,插入时总需要补齐全字段。另一个典型问题是过早优化:在设计阶段就为了想象中的性能问题大量反范式,反而让写入路径复杂化。正确做法是先用规范化模型保证正确性,再根据真实慢查询做定向反范式。
最后要记住,数据库建模不是一次性的动作。业务演进过程中,表结构需要持续迁移和版本管理。每次变更都应该评估对现有数据、查询计划和下游消费方的影响。把建模文档和变更记录保存下来,新的团队成员才能快速理解模型背后的业务规则,而不是只看到一堆表和字段。系统化掌握建模,本质上就是建立从业务到结构的稳定翻译能力。