3. 数据建模与表设计
数据建模不是画几张 ER 图就结束,而是把业务语言翻译成数据库可约束、可查询、可演进的结构。以“员工—部门—项目—工时”这一常见业务为例:业务方说“一个部门有多名员工,一名员工只属于一个部门;一个项目可有多名员工参与,一名员工也可参与多个项目;员工每天为某个项目填报工时”。这段话里已经隐含了实体、属性和基数,建模的任务就是把它显式化。
一、从需求到实体关系模型
先做名词提取:部门、员工、项目、工时记录。再判断哪些是实体、哪些是关系。部门、员工、项目是独立实体;工时记录不是简单关系,因为它带有“日期、工时数、工作说明”等属性,应升级为独立实体(关联实体)。接着确定主键与基数:部门与员工是 1:N,员工与项目是 M:N,而工时记录把 M:N 拆成两条 1:N,并额外承载业务属性。
这一步的关键判断是:带属性的多对多关系必须实体化。如果只建 employee_project 关联表而不放日期和工时,就无法表达“同一员工同一项目在不同日期填不同工时”。因此工时表的主键通常采用 (employee_id, project_id, work_date) 组合,或使用代理主键加唯一约束。
二、规范化与反规范化的权衡
得到 ER 模型后,需要按范式检查结构。第一范式要求字段原子,例如不要把“张三,李四”塞进一个字段;第二范式要求非主属性完全依赖主键,工时表中 hours 依赖 (emp_id, project_id, work_date) 整体,符合 2NF;第三范式要求消除传递依赖,员工表中 dept_name 不应出现,因为 dept_name 依赖 dept_id 而非 emp_id,应放在部门表。
但规范化到 3NF 后,查询“员工姓名 + 部门名称 + 项目名称 + 工时”需要四表连接。在高频报表场景下,连接成本可能成为瓶颈。此时可考虑反规范化:在工时表冗余 dept_id,或建立汇总表 monthly_hours(emp_id, project_id, month, total_hours)。反规范化的判断依据是读多写少、连接代价高、且冗余字段的更新频率可控。如果部门会频繁调整,冗余 dept_name 就会带来一致性风险,应改用视图或物化视图。
三、DDL 编写:约束即文档
DDL 不只是建表,更是把业务规则写进数据库。以下示例基于 MySQL 8.0,展示核心表的建表语句。注意主键、外键、唯一约束、非空和检查约束的配合使用。
CREATE TABLE department (
dept_id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(64) NOT NULL UNIQUE,
location VARCHAR(128)
) ENGINE=InnoDB;
CREATE TABLE employee (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(64) NOT NULL,
hire_date DATE NOT NULL,
dept_id INT NOT NULL,
CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id)
REFERENCES department(dept_id)
) ENGINE=InnoDB;
CREATE TABLE project (
project_id INT PRIMARY KEY AUTO_INCREMENT,
project_name VARCHAR(128) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
CONSTRAINT chk_project_status
CHECK (status IN ('ACTIVE','CLOSED','PAUSED'))
) ENGINE=InnoDB;
CREATE TABLE timesheet (
ts_id BIGINT PRIMARY KEY AUTO_INCREMENT,
emp_id INT NOT NULL,
project_id INT NOT NULL,
work_date DATE NOT NULL,
hours DECIMAL(4,1) NOT NULL,
CONSTRAINT fk_ts_emp FOREIGN KEY (emp_id) REFERENCES employee(emp_id),
CONSTRAINT fk_ts_proj FOREIGN KEY (project_id) REFERENCES project(project_id),
CONSTRAINT uq_ts UNIQUE (emp_id, project_id, work_date),
CONSTRAINT chk_hours CHECK (hours > 0 AND hours <= 24)
) ENGINE=InnoDB;
这里有几个评审要点:uq_ts 防止同一员工同一天对同一项目重复填报;chk_hours 把“工时必须在 0 到 24 之间”的业务规则下沉到数据库;fk_ts_emp 保证工时不会指向不存在的员工。如果业务允许员工跨部门借调,则 employee.dept_id 可能需要改为多对多关系,这属于模型演进,应在评审时提出。
四、DDL 评审清单
评审不是走过场,而是用固定维度逐项检查。以下清单可直接用于团队评审:
| 评审维度 | 检查项 | 示例判断 |
|---|---|---|
| 主键 | 是否有稳定主键,是否使用自增或业务键 | timesheet 用 ts_id 代理键,同时保留唯一约束 |
| 外键 | 引用关系是否完整,删除策略是否明确 | 员工离职时工时记录应保留,外键用 RESTRICT |
| 约束 | 非空、唯一、检查约束是否覆盖业务规则 | hours 必须大于 0 且不超过 24 |
| 范式 | 是否存在传递依赖或部分依赖 | 员工表不应出现 dept_name |
| 反规范化 | 冗余字段是否有同步方案和一致性验证 | 汇总表需每日凌晨重算并比对源表 |
| 索引 | 高频查询条件是否有索引支撑 | timesheet(emp_id, work_date) 支持按人按月查询 |
| 命名 | 表名、字段名、约束名是否统一规范 | 外键统一 fk_表_引用表,唯一约束 uq_表_字段 |
评审时还应关注变更成本:如果项目已上线,新增非空列需要默认值或分步迁移;修改主键几乎不可行,应在设计阶段避免。对于工时表,若未来要支持“一个员工一天为多个项目填报”,当前唯一约束 (emp_id, project_id, work_date) 已经允许,因为不同 project_id 不冲突;但若要限制“一天总工时不超过 24”,则需要应用层或触发器汇总校验,单行 CHECK 无法完成跨行约束。
最后,把模型和 DDL 纳入版本管理,每次变更通过迁移脚本执行,并在评审记录中保留决策理由。这样,数据模型才能随着业务演进而可控地生长,而不是在后期变成难以维护的“大泥球”。