最近在搭 React + Nest.js 的全栈社区项目,地基阶段最头疼的就是数据库表设计 ------ 表拆多了关联复杂,表合少了后期扩展难,索引建错了查询慢,约束少了数据脏。踩了一圈坑之后,整理出一套可直接落地的业务表设计,涵盖用户、文章、点赞、评论、标签、文件等核心模块,连索引、外键、分表思路都给你捋明白。
一、先算总账:后端核心业务一共几张表?
整个社区后端的核心业务围绕「用户生产内容、用户互动内容」展开,一共设计 9 张核心业务表,分别是:
- 用户表(
user):账号登录核心信息 - 头像表(
avatar):用户头像元数据,与用户一对一关联 - 文章表(
post):内容主体表 - 点赞表(
user_like_post):用户 - 文章点赞关联表 - 收藏表(
user_collect_post):用户 - 文章收藏关联表 - 评论表(
comment):文章评论,支持树形回复 - 标签表(
tag):全局标签字典 - 文章标签关联表(
post_tag):文章 - 标签多对多关联 - 文件表(
file):文章附件、图片等资源元数据
二、逐表拆解:建表语句 + 设计思路
2.1 用户表:精简至上,为分布式铺路
用户表是整个系统的核心,设计原则是 只存登录必备的核心字段,不塞冗余信息。表越小,越有利于分布式场景下的快速查询、分库分表。
sql
CREATE TABLE `user`(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4_unicode_ci
设计要点:
id自增主键,作为全局用户唯一标识name加唯一索引(UNIQUE KEY),保证用户名不重复,同时支撑「按用户名搜索 / 登录」的高频查询password只存加密后的密文,绝对不存明文- 头像、个性签名、用户资料等扩展字段全部拆分到其他表,避免单表过宽
2.2 头像表:解耦扩展,静态资源独立存储
头像属于用户的扩展属性,且涉及文件资源管理,单独拆表更灵活。图片本身不存数据库,只存元数据,文件放在独立的静态资源服务器 / OSS。
sql
CREATE TABLE `avatar` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mimetype` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`filename` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`size` int(11) NOT NULL,
`userId` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `userId` (`userId`),
CONSTRAINT `avatar_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4_unicode_ci
设计要点:
- 通过
userId与用户表一对一关联,加普通索引支撑「根据用户 ID 查头像」的查询 - 存储文件类型、文件名、大小等元数据,实际文件托管在阿里云 OSS 等静态资源服务,返回 CDN 地址即可
- 外键约束保证用户删除时,头像数据的一致性
2.3 文章表:内容主体,关联作者
文章是核心内容载体,关联作者用户 ID,大字段内容单独用 longtext 存储。
sql
CREATE TABLE `post` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`content` longtext COLLATE utf8mb4_unicode_ci,
`userId` int(11) DEFAULT NULL,
PRIMARY KEY(`id`),
KEY `userId` (`userId`),
CONSTRAINT `post_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4_unicode_ci
设计要点:
userId加普通索引,支撑「查看某个用户的所有文章」的查询- 外键关联用户表,用户删除时文章作者置空(
DEFAULT NULL),保留内容 content用longtext适配长文场景
2.4 点赞表:联合主键,避免冗余索引
点赞是典型的「用户 - 文章」多对多关系,用联合主键即可,不需要额外自增 ID,还能天然去重。
go
CREATE TABLE `user_like_post` (
`userId` int(11) NOT NULL,
`postId` int(11) NOT NULL,
PRIMARY KEY (`userId`, `postId`),
KEY `postId` (`postId`),
CONSTRAINT `user_like_post_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
CONSTRAINT `user_like_post_ibfk_2` FOREIGN KEY (`postId`) REFERENCES `post` (`postId`)
ON DELETE CASCADE ON UPDATE CASCADE
)
设计要点:
- 以
(userId, postId)作为联合主键,既保证一个用户不能重复点赞同一篇文章,又天然覆盖「查某个用户点赞了哪些文章」的查询(最左前缀原则) - 只需要额外建
postId普通索引,支撑「查某篇文章有多少点赞」 - 不用单独给 userId 建索引,联合主键已经覆盖,建了就是浪费空间
- 文章删除时,级联删除对应点赞记录,保证数据一致
2.5 收藏表:同构设计,业务隔离
收藏和点赞业务逻辑类似,结构同构,但必须分表,方便后续业务独立扩展(比如收藏加分组、备注等字段)。
go
CREATE TABLE `user_collect_post` (
`userId` int(11) NOT NULL,
`postId` int(11) NOT NULL,
PRIMARY KEY (`userId`, `postId`),
KEY `postId` (`postId`),
CONSTRAINT `user_collect_post_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
CONSTRAINT `user_collect_post_ibfk_2` FOREIGN KEY (`postId`) REFERENCES `post` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE
)
2.6 评论表:树形结构,自关联设计
评论支持盖楼回复,用 parentId 自关联实现树形结构,顶级评论的 parentId 为 NULL。
less
CREATE TABLE `comment` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`content` longtext COLLATE utf8mb4_unicode_ci,
`postId` int(11) NOT NULL,
`userId` int(11) NOT NULL,
`parentId` int(11) DEFAULT NULL,
PRIMARY KEY(`id`),
KEY `postId` (`postId`),
KEY `userId` (`userId`),
KEY `parentId` (`parentId`),
CONSTRAINT `comment_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
CONSTRAINT `comment_ibfk_2` FOREIGN KEY (`parentId`) REFERENCES `comment` (`id`)
ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `comment_ibfk_3` FOREIGN KEY (`postId`) REFERENCES `post` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE
)
设计要点:
- 三个普通索引分别对应三个高频查询场景:查某篇文章的评论、查某个用户的评论、查某条评论的子回复
- 父评论删除时,子评论的
parentId置NULL(ON DELETE SET NULL),保留子评论,避免连锁删除 - 文章删除时,级联删除所有关联评论
2.7 标签体系:多对多关系的标准解法
标签和文章是多对多关系,必须拆成「标签字典表 + 关联表」,避免冗余存储。
标签表(字典)
less
CREATE TABLE `tag` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY(`id`),
UNIQUE KEY `name` (`name`)
)
文章标签关联表
swift
CREATE TABLE `post_tag` (
`postId` int(11) NOT NULL,
`tagId` int(11) NOT NULL,
PRIMARY KEY(`postId`, `tagId`),
KEY `tagId` (`tagId`),
CONSTRAINT `post_tag_ibfk_1` FOREIGN KEY (`postId`) REFERENCES `post` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `post_tag_ibfk_2` FOREIGN KEY (`tagId`) REFERENCES `tag` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE
)
设计要点:
- 标签名加唯一索引,保证全局标签不重复
- 关联表用联合主键,避免重复打标签;左前缀覆盖「查某篇文章的所有标签」
- 额外加
tagId索引,支撑「查某个标签下的所有文章」
2.8 文件表:附件元数据全量存储
文章中的图片、附件等资源,统一在文件表管理元数据,支持关联文章和上传用户。
sql
CREATE TABLE `file` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`originalname` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`mimetype` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`filename` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`size` int(11) NOT NULL,
`postId` int(11) DEFAULT NULL,
`userId` int(11) NOT NULL,
`width` smallint(6) NOT NULL,
`height` smallint(6) NOT NULL,
`metadata` json DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `postId` (`postId`),
KEY `userId` (`userId`),
CONSTRAINT `file_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
CONSTRAINT `file_ibfk_2` FOREIGN KEY (`postId`) REFERENCES `post` (`id`)
ON DELETE SET NULL ON UPDATE CASCADE
)
设计要点:
- 存原始文件名、存储文件名、类型、大小、图片宽高等完整元数据
metadata用 JSON 类型存扩展信息,灵活扩展不用改表结构- 文章删除时,文件记录保留(置空 postId),方便资源复用和管理
三、索引设计:不是越多越好,要按需命中
很多人建索引的误区是 "每个字段都建一个",实际上索引占空间、增删改会变慢,必须按需建。总结四类常用索引:
表格
| 索引类型 | 作用 | 使用场景 |
|---|---|---|
| 主键索引 | 唯一标识一行数据,查询最快 | 每张表必须有,一般用自增 id |
| 唯一索引 | 保证字段值不重复,同时加速查询 | 用户名、标签名等全局唯一字段 |
| 普通索引 | 加速普通查询 | 外键字段、高频 where 条件字段 |
| 联合主键 / 联合索引 | 多字段组合加速,同时左前缀覆盖 | 多对多关联表、多条件组合查询 |
核心原则:
- 高频查询字段才建索引,低频筛选字段不建
- 联合索引遵循最左前缀原则,能覆盖就不单独建
- 小表(比如几千行的字典表)可以不建索引,全表扫描更快
四、约束设计:数据一致性的最后一道防线
MySQL 的约束是保证数据干净的低成本方案,核心用好三种:
- 主键约束:每行唯一标识,非空不重复
- 唯一约束:业务字段全局唯一,防止脏数据
- 外键约束 :保证关联表数据一致性,配合
ON DELETE/ON UPDATE策略
级联策略选型建议:
- 文章删除 → 点赞、评论、标签关联全部删除:用
ON DELETE CASCADE - 父评论删除 → 子评论保留,父 ID 置空:用
ON DELETE SET NULL - 用户删除 → 文章、评论保留,作者置空:用
ON DELETE SET NULL
五、性能与分布式:从小型到集群的演进思路
用户表的分表思路
用户表是核心瓶颈,当用户量上来后,按 id 范围或哈希分表是常见方案。正因为用户表设计得足够精简,字段少、行长度短,分表后查询性能提升非常明显。
访问链路的分层加速
整套系统部署在集群中时,访问链路是层层递进的:
- 浏览器缓存:静态资源、接口数据本地缓存
- CDN 节点:头像、图片、JS/CSS 就近访问
- Nginx 负载均衡:反向代理,分发到健康的后端服务器
- 本地缓存 / Redis:热点数据(文章列表、用户信息)内存缓存
- 数据库集群:主从读写分离,逐级递归查找
DNS 解析流程
用户访问 juejin.cn 这类域名时,会经过本地 DNS → 运营商 DNS → 根服务器 → 顶级域服务器,最终返回离你最近的 Nginx 节点 IP,Nginx 再把请求转发到后端真实服务集群。
六、工程化落地:从 SQL 文件到 Docker 部署
项目目录管理
在 Nest.js 项目根目录建 database 文件夹,把 blog.sql 表结构文件放进去,作为数据库设计文档,版本化管理。
Docker 容器化部署
Dockerfile 就是镜像的 "配方文件",里面写好构建步骤,就能一键打出一致的运行环境:
sql
# 简单示例
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/main.js"]
常用部署命令:
perl
# 构建镜像
docker build -t my-blog-backend .
# 登录镜像仓库
docker login
# 推送镜像
docker push my-blog-backend
# 服务器拉取运行
docker pull my-blog-backend
docker run -p 3000:3000 my-blog-backend
Nginx 反向代理
前端跑 80 端口,后端跑 3000 端口,用 Nginx 做反向代理,顺便解决跨域、负载均衡:
bash
server {
listen 80;
server_name shturl.cc/nr7Qe;
# 前端静态资源
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# 后端接口代理
location /api/ {
proxy_pass http://localhost:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
七、总结
一套好的后端表设计,不是字段越多越全越好,而是:
- 核心表精简:用户、文章等主表只留核心字段,扩展属性拆表
- 关联关系清晰:一对多用外键,多对多用中间表,树形结构用自关联
- 索引按需建设:跟着查询场景走,不盲目建索引
- 约束保障一致:用好主键、唯一、外键,减少脏数据
- 预留扩展空间:JSON 字段、分表思路,支撑业务从小到大规模演进
这套表结构可以直接拿去做博客、社区、资讯类项目的后端底座,配合 Nest.js 的模块化开发,很快就能搭出一套可用的全栈系统。