学习 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,而是理解业务。
面对一张表,可以反复问:
这张表代表什么?
一行数据代表什么?
谁唯一标识它?
哪些字段不能重复?
哪些字段不能为空?
它属于谁?
系统经常怎么查询它?
回答完这些问题,通常就能逐渐推导出:
字段
主键
唯一约束
索引
外键
下一篇,我们继续设计博客系统里更有意思的部分:
点赞
收藏
评论
标签
文件
这些业务会真正引出数据库设计中非常重要的:
多对多、中间表、联合主键、自关联以及级联删除。