一条点赞,六张表:SQL 数据库设计实战

一条点赞,六张表:SQL 数据库设计实战

一个类掘金的内容社区,看起来只是"用户发文章、其他人点赞评论",落到数据库里,却至少涉及用户、头像、文章、点赞、收藏、评论六类数据。

数据库表不是越多越专业,索引也不是加得越多越快。设计一张表时,真正应该先问三个问题:

  1. 这张表服务哪个业务动作?
  2. 业务最常怎么查询它?
  3. 哪些数据关系必须由数据库强制保证?

本文用一个内容社区的业务模型,串起建表、索引、外键、联合主键和自引用评论五个核心知识点。示例以 MySQL 8.0 为主,字段命名统一使用 snake_case

一、先画业务关系,再写 CREATE TABLE

六张表的职责可以先压缩成一张图:

text 复制代码
users 1 ─── 1 avatars
users 1 ─── n posts
users n ─── n posts  ← post_likes / post_bookmarks
users 1 ─── n comments ─── n 1 posts
comments 1 ─── n comments  ← parent_id 自引用

这张图里包含四种关系:

关系 表达方式
一对一 在从表外键上加 UNIQUE
一对多 在多的一方保存外键
多对多 增加一张中间表
树状关系 表通过外键引用自身

二、用户表:字段少一点,边界清楚一点

用户表只放身份认证和稳定的核心字段:

sql 复制代码
CREATE TABLE users (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  username VARCHAR(64) NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY uk_users_username (username)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

这里有三个设计点:

  • id 是主键,适合按 ID 查询用户和建立关联。
  • username 用唯一索引,既支持登录查询,也从数据库层阻止重名。
  • 不保存明文密码,字段名直接写成 password_hash,实际存放 bcrypt、Argon2 等算法生成的哈希值。

头像、个人简介、偏好设置等扩展信息不一定都要塞进用户表。拆表不是为了"分布式",而是为了让核心用户记录和低频扩展字段有清晰边界;是否分表,还要看访问模式和业务复杂度。

三、头像表:数据库存元数据,不存图片文件

图片文件通常放在对象存储、静态服务器或 CDN,数据库保存文件描述和归属关系:

sql 复制代码
CREATE TABLE avatars (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id BIGINT UNSIGNED NOT NULL,
  object_key VARCHAR(512) NOT NULL,
  mime_type VARCHAR(100) NOT NULL,
  file_size BIGINT UNSIGNED NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY uk_avatars_user_id (user_id),
  FOREIGN KEY (user_id) REFERENCES users (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

UNIQUE KEY (user_id) 很关键。它把"一个用户最多一张当前头像"落实成数据库约束,否则普通索引只能加速查询,不能阻止同一用户插入多条头像记录。

object_key 比直接存完整 URL 更灵活。以后更换 CDN 域名时,只需要在应用层拼接 URL,不必批量修改数据库里的每一条记录。

四、文章表:一对多关系的典型场景

一个用户可以发布多篇文章,所以外键放在文章表:

sql 复制代码
CREATE TABLE posts (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  author_id BIGINT UNSIGNED NOT NULL,
  title VARCHAR(255) NOT NULL,
  content LONGTEXT NOT NULL,
  created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  PRIMARY KEY (id),
  KEY idx_posts_author_created (author_id, created_at),
  FOREIGN KEY (author_id) REFERENCES users (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

为什么索引不是只写 author_id?因为常见查询往往是:

sql 复制代码
SELECT * FROM posts
WHERE author_id = ?
ORDER BY created_at DESC;

(author_id, created_at) 让索引同时服务"查某个作者"和"按时间排序"。索引设计应该从真实 SQL 出发,而不是看到外键就机械地加一个同名索引。

如果允许删除用户,要先决定文章怎么办:禁止删除作者、把文章转给系统用户,还是级联删除。内容社区通常不会轻易级联删除文章,避免一次用户删除带走大量内容。

五、点赞与收藏:多对多关系交给中间表

用户和文章是多对多关系:一个用户可以点赞多篇文章,一篇文章也可以被多个用户点赞。用中间表保存这次关系:

sql 复制代码
CREATE TABLE post_likes (
  user_id BIGINT UNSIGNED NOT NULL,
  post_id BIGINT UNSIGNED NOT NULL,
  created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  PRIMARY KEY (user_id, post_id),
  KEY idx_likes_post_user (post_id, user_id),
  FOREIGN KEY (user_id) REFERENCES users (id),
  FOREIGN KEY (post_id) REFERENCES posts (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

联合主键 (user_id, post_id) 的意义是:同一个用户对同一篇文章只能出现一条点赞关系。重复插入时,数据库会拒绝,而不是等业务代码"记得检查"。

为什么还要有 (post_id, user_id)?联合主键的最左列是 user_id,适合查"某用户点过哪些文章";如果要查"某文章被哪些用户点赞",就需要以 post_id 开头的索引。

收藏表结构几乎一样:

sql 复制代码
CREATE TABLE post_bookmarks (
  user_id BIGINT UNSIGNED NOT NULL,
  post_id BIGINT UNSIGNED NOT NULL,
  created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  PRIMARY KEY (user_id, post_id),
  KEY idx_bookmarks_post_user (post_id, user_id),
  FOREIGN KEY (user_id) REFERENCES users (id),
  FOREIGN KEY (post_id) REFERENCES posts (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

点赞和收藏虽然字段相似,但它们是两个不同的业务动作。拆成两张表,查询语义更清楚,也能分别扩展取消时间、来源等字段。

六、评论表:parent_id 把平面数据变成树

评论除了属于一篇文章,还可能回复另一条评论:

sql 复制代码
CREATE TABLE comments (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  post_id BIGINT UNSIGNED NOT NULL,
  user_id BIGINT UNSIGNED NOT NULL,
  parent_id BIGINT UNSIGNED NULL,
  content TEXT NOT NULL,
  created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  PRIMARY KEY (id),
  KEY idx_comments_post_created (post_id, created_at),
  KEY idx_comments_parent (parent_id),
  FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE,
  FOREIGN KEY (user_id) REFERENCES users (id),
  FOREIGN KEY (parent_id) REFERENCES comments (id) ON DELETE SET NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

parent_idNULL 时是顶级评论,不为 NULL 时指向父评论。它是一条指向自身的外键:

text 复制代码
评论 1
├── 评论 2(回复评论 1)
└── 评论 3(回复评论 1)

两种删除策略要分开理解:

  • 删除文章时,评论没有独立存在的意义,使用 ON DELETE CASCADE 比较自然。
  • 删除父评论时,通常保留子评论,把 parent_id 置空,所以使用 ON DELETE SET NULL

外键动作不是越多越好。CASCADE 可能造成大范围删除,应该根据业务生命周期谨慎选择。

七、索引:不要看到字段就加索引

类型 示例 解决什么问题
主键 PRIMARY KEY (id) 唯一标识记录,支持主键查询
唯一索引 UNIQUE (username) 防重复,同时支持查询
普通索引 KEY (parent_id) 加速过滤和关联查询
联合索引 KEY (post_id, created_at) 服务多列过滤、排序
联合主键 PRIMARY KEY (user_id, post_id) 防止关系重复

设计索引时记住两个代价:

  • 索引会占用磁盘和内存,写入、更新、删除时也需要维护。
  • 联合索引遵循最左前缀原则。(user_id, post_id) 能服务只按 user_id 查询,但通常不能高效服务只按 post_id 查询。

索引是否有效,最终要用 EXPLAIN 看执行计划,而不是凭感觉判断。

八、外键:让数据库拒绝脏关系

外键表达的是"这条引用必须指向真实记录":

sql 复制代码
FOREIGN KEY (post_id) REFERENCES posts (id)

它可以阻止点赞指向不存在的文章,也能配合删除策略维护关联数据。但外键不是所有系统都必须使用的银弹:跨库、分库分表、极高写入吞吐场景可能会选择应用层或异步校验。单体 MySQL 内容社区通常可以优先使用外键,换取更强的数据完整性。

九、这些 SQL 坑很小,排查起来很浪费时间

1. 注释语法写错

SQL 中不要混入 HTML 注释:

sql 复制代码
-- 正确:单行注释
KEY idx_posts_author (author_id)

2. 外键约束漏写完整

sql 复制代码
CONSTRAINT fk_comments_post
  FOREIGN KEY (post_id) REFERENCES posts (id)

3. 拼写和逗号错误

CASCADE 不能写成 CASADE;最后一个字段或约束后不能多一个逗号。

4. 表名和字段名不统一

userIDuserIduser_id 混用会让 SQL、ORM 和接口 DTO 都变得难以维护。项目开始前先确定命名规范。

十、面试怎么答

数据库表设计应该从业务关系和查询场景出发。用户和文章是一对多,外键放在文章表;用户和文章的点赞、收藏是多对多,用中间表和联合主键防止重复关系;评论通过 parent_id 自引用形成树。索引服务于高频查询,联合索引要考虑最左前缀;外键和级联策略负责维护引用完整性,但删除动作必须结合业务生命周期选择。

总结

业务 表设计 关键约束
用户 users 用户名唯一、密码只存哈希
头像 avatars user_id 唯一,文件放对象存储
文章 posts 作者外键,作者与时间联合索引
点赞 post_likes (user_id, post_id) 联合主键
收藏 post_bookmarks 与点赞相同的多对多模型
评论 comments parent_id 自引用,按文章和父评论建索引

一句话总结:每张表服务一个业务动作,索引服务查询路径,约束负责让错误数据进不来。

相关推荐
MetaLite1 小时前
SpringBoot项目Maven-BOM统一版本就不会冲突吗
spring boot·后端·maven
benchmark_cc1 小时前
Claude Code + MCP + QuantDash:打造全自动量化研究流水线的终极指南
人工智能·后端·爬虫·算法·claude·mcp·quantdash
小刘是地理大王1 小时前
Nacos 注册与配置中心实战笔记
后端
Csvn1 小时前
🐍 Day 6: Python 异常处理 — 防御式编程的核心
后端·python
叫我少年1 小时前
Git SSH 配置:从生成密钥到远程连接
git·后端
IT_陈寒1 小时前
Python多进程池的坑:子进程竟然不会退出
前端·人工智能·后端
叫我:松哥2 小时前
基于flask图书管理系统,技术栈flask+layui+sqlite+Echarts
后端·python·数据分析·flask·echarts·layui
铁皮饭盒2 小时前
网页端, 5.5mb谷歌模型, 识别躯干, 视频都不卡
前端·javascript·后端
WongKyunban2 小时前
Go语言的简洁并发编程
开发语言·后端·golang