从 0 设计一个博客数据库:用户、头像与文章表应该怎么建?

学习 SQL 时,真正困难的往往不是记住 CREATE TABLE 怎么写,而是:

面对一个真实业务,我到底应该建哪些表?每张表放什么字段?为什么需要主键、索引和外键?

这篇文章不从 SQL 语法表开始背,而是假设我们正在开发一个类似掘金的博客系统,从业务一步一步推导数据库结构。


一、先从业务出发:博客系统需要哪些数据?

假设我们的博客支持这些功能:

  • 用户注册、登录
  • 用户设置头像
  • 用户发布文章
  • 用户点赞文章
  • 用户收藏文章
  • 用户评论文章
  • 文章添加标签
  • 用户上传图片

那么数据库中可能会出现:

sql 复制代码
user
avatar
post
user_like_post
user_collect_post
comment
tag
post_tag
file

先不要急着建所有表。

数据库设计最重要的能力不是"一口气把表写出来",而是:

一个业务一个业务拆。

这一篇先解决最基础的三个:

复制代码
用户
头像
文章

二、设计表之前,先问四个问题

以后看到任何业务,都可以先问:

markdown 复制代码
1. 这张表存什么?
2. 一行数据代表什么?
3. 怎么唯一找到这一行?
4. 它和其他表有什么关系?

这四个问题搞清楚以后,字段、主键、索引、外键就比较容易确定了。


三、用户表应该存什么?

用户系统最核心的数据通常包括:

bash 复制代码
id
username
password

为什么不一开始就把:

css 复制代码
avatar
slogan
birthday
address
...

全部塞到用户表里?

因为用户表通常是非常高频访问的表。

例如登录:

sql 复制代码
SELECT id, username, password
FROM user
WHERE username = ?;

查询用户:

sql 复制代码
SELECT *
FROM user
WHERE id = ?;

这些操作根本不需要头像、个人简介等资料。

因此在一些系统设计中,会倾向于:

核心用户表保存核心身份数据,扩展资料根据需要拆出去。

当然,这并不是说"头像一定必须单独建表"。

小项目完全可以直接把 avatar_url 放在 user 表里。

这里把头像拆出来,主要是为了学习:

  • 表和表之间如何建立关系
  • 外键是什么
  • 索引为什么存在

四、设计 user 表

先看一个基础版本:

sql 复制代码
CREATE TABLE `user` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(255) NOT NULL,
  `password` VARCHAR(255) NOT NULL,

  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_unicode_ci;

这张表可以理解为:

erlang 复制代码
user

id      username      password
1       zhangsan      ...
2       lisi          ...
3       wangwu        ...

一行数据代表:

一个用户。


五、为什么需要 id?

sql 复制代码
id INT NOT NULL AUTO_INCREMENT

AUTO_INCREMENT 表示数据库可以自动生成:

erlang 复制代码
1
2
3
4
5
...

然后:

bash 复制代码
PRIMARY KEY (`id`)

表示:

id 是这条用户记录的唯一标识。

例如:

ini 复制代码
id = 12

无论用户以后把名字改成什么,用户 12 仍然是用户 12。

所以业务中通常会使用:

bash 复制代码
/user/12

而不是直接把用户名作为整个系统的身份标识。


六、Primary Key 到底是什么?

Primary Key 就是:

主键。

最简单的理解:

数据库通过它唯一确定一条记录。

例如:

bash 复制代码
id
1
2
3

不能出现:

复制代码
1
1

因为主键不能重复。

并且 MySQL 会为主键建立索引,所以:

sql 复制代码
SELECT *
FROM user
WHERE id = 100;

通常可以非常快地找到目标用户。


七、为什么 username 又要 UNIQUE?

用户名通常不能重复。

例如系统里已经有:

复制代码
zhangsan

另外一个人就不能再次注册:

复制代码
zhangsan

因此:

go 复制代码
UNIQUE KEY `uk_username` (`username`)

相当于告诉数据库:

username 必须唯一。

这样即使代码层忘记检查,数据库本身也会阻止重复数据。


八、UNIQUE 不只是约束,也是索引

这一点很重要。

因为登录时很可能执行:

sql 复制代码
SELECT *
FROM user
WHERE username = 'zhangsan';

username 上存在唯一索引,因此数据库不需要每次都从大量用户记录中一条一条寻找。

所以 UNIQUE 同时承担两个作用:

复制代码
业务约束
用户名不能重复

查询性能
根据用户名查询更快

九、到底什么是索引?

刚接触数据库时,不需要马上钻进 B+ Tree 的细节。

先建立一个最重要的直觉:

索引是用额外的空间,换取更快的查询。

假设数据库中有几百万个用户:

sql 复制代码
SELECT *
FROM user
WHERE username = 'dfp';

如果 username 没有合适的索引,数据库可能需要检查大量记录。

有索引以后,数据库可以更快定位目标。

所以设计索引时,一个非常重要的原则是:

根据查询需求设计索引。

不是:

感觉这个字段重要,所以给它加索引。

而应该思考:

复制代码
系统经常按什么字段查?
经常怎么排序?
经常怎么关联?

例如:

bash 复制代码
GET /user/:id

经常根据:

bash 复制代码
id

找用户。

主键已经解决。

登录经常根据:

复制代码
username

找用户。

唯一索引解决。

这就是:

业务查询决定索引。


十、密码为什么不能存明文?

假设数据库直接保存:

复制代码
username     password

zhangsan     123456
lisi         abc123

一旦数据库泄露,所有用户密码都会直接暴露。

所以真实系统不会保存用户的原始密码。

通常会保存经过专门密码哈希算法处理后的结果,例如使用:

复制代码
bcrypt
Argon2

这样的密码哈希方案。

因此数据库里的:

复制代码
password

实际上更准确地理解为:

复制代码
password_hash

登录时也不是:

把数据库里的原密码拿出来比较。

而是:

用密码哈希算法验证用户输入的密码是否匹配。


十一、头像应该存在哪里?

很多新手第一次做头像上传时,会想:

要不要直接把图片塞数据库?

一般 Web 项目不会把普通图片文件本体直接放在关系型数据库中。

更常见的方式是:

markdown 复制代码
图片文件
    ↓
对象存储 / 静态资源服务器
    ↓
得到文件地址

例如可能存到:

复制代码
阿里云 OSS
AWS S3
腾讯云 COS
自己的文件服务器

数据库主要保存:

arduino 复制代码
filename
mimetype
size
userId
url / objectKey

等元数据。


十二、设计 avatar 表

例如:

sql 复制代码
CREATE TABLE `avatar` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `mimetype` VARCHAR(255) NOT NULL,
  `filename` VARCHAR(255) NOT NULL,
  `size` INT NOT NULL,
  `userId` INT NOT NULL,

  PRIMARY KEY (`id`),

  KEY `idx_userId` (`userId`),

  CONSTRAINT `fk_avatar_user`
    FOREIGN KEY (`userId`)
    REFERENCES `user` (`id`)
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_unicode_ci;

假设数据是:

arduino 复制代码
id   filename       size     userId
1    abc.webp       10240    7

这句话翻译成人话就是:

abc.webp 是用户 7 上传的头像。


十三、userId 是什么?

这里出现了数据库设计中一个极其重要的概念:

复制代码
userId

avatar 表本身不知道:

这个头像属于谁。

所以在里面保存一个:

复制代码
userId

指向:

python 复制代码
user.id

于是:

ini 复制代码
user
id = 7

和:

ini 复制代码
avatar
userId = 7

就建立了关系。


十四、Foreign Key 是什么?

这一段:

go 复制代码
FOREIGN KEY (`userId`)
REFERENCES `user` (`id`)

就是外键。

可以翻译成:

avatar.userId 引用 user.id

例如 user 表只有:

复制代码
1
2
3

那么:

ini 复制代码
avatar.userId = 2

没问题。

但如果你插入:

ini 复制代码
avatar.userId = 999

而系统根本没有用户 999,在启用外键约束的情况下数据库就会拒绝。

所以外键的重要作用之一就是:

保证表之间的数据关系正确。


十五、为什么 userId 还要建立索引?

实际业务中可能经常执行:

ini 复制代码
SELECT *
FROM avatar
WHERE userId = 7;

也就是:

查询用户 7 的头像。

因此:

go 复制代码
KEY `idx_userId` (`userId`)

可以帮助这个查询。

另外,在 MySQL InnoDB 中,外键列通常也需要相应索引来高效维护关联。


十六、接下来设计文章表

博客系统当然要有文章:

复制代码
post

一篇文章至少需要:

bash 复制代码
id
title
content
userId

其中:

复制代码
userId

表示:

这篇文章是谁写的?


十七、post 表

可以写成:

r 复制代码
CREATE TABLE `post` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(255) NOT NULL,
  `content` LONGTEXT,
  `userId` INT NOT NULL,

  PRIMARY KEY (`id`),

  KEY `idx_userId` (`userId`),

  CONSTRAINT `fk_post_user`
    FOREIGN KEY (`userId`)
    REFERENCES `user` (`id`)
) ENGINE=InnoDB
  DEFAULT CHARSET=utf8mb4
  COLLATE=utf8mb4_unicode_ci;

例如:

bash 复制代码
id    title              userId

101   JavaScript 入门      7
102   MySQL 索引学习       7
103   React Hooks         12

意味着:

复制代码
用户 7 写了文章 101
用户 7 写了文章 102
用户 12 写了文章 103

十八、这里出现了"一对多"

观察:

复制代码
一个用户
可以写很多文章

但是:

复制代码
一篇文章
通常只有一个作者

所以:

sql 复制代码
user 1 ------ N post

这就是:

一对多关系。

实现方式非常简单:

在"多"的一方保存"一"的 id。

也就是:

复制代码
post.userId

指向:

python 复制代码
user.id

以后看到:

复制代码
一个分类有很多文章
一个部门有很多员工
一个用户有很多订单

都应该想到这种设计方式。


十九、主键、索引、约束终于可以串起来了

现在重新看这三张表。

user

sql 复制代码
id
→ PRIMARY KEY

username
→ UNIQUE

原因:

bash 复制代码
id 唯一标识用户
username 不能重复

avatar

sql 复制代码
id
→ PRIMARY KEY

userId
→ INDEX
→ FOREIGN KEY

原因:

bash 复制代码
id 唯一标识头像记录
经常根据 userId 查头像
头像必须属于真实用户

post

sql 复制代码
id
→ PRIMARY KEY

userId
→ INDEX
→ FOREIGN KEY

原因:

bash 复制代码
id 唯一标识文章
经常查某个用户的文章
文章必须关联作者

你会发现:

SQL 语法并不是凭空出现的。

每一个设计都有对应的业务原因。


二十、数据库约束到底是在干什么?

可以把约束理解为:

把业务规则写进数据库。

例如:

sql 复制代码
PRIMARY KEY
一条记录必须能够被唯一识别

UNIQUE
用户名不能重复

NOT NULL
这个字段必须有值

FOREIGN KEY
关联的数据必须合法

数据库不是简单的"存数据"。

一个设计良好的数据库还应该尽量阻止错误数据进入。


二十一、这篇真正应该掌握什么?

不要只记住三段 CREATE TABLE

真正应该掌握的是这个推导过程:

sql 复制代码
出现"用户"
→ user 表

一条用户数据必须唯一
→ PRIMARY KEY

用户名不能重复
→ UNIQUE

经常按用户名登录查询
→ 索引

用户拥有头像
→ avatar.userId

头像必须属于用户
→ FOREIGN KEY

一个用户能写多篇文章
→ post.userId
→ 一对多

当你可以根据业务自己推出这些设计时,SQL 才真正开始入门。


总结

数据库设计的第一步永远不是写 SQL,而是理解业务。

面对一张表,可以反复问:

复制代码
这张表代表什么?

一行数据代表什么?

谁唯一标识它?

哪些字段不能重复?

哪些字段不能为空?

它属于谁?

系统经常怎么查询它?

回答完这些问题,通常就能逐渐推导出:

复制代码
字段
主键
唯一约束
索引
外键

下一篇,我们继续设计博客系统里更有意思的部分:

复制代码
点赞
收藏
评论
标签
文件

这些业务会真正引出数据库设计中非常重要的:

多对多、中间表、联合主键、自关联以及级联删除。

相关推荐
大鸡腿同学2 小时前
黄仁勋点透的 AI 真相:能实现你知道但做不到的,却实现不了你不知道的
后端
FfHUCisI2 小时前
Go 编译过程全景
开发语言·后端·golang
FfHUCisI2 小时前
Golang 语法分析与 AST:Parser 与 go/ast
开发语言·后端·golang
大模型码小白3 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring
++==4 小时前
RESTful详解:核心思想、框架、与HTTP的区别,API的设计风格
后端·http·restful
云计算DevOps-韩老师4 小时前
【MySQL运维DBA】【SQL基础系列002篇】
运维·mysql·dba
放风铃的兔子5 小时前
15 年 DBA 经验:MySQL 迁移到 KES,哪些东西真的不用改?
mysql
IT_陈寒5 小时前
Vue的响应式更新有时候真的不听话
前端·人工智能·后端
向生6 小时前
Ubuntu Caddy 保姆级完整教程
后端