跳到主要内容

数据建模实战

问题

如何进行数据建模?常见业务场景(电商、社交、内容系统)的数据模型如何设计?

答案

数据建模方法论

ER 模型核心要素

  • 实体(Entity):业务中的对象(用户、商品、订单)
  • 属性(Attribute):实体的特征(用户名、价格)
  • 关系(Relationship):实体间的联系(1:1、1:N、M:N

建模流程

电商系统数据模型

关键设计决策

决策选择原因
订单金额订单表存总金额避免每次查询都聚合计算
商品快照order_items 存当时的价格和名称商品修改后不影响历史订单
地址独立地址表 + 订单冗余地址可修改,但订单地址不能变
库存products 表存 stock简单场景够用,高并发需独立库存服务
-- 订单明细表:保存下单时的商品快照
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
product_name VARCHAR(200) NOT NULL, -- 快照:下单时的商品名
product_price DECIMAL(10,2) NOT NULL, -- 快照:下单时的价格
quantity INT NOT NULL,
subtotal DECIMAL(10,2) NOT NULL,
INDEX idx_order_id(order_id)
);

社交系统数据模型

关注关系(M:N

-- 用户关注关系
CREATE TABLE follows (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
follower_id BIGINT NOT NULL, -- 关注者
following_id BIGINT NOT NULL, -- 被关注者
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_follow(follower_id, following_id),
INDEX idx_following(following_id)
);

-- 查询 A 关注了谁
SELECT following_id FROM follows WHERE follower_id = ?;
-- 查询谁关注了 A
SELECT follower_id FROM follows WHERE following_id = ?;

朋友圈/Feed 流

方案说明优势劣势
拉模型查询时拉取关注人的动态写入简单查询慢(扇出读)
推模型发布时推送到粉丝收件箱查询快写入多(大 V 问题)
推拉结合大 V 用拉,普通用户用推均衡实现复杂

内容管理数据模型

树形结构(分类/评论)

-- 方案 1:邻接表(最常用)
CREATE TABLE categories (
id BIGINT PRIMARY KEY,
parent_id BIGINT DEFAULT 0, -- 父级 ID,0 表示顶级
name VARCHAR(100),
level INT, -- 层级深度
sort_order INT,
INDEX idx_parent(parent_id)
);

-- 方案 2:路径枚举(适合读多写少)
CREATE TABLE categories (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
path VARCHAR(500), -- 如 '/1/3/7/'
INDEX idx_path(path)
);
-- 查询某节点的所有子节点
SELECT * FROM categories WHERE path LIKE '/1/3/%';

-- 方案 3:闭包表(适合深层嵌套,如评论楼中楼)
CREATE TABLE comment_paths (
ancestor_id BIGINT, -- 祖先节点
descendant_id BIGINT, -- 后代节点
depth INT, -- 深度差
PRIMARY KEY (ancestor_id, descendant_id)
);
方案查子节点查祖先链移动节点适用场景
邻接表递归查递归查简单通用、层级不深
路径枚举LIKE 查字符串切割需更新所有子节点读多写少
闭包表关联查关联查需删除重建深层嵌套

多态关联设计

一个实体关联多种不同类型的实体(如评论可以属于文章/商品/视频):

-- 方案 1:通用外键(不推荐,无法加 FK 约束)
CREATE TABLE comments (
id BIGINT PRIMARY KEY,
target_type VARCHAR(20), -- 'article', 'product', 'video'
target_id BIGINT,
content TEXT,
INDEX idx_target(target_type, target_id)
);

-- 方案 2:独立关联表(推荐)
CREATE TABLE article_comments (
comment_id BIGINT,
article_id BIGINT,
FOREIGN KEY (article_id) REFERENCES articles(id)
);
CREATE TABLE product_comments (
comment_id BIGINT,
product_id BIGINT,
FOREIGN KEY (product_id) REFERENCES products(id)
);

通用设计原则

原则说明
选择合适的主键推荐自增 BIGINT 或分布式 ID,避免 UUID 做聚簇索引
合理使用 NOT NULL尽量定义 NOT NULL + DEFAULT,减少 NULL 判断
选择合适的数据类型用最小够用的类型:TINYINT、SMALLINT、日期用 DATETIME
避免过早优化先按 3NF 设计,遇到性能瓶颈再反范式
预留扩展字段JSON 列或 ext_info 存储非核心扩展数据
软删除is_deleted 标记代替物理删除

常见面试问题

Q1: 为什么不推荐 UUID 做主键?

答案

InnoDB 使用聚簇索引按主键顺序存储数据:

  1. UUID 无序:插入时数据随机分布,导致频繁页分裂,写入慢
  2. 占用空间大:UUID 36 字节 vs BIGINT 8 字节,索引更大
  3. 比较效率低:字符串比较比整数慢

推荐:自增 BIGINT 或雪花算法生成的有序 ID。

Q2: 订单表为什么要保存商品快照?

答案

商品信息(名称、价格、图片)可能随时变更,但历史订单必须保留下单时的信息:

  • 用户查看历史订单时,应看到下单时的价格
  • 发生退款纠纷时,以下单时的价格为准

做法:在 order_items 表中冗余 product_nameproduct_priceproduct_image 等字段。

Q3: 如何设计评论的楼中楼结构?

答案

推荐方案:邻接表 + 冗余 root_id

CREATE TABLE comments (
id BIGINT PRIMARY KEY,
parent_id BIGINT DEFAULT 0, -- 上级评论(0=顶级)
root_id BIGINT DEFAULT 0, -- 所属的顶级评论
article_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
content TEXT,
INDEX idx_article(article_id, id),
INDEX idx_root(root_id, id)
);

查询方式:

  • 先查文章的顶级评论(parent_id = 0),分页
  • 再查每条顶级评论的回复(root_id = ?),LIMIT 前几条
  • 点击"展开更多回复"时加载更多

相关链接