第四部分:数据建模 —— 范式化与反范式化的艺术
数据库设计不仅仅是建几个表,而是通过范式化减少冗余,再通过适度的反范式化提升查询性能。
4.1 范式理论快速回顾
第一范式(1NF):列不可再分,即每个属性都是原子的。例如地址字段不应该存储复合值(省市区混在一起)。
第二范式(2NF):满足 1NF 且所有非主属性完全依赖主键(没有部分依赖)。通常消除联合主键中的部分依赖。
第三范式(3NF):满足 2NF 且没有传递依赖(非主属性不依赖于其他非主属性)。例如订单表中,应存商品 ID,不应存商品价格,因为价格依赖于商品 ID,而非订单号。
BCNF:一种更严格的 3NF,每个决定因素都包含候选键。
4.2 范式化的优点与代价
优点:
数据冗余小,节省存储。
更新操作只需在一处修改,避免数据不一致(更新异常)。
数据结构清晰,易于维护。
代价:
查询时往往需要多次连接(JOIN),当表数据量大且并发高时,JOIN 成本高。
数据库需要维护更多的外键约束,对写入性能有影响。
4.3 反范式化(Denormalization)的场景与技巧
为了性能,可以有意引入冗余数据,减少 JOIN。
常见反范式化设计:
冗余常用字段:订单表冗余用户昵称、商品标题,避免每次查询都 JOIN 用户表、商品表。
预先计算汇总值:用户行为统计表实时维护计数(如帖子点赞数),而非每次 COUNT。
使用 JSON/文档列:将子对象序列化后存在一列,避免一对一或一对多关系的扩展表。
代价:更新时必须同步修改多处,可能产生不一致。适用于读多写少、对实时一致性要求不严的场景。
示例:论坛帖子表反范式化
规范化设计:
CREATE TABLE posts (id INT PRIMARY KEY, title VARCHAR(200), user_id INT);
CREATE TABLE users (id INT PRIMARY KEY, username VARCHAR(50));
CREATE TABLE post_stats (post_id INT PRIMARY KEY, like_count INT, comment_count INT);
查询帖子详情及用户名、点赞数需要 JOIN 三张表。
反范式化:
CREATE TABLE posts_denorm (
id INT PRIMARY KEY,
title VARCHAR(200),
username VARCHAR(50), -- 从 users 冗余过来
like_count INT DEFAULT 0,
comment_count INT DEFAULT 0
);
当用户名变更时,需要更新所有相关帖子(代价高);点赞数增加需要 UPDATE。但查询极快(单表)。适用于用户名几乎不变、点赞频繁但一致性要求不极高的场景。
4.4 分表与分区 —— 突破单表容量上限
当表数据达到几千万甚至上亿行时,即使有合适的索引,查询和维护也会变慢。此时需要考虑水平分割数据。
4.4.1 分区(Partitioning)
分区是数据库内部将一张大表的数据按照一定规则(范围、列表、哈希)拆分到多个物理子表(但逻辑上仍是一张表)。对应用透明。
MySQL 分区示例(按年份分区):
CREATE TABLE orders (
id INT,
order_date DATE,
amount DECIMAL(10,2)
) PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
查询 WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31' 时,优化器只扫描 p2023 分区。
分区注意事项:
分区键必须包含在主键或唯一索引中。
跨分区查询可能比单表慢(需合并多个分区结果)。
分区数量过多(上千个)会降低管理效率。
4.4.2 分片(Sharding)
分片将数据分布到多个独立的数据库实例(物理服务器)上,每个分片称为一个 Shard。分片键的选择至关重要,决定了查询是否需要跨分片。
分片策略:
哈希分片:shard_id = hash(user_id) % N,数据分布均匀,但范围查询需要查询所有分片。
范围分片:按某列值范围分配,便于范围查询,但可能产生热点(如最近注册的用户集中在最新分片)。
目录分片:维护一个查找表(或配置中心),灵活性高但增加了额外查询。
分片的挑战:
跨分片的 JOIN 非常困难,通常需要应用层做多次查询再聚合。
分片事务(分布式事务)代价高,尽量设计无跨分片事务的业务。
扩缩容需要数据迁移。
许多现代分布式数据库(如 TiDB、CockroachDB、Vitess)自动处理分片和重分布。
4.5 数据库设计实战:选择主键、外键、约束
主键:选择不可变、短小、且通常具有顺序性的列(自增整数)。避免使用可更新列(如用户邮箱)做主键。
外键:可以保证引用完整性,但在高并发写入时可能带来额外的锁开销。有些团队为了性能在应用层维护一致性,放弃外键约束。
检查约束:MySQL 8.0 支持 CHECK 约束,PostgreSQL 一直支持,可以防止脏数据写入,如 CHECK (age >= 0 AND age <= 150)。