MySQL八股文
存储引擎及架构
数据库的三大范式
- 第一范式:字段必须是原子性,不能再分
- 第二范式:每个非主属性必须完全依赖于主键,消除部分依赖
- 第三范式:非主键之间不能有依赖关系,消除传递依赖
关系型数据库区别
- 关系型数据库:复杂的数据结构归结为简单二维表格形式
- MySQL、Oracle
- 非关系型数据库:将数据存储为键值对集合,其中键作为唯一标识符
- Redis、MongoDB
内部架构及作用

- 连接器:不同语言与数据库进行交互,如java,php
- 系统管理和控制工具:涵盖:运维监控、数据维护、性能优化、安全管控四大核心场景
- 连接层
连接时会优先加载全局权限和数据库级权限
连接过程是一个TCP长连接,MySQL服务器和客户端通讯是“半双工”,只能单方面发送请求,不能同时发送
TCP长连接带来的问题 :占用内存,内存涨的特别快;如何解决?
- 定期断开长连接
- 使用Druid会定期检测空闲连接,超过设定时间的连接会被释放
- SQL接口:接收用户的SQL命令,并返回用户查询的结果
- 解析器
在执行语句之前,会解析查询的语言,在过程中会判断语法,将查询字段、表、条件封装到内部的数据结构形成解析树

- 查询缓存
MySQL8将这块删除了,因为缓存匹配条件严格,需要SQL完全一致
- 查询优化器
通过语法解析,MySQL知道真实意图了,但是写的SQL语句不够高效,MySQL会给语句做优化,比如使用了哪个索引?最左匹配原则?
- 执行:判断是否有权限才可以接着执行
- 存储引擎接口
- 文件系统层
一条SQL请求的过程
- SQL客户端与服务器建立连接,该请求发送到连接器
- 通过MySQL关键字将语法解析成一个内部解析树,再判断有没有错误语法
- 如果命中缓存,直接返回结果
- 基于成本优化SQL语句
- 调用存储引擎接口执行
主流的存储引擎
- InnoDB(默认)
- 支持事务处理,具备ACID特性(原子性、一致性、隔离性、持久性)
- 采用行级锁,高并发下减少锁冲突
- 支持外键约数,便于维护表之间关系
- MyISAM
- 读取速度快
- 不支持事务和行级锁
- Memory
- 数据存储在内存中
- 支持哈希索引
一条查询语句的关键字执行顺序
逻辑执行顺序:
FROM
ON
JOIN
WHERE
GROUP BY
HAVING
SELECT
DISTINCT
ORDER BY
LIMIT / OFFSET
【例子】

- FROM:拿到emp、dept两张表的原始行
- ON:按照 d.id = e.dept_id 先做匹配
- JOIN:把匹配成功的行拼在一起
- WHERE:丢掉 salary <= 10000 的行
- GROUP BY:按照 d.name 分组
- HAVING:保留组内数据 ≥ 3 的组
- SELECT:算出 dept 别名、cnt 聚合列
- ORDER BY:按 cnt 排序
- LIMIT:取前 5 行
InnoDB的三大特性
- “外在能力”
- 事务(ACID)
- 行级锁
- MVCC
- “内部机制” / 核心优化特性、机制
- 插入缓存
往二级索引里插一条记录,目标叶子页如果不在内存中,需要将那一页随机读进来,修改,再写回去,很慢
InnoDB优化:先不读那页,将要插入的数据、插到哪一页,写到另一个 B+ 树里(这棵树叫 Change Buffer),等以后那一页正好进内存或者后台空闲,再写入数据
叶子页:
想要插入0036这条数据,就需要将 【0035, 0065, 0088】这个叶子页的数据全部拿出来
- 双写
页(Page)写入磁盘分为两步:先写到共享双写区;再落目标数据文件
目的:发生意外情况,可以从副本恢复这页数据
- 自适应哈希索引
热点范围自动创建哈希加速等值查找,省去多层B+树跳转
锁

大致介绍
- 兼容性
共享锁:共享锁之间是兼容的,共享锁和排他锁是互斥的
排他锁:排他锁和任何锁都是不兼容的
- 粒度
全局锁:锁住数据库中的所有表,即锁住库
表级锁:每次操作要锁住整张表,锁定粒度大,发生锁冲突概率最高
- 表锁
- 元数据锁
- 意向锁
行级锁:每次操作要锁住对应的行,发生锁冲突概率最低
- 行锁(记录锁)
- 间隙锁
- 临建锁
全局锁
加锁后整个实例处于只读状态,后续的所有DML、DDL都会被阻塞,但可以执行DQL语句
使用场景:用于从数据库备份数据,获得一致性的数据

全局锁特点:
- 如果在主库上备份,备份期间不能执行更新,业务被暂停
- 如果在从库上备份,备份期间不能执行主库同步的二进制文件(binlog),导致从数据延迟
- 【解决方式】使用快照读备份,不加全局锁
表级锁
- 表锁
- 表共享读锁(read lock,读锁,S锁):读锁不会阻塞其他客户端的读,但是会阻塞写
- 表独占写锁(write lock,写锁,X锁):写锁会阻塞其他客户端的读和写
- 元数据锁:元数据锁会在执行增删改的时候自动加上,事务提交后会被自动释放
- 意向锁
想上表级别读锁,首先需要确保没有行级别写锁,如果有,需要等到行级别写锁释放。
想上表级别写锁,首先需要确保没有行级别读锁和行级别写锁,如果有,需要等到行级别读锁和行级别写锁释放。
怎么快速知道有没有行级锁,就需要用到意向锁
- 意向共享锁(IS锁)锁粒度:表级锁,当事务准备在某条记录上加行级读锁时,需要先在表级别加一个意向共享锁
- 意向排他锁(IX锁)锁粒度:表级锁,当事务准备在某条记录上加行级写锁时,需要先在表级别加一个意向排他锁
如果没有意向锁,系统每次想加表锁,都得翻遍整张表看有没有行锁,效率低下
有了意向锁,只需要查看是否存在**意向共享锁(IS锁)、意向排他锁(IX锁)**即可
执行查询语句时,避免行锁和表锁的冲突,不用检查每行数据
行级锁
- 行锁(记录锁)
锁定单个行记录的锁,针对索引加锁,防止其他事务进行update和delete操作
InnoDB有两种类型行锁
- 共享锁(S-share):允许多个事务去读一行,用于并发读取
- 排他锁(X-exclude):加锁后只允许当前事务进行 读/写 操作,其他事务不能进行 读/写操作
- 间隙锁
锁“空隙”,不锁行本身,用来阻止别人往这个空隙里插入新行,从而避免幻读或违反唯一性

- 临键锁
左开右闭 ( ]
记录锁 (Record Lock) + 右侧间隙锁 (Gap Lock)
既把命中的那一行锁住,又把它右边那段空档一并锁上,防止别人插队产生幻读

- 区别
假设有 id 为 {1,3,6,7,9} 这五个数据
- 行锁:命中存在值,查询id = 6的数据;锁住 id = 6
- 间隙锁:命中不存在值,查询 id = 5 的数据;锁住 id 在 (3, 6) 之间的数据,防止插入5
- 临键锁:查询范围触及6,查询 id >= 6 的数据;锁住 id 在 (3, 6]、(6, 7]、(7, 9] 数据;右边 ] 都是真实数据
查询 id > 5 and id < 8 的数据
1 3 【 6 7 】 9
- 行锁:锁住 id = 6, id = 7
- 临键锁:锁住 (3, 6]、(6, 7]
- 间隙锁:锁住右侧边界间隙 (7, 9)
死锁的检测及处理策略
- 检测:MySQL使用等待图(Wait-for Graph)算法来检测死锁,通过检查事务之间的依赖关系来判断是否存在死锁,MySQL自带的死锁检测

- 死锁处理的策略
- 回滚事务:发现死锁后,可以回滚其中资源持有最少得一个,解除死锁
- 返回错误信息
- 超时设置和重试机制
- 死锁的预防和避免
- 尽早提交事务:避免在事务中执行长时间计算、减少锁持有的时间
- 拆分大事务:减少事务持有锁的时间
- 使用合适的索引:减少锁定的数据量,提高查询效率
- 优化查询语句:使用精确条件限制查询范围,避免长时间持有锁定的行
- 使用乐观锁:通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出;如果当前版本号 > 读取的版本号,认为更新成功,顺带将版本号 + 1
索引
什么是索引?
索引就像数据库里的加速目录,相当于告诉你“第123页是第5章”,记录了【关键值 ——> 数据物理位置】映射,查询时不需要将整本书翻一遍
个人理解的索引相关知识

每一个索引对应一颗独立的B+树
对于主键索引,它的叶子节点存的是整行数据
对于二级索引(普通索引/唯一索引),它的叶子节点存的是索引列数据 和 主键值(PK值),根据这个主键值回主键索引查找数据
如果有二级索引,没有主键索引怎么办?
- InnoDB中必须有一个聚簇索引
- 如果有主键索引,那么聚簇索引就是主键索引
- 如果没有主键索引,但有一个NOT NULL 且 UNIQUE 的索引,聚簇索引就用它
- 如果都没有,InnoDB会生成一个隐藏的6字节row_id,作为聚簇索引的key
为什么要有索引?
- 极速查找
- 减少IO
- 约数 & 唯一性 :比如保证邮箱不重复
- 支持排序和分组:B+树天生按键排序
索引类型

Hash索引

hash索引存储了索引字段的hash值和数据所在磁盘文件指针,如果来了一句sql:
select * from tab where id= 1 那么mysql会怎么去执行这条sql语句:
a、将id = 1 做一次hash运算得到hash为123
b、拿到hash为123在hash索引中去找123的节点
c、节点所对应的数据就是数据内容所在磁盘文件的指针
d、通过一次磁盘I/O得到所有的内容,即id=1,name=张三。
e、最后返回结果。
适用场景:
- 不存储字段值
- 无法进行排序
- 不支持部分索引列匹配查找
- 不支持任何范围查询
B树索引特点
B树叫做多路平衡查找树,多路就是说一个结点下面可以有多个子节点。

B+树索引特点
B+树就是B树的一种变种,非叶子节点存放索引,叶子节点存放数据
B+树叶子节点使用的是双向链表

B树和B+树的区别
- B树的所有节点即存放键(key),也存放值(value);B+树只有叶子结点存放key和value,非叶子结点只存放key
- B树的叶子节点是独立的;B+树的叶子节点有一条链表指向相邻叶子节点。
- B树的检索过程不稳定,可能还没有到达叶子节点,检索就结束了。但是B+树每次检索数据都要检索到叶子节点,查找比较稳定。
- B+树的范围查询只需要对链表进行遍历即可。
为什么用B+树而不是红黑树?
- 树高
红黑树是一种特殊的平衡树,想要覆盖大量键,树高很容易升高,在磁盘上就多一层随机I/O
B+ 树每层能存放几百上千指针,树高很小,随机 I/O 很低
- 单位
红黑树的单位是一个对象,想要获取连续的数据,需要随机读
B+ 树的单位是页,一次读取一个节点,就等于读取这一页的数据
- 范围查询
红黑树的范围查询需要不断的进行中序遍历,耗时
B+ 树叶子节点之间有双向链表,对于范围查询很有帮助
- 维护成本
红黑树每次插入/删除都可能触发旋转和颜色翻转
B+树的插入/删除是以页为单位进行的,能够批量移动
索引的分类
- 按照逻辑作用
主键索引、唯一索引、普通索引、联合索引、全文索引
- 数据结构/算法
空间索引、B+树、Hash索引
- 物理索引
聚簇索引、非聚簇索引、覆盖索引
为什么B+树三层能存储2000万数据?


Innodb&MyISAM在存储结构的区别?
Innodb—聚簇索引:叶子结点包含了完整的数据记录
MyISAM—非聚簇索引:叶子节点存的是主键值,通过主键值去聚簇索引找到完整数据
索引分类
- 主键索引
每个表只能有一个主键索引
特性
- 强制唯一性约束,不允许NULL值
- 是表的主标识符
应用场景
- 作为表的行标识符
- 需要快速通过主键值检索单行数据
- 建立表与表之间的关系
- 唯一索引
确保索引列或列组合的值唯一的索引
特性
- 允许NULL值(通常只能有一个NULL值)
- 可以创建在多个列上
- 不同于主键索引,一个表可以有多个唯一索引
应用场景
-
需要确保业务数据唯一性的列(身份证、邮箱等)
-
代替主键索引
- 普通索引
最基本的索引类型,没有唯一性约束
特性
- 仅用于加速查询,不强制数据唯一性
- 可以包含NULL值
- 创建和维护成本相对较低
应用场景
- 常用于WHERE条件、JOIN条件和ORDER BY子句中的列
- 查询频繁但不需要唯一约束的列
- 联合索引
建立在多个列上的索引,也称为复合索引,其本质是一个二级索引
特性
- 索引顺序很重要(遵循最左前缀原则)
“最左前缀原则”(Leftmost Prefix Rule)指的是:
当你为表建立了联合索引(例如 INDEX (a, b, c)),MySQL 在检索、排序、分组时,只能从最左边开始,连续地利用索引里的列。也就是它能用上 a,或 a,b,或 a,b,c,但跳过中间某列就不行。
- 可以是唯一索引或普通索引
- 比单列索引能覆盖更复杂的查询条件
应用场景
- 多列组合查询频繁的情况
- 需要覆盖索引优化查询性能时
- 示例:INDEX idx_name_age (name, age)
- 全文索引
通过关键字的匹配来进行查询过滤,就是全文索引
只有字段的数据类型为 char、varchar、text 及其系列才可以建全文索引。
和常用的模糊匹配使用 like + % 不同,全文索引有自己的语法格式,使用 match 和 against 关键字
应用场景
- 文本内容的模糊匹配和关键词搜索
- 替代效率低的LIKE '%keyword%'查询
- 搜索引擎类应用
索引的利弊
优点
- 保证建立索引的列唯一性,生成唯一的rowId
- 有效缩短数据的检索时间
- 加快表与表之间的连接
- 加快分组和排序顺序
缺点
- 创建、维护时间成本大
- 空间成本大
- 影响增删改的效率
什么情况下需要建立索引?
- 数据量大
- 需要排序的字段
- 表与表连接用于多表查询的约束条件建立索引
回表查询、索引覆盖、索引下推、索引跳跃
- 回表查询
假设执行 select name, age from user where name=‘X’;
通过where name=‘X’ 看有没有对应的name索引,或者包含name的索引
找到二级索引 idx_name(name)
但它的叶子页只保存 name + 主键 PK,不包含 age。
于是优化器用 idx_name 找到主键,再回到聚簇索引页取出整行获取 age
说白了:查询的字段,二级索引没有包含

- 索引覆盖
想要查询通过name查询age,由于二级索引idx_name_age包含了name,通过name定位到叶子结点,查询的age字段索引也包含,直接将结果返回
说白了:你要根据条件查找的列,索引里全都有,于是直接在索引里把结果拿走,不用再回到主键树里取整行数据

- 索引下推
说白了:在索引的时候,在索引里判断把不合格的记录排除,再去回表拿剩下的少数几行。这样回表次数大幅减少,查询更快。

- 索引跳跃
若复合索引为 (age, gender),而查询只给了 gender = ‘m’ 没有 age 条件,
优化器就会启用 索引跳跃 (Skip Scan):
先枚举可能的 age 值(例如 18、20),
分别在 (age, *) 区间内利用 gender = ‘m’ 做范围查找,
从而避免全表扫描。

复合索引 (A, B) 下,只给了 B 的条件时,优化器会按 A 的每个不同取值 逐个做 小范围扫描(A=某值 AND B 满足条件),再把结果合并,这样可以避免全表扫描
**【问题】**查询age = 11,不是已经满足索引覆盖的条件了吗?age在复合索引(gender, age)中了
【答】:因为没有满足最左侧条件
索引失效的场景
- 不满足最左匹配原则
- 使用了SELECT *
- 索引列参与运算
- 索引列使用了函数
- 使用了错误的LIKE语句
- 类型隐式转换
- 使用OR且OR左右其中一列没有索引
- 不等于比较
- ORDER BY导致索引失效
索引越多越好吗?
- 每次增删改操作都要维护所有相关索引结构(更新B+树),写入时间上升
- 空间占用大
日志
执行一条 update 语句,期间发生了什么?

undo log
undo log 记录的是数据修改前的状态,例如
- 执行 UPDATE 语句时,undo log会记录修改之前的数据
- 执行 DELETE 语句时,undo log 会记录被删除行的完整数据(以便回滚时恢复)
- 执行 INSERT 时,undo log 会记录插入的行信息(回滚时直接删除这行)
应用场景
- 回滚事务:当事务需要被回滚时,通过Undo Log可以恢复到事务开始前的数据状态。
- MVCC(多版本并发控制):InnoDB利用Undo Log来提供不同事务之间的一致性读视图,使得事务可以看到其他事务未提交之前的旧版本数据,从而避免锁竞争,提高并发性能。

undo log版本链
我们每条数据其实都有两个隐藏字段,一个是trx_id,一个是roll_pointer
- trx_id:当前更新这条数据的事务ID
- roll_pointer指向上一个版本的指针

事务A id为50进行操作
事务B id为58进行操作
事务C id为69进行操作
redo log
Redo Log记录的是对数据库页的物理修改操作,即每次事务对数据页进行更改后,都会将变更以“redo record”的形式写入Redo Log。
应用场景
- 数据库崩溃恢复:当系统发生异常重启或宕机时,通过重放Redo Log,能够确保已提交事务的修改不会丢失,保证了事务的持久性。
- 避免频繁刷盘:InnoDB采用WAL(Write-Ahead Logging)策略,先写日志再修改磁盘数据,这样可以在一定程度上减少磁盘I/O,提升写入性能。

buffer pool

redo log刷盘时机
Redo log刷盘策略,innodb_flush_log_at_trx_commit,取值(0/1/2)
- 0:每隔一秒把log buffer刷到文件系统中
- 1:每次事务提交的时候,都把log buffer刷到文件系统中,立即刷新到磁盘上去
- 2:每次事务提交的时候,都把log buffer刷到系统文件中,但不会立即写入磁盘

日志文件组

在这个日志文件组中还有两个重要的属性,分别是 write pos、checkpoint
- write pos 是当前记录的位置,一边写一边后移
- checkpoint 是当前要擦除的位置,也是往后推移,检查点的推进依赖于脏页被成功刷新到磁盘的数据页上
当日志环将满时,InnoDB 会把最早那批未落盘的脏页刷进表空间文件,使 checkpoint 前移、Redo 日志重新腾出空间,再继续写后面的改动
两阶段提交
MySQL 为了避免出现redo log和bin log之间的逻辑不一致的问题,使用了「两阶段提交」来解决
两阶段提交是将单个事务的提交拆分成了2个阶段,【准备阶段】、【提交阶段】

- 【prepare阶段】:将XID(内部XA事务的ID)写入redo log中
同时将redo log对应的事务状态设置为prepare
将redo log持久化到磁盘 - 【commit阶段】:把XID写入binlog,然后将binlog持久化到磁盘,
调用引擎的提交事务接口,将redo log状态设置为commit
在两阶段提交的不同时刻,MySQL 异常重启会出现什么现象?


redo log思考
只要每次把修改后的数据页直接刷盘不就好了,还有 redo log 什么事?
【答】因为数据页大小为16KB,刷盘比较耗时间,如果写redo log,一行记录可能就占几十Byte,刷盘速度很快
bin log
bin log 记录的是数据库的逻辑修改事件(可以是 SQL 语句,也可以是行级别的修改记录,取决于格式),且只记录已提交的事务
作用
- 主从复制:主库把自己的 Binlog 推送给从库,从库按顺序 replay,保证数据一致
- 数据恢复:通过重放 Binlog 恢复数据到某个时间点或事务状态
三种格式
- Statement
- 记录每一条SQL语句
- 优点:日志量小
- 缺点:对于非等幂操作(NOW(), UUID())可能导致数据不一致
- Row
- 记录每一行数据的变化
- 更加精确,可避免语句级问题
- 缺点:日志量大
- Mixed
- 混合模式,看实际操作选择Statement、Row
bin log刷盘时机
对于 InnoDB 存储引擎而言,只有在事务提交时才会记录bin log ,此时记录还在内存中,那么 bin log是什么时候刷到磁盘中的呢?
bin log刷盘策略,sync_binlog,取值(0/1/N)
- 0:依赖操作系统定期同步,性能最佳,但崩溃时可能丢失多个事务
- 1:每次事务提交后同步,确保崩溃时最多丢失一个事务,安全性最高,但性能耗损最大
- N:每提交N个事务后同步一次,平衡性能和数据安全
中继日志
用于暂存从主库(Master)接收到的二进制日志事件,然后在本地执行的中间存储,确保数据同步的可靠性和持久性

慢查询日志
开启慢查询日志后,设定long_query_time,只要超过这个时间的SQL语句就会被记录下来
事务
事务是逻辑上的一组操作,要么都执行,要么都不执行
事务的开启方式
- 隐式事务:事务自动开启、提交、回滚,比如使用insert、update等语句
- 显式事务:需要手动开启、提交、回滚。命令BEGIN,COMMIT,ROLLBACK
事务ACID属性
- 原子性(Atomicity):事务是最小执行单位,要么全部完成,要么全部失败,【由undo log日志来实现】
- 一致性(Consistency):执行事务前后,数据保持一致,【由另外三个特性维持】
- 隔离性(Isolation):并发访问数据库时,事务不会被其他事务干扰【锁和MVCC机制来实现】
- 持久性(Duration):事务提交后,对数据库中数据改变时持久的【redo log日志来实现】
并发事务处理带来的问题
- 脏读:一个事务读取了另一个事务未提交的数据
- 不可重复读:一个事务内,前后读取的数据不一致。(修改)
- 在一个事务中,相同查询条件,返回发结果集的数量不一致(新增)
InnoDB 通过 临键锁 在当前读/写下避免RR ,而普通快照读在 RR 下就看不到新插入。
隔离级别

- 读未提交:直接绕过Read View
- 读已提交:每一条查询操作都会创建一次Read View
RC只有记录锁(行锁)
- 可重复读:第一次查询操作的时候创建一次Read View
RR有临键锁(记录锁+间隙锁)
- 串行化:在可重复读的情况下,将Select 语句替换为 Select … lock in share mode,同时也会给读到的区间加锁
RC和RR的锁有什么区别?
- 读已提交(RC)
RC保证了读已提交的数据,只看其他事务已经提交的东西,其他事务还在编辑、没提交的内容,看不到
RC通常使用行锁或者MVCC的方式,保证事务之间的隔离性
- 可重复读(RR)
RR保证了第一次快照读创建的Read View会贯穿整个事务,所以多次普通 Select 读取结果是一致的,解决了不可重复读问题
MVCC机制
快照读和当前读

- 快照读
使用MVCC实现
不加锁
- 当前读
使用临键锁(记录锁 + 间隙锁)实现
加锁
直接读物理行的当前版本,如果有并发写锁着它,就等待,等到对方提交并释放锁,再读最新已提交的版本
RR隔离级别完全解决了幻读吗?
没有,比如下面这种情况:
- 事务A使用快照读读取了3条数据
- 事务B插入ID = 100的数据并保存
- 此时事务A使用当前读就会读取到4条数据
这就产生了幻读现象
- 解决方法:在事务A的时候加间隙锁,此时事务B就不能插入数据了
MVCC(Multi-Version Concurrency Control)多版本并发控制
可以做到读写不阻塞,且避免了类似脏读这样的问题,主要通过undo Log版本链和Read View读视图来实现。
Read View读视图


【理解】

当使用快照读的时候,会生成一个Read View,给这个Read View分配一个creator_trx_id,同时会记录下这一瞬间仍在跑的事务id列表。
判断一行数据能不能看见,根据(当前行由事务Id X进行修改)
- 如果
X == creator_trx_id,代表是由当前事务自身进行修改的,可以查看 - 如果
X < min_trx_id,代表X事务不在活跃列表之前,已经被提交了,可以查看 - 如果
X ≥ max_trx_id,这行来自拍照之后还未开启/未提交的事务 → 这张快照里永远不可见 - 如果
min ≤ X < max,看X事务Id是否属于m_ids,① 在 → 不可见(还没提),②不在 → 可见(已提)
在可重复读(RR)隔离级别下,只会在第一次执行快照读获取一次read view
在读已提交(RC)隔离级别下,同一个事务里面,每一次执行快照读都会产生一个新的Read View副本,这样就可能造成同一个事务里前后读取数据可能不一致的问题(不可重复读并发问题)
Read View什么时候会消失?
- 在读已提交(RC)隔离级别下,语句结束,Read View就消失了
- 在可重复度(RR)隔离级别下,事务结束,Read View就消失了
MySQL调优
零碎知识点
内连接和外连接的区别
内连接保留两张表拥有的数据
外连接分为左外连接、有外连接、全外连接,匹配不到的地方用NULL填充
MySQL六大约束
- 主键约束
- 非空约束
- 唯一约束
- 默认值约束
- 外键约束







