一、 索引
1. 为什么需要索引
在数据库中,数据通常以表的形式存储。
当表中数据量较小时,直接扫描整张表问题不大;
但当数据量达到几十万、上百万行时,查询性能会急剧下降。
如果没有索引,数据库只能从第一行开始逐行扫描,这称为全表扫描 ,时间复杂度近似为 O(n)。
索引的作用,就是通过额外的数据结构,帮助数据库快速定位数据,将查询复杂度降低到 O(log n)。
说白了创建索引就是给数据库创建目录,使用索引是为了快速查询
2. 索引的本质
索引的本质是一种 "空间换时间" 的技术。
数据库会额外维护一份有序的数据结构,用来保存:
(索引列的值 → 对应数据行的位置)
查询时,数据库先查索引,再根据索引结果定位数据,从而避免扫描整张表。
3. 索引底层用了啥数据结构
索引底层数据结构存在很多种类型,常见的索引结构有:B 树、 B+ 树 和 Hash、红黑树 。
在 MySQL 中 ,无论是 Innodb 还是 MyISAM,都使用了 B+ 树 作为索引结构。
(推荐先简单了解一下B+树)
B+ 树相比普通二叉树,具有以下优势:
- 一个节点可以存放多个 key
→ 树的高度更低
→ 磁盘 IO 次数更少 - 所有数据都存放在叶子节点
→ 查询路径稳定 - 叶子节点通过链表相连
→ 非常适合范围查询和排序
举个例子
"假设user表是一本按学号排名的同学录,userId(主键)是学号。
查学号100的同学:先看总目录(根节点):"1-50班在卷一,51-100班在卷二,101-150班在卷三" → 学号100在卷二
再看分卷目录(中间节点):"51-75班在第二章,76-100班在第三章" → 学号100在第三章
最后翻到具体页(叶子节点):找到学号100那一页,上面有他的详细信息(姓名、年龄等)
链表就像页码:
如果你要查"学号100到120的所有同学",找到100那一页后,直接往后翻页就行(顺着链表),不需要回头重新查目录。
这就是B+树叶子的链表作用------范围查询不用走回头路。"
4. MySQL 中常见的索引类型
主键索引(聚簇索引)
主键索引是建立在 主键字段 上的索引。
- 一张表只能有一个主键
- 主键 不能为 NULL,不能重复
- 在 InnoDB 中:
- 主键索引就是 聚簇索引
- 数据和索引 存储在同一棵 B+ 树中
补充:
- 如果表没有显式定义主键:
- InnoDB 会优先选择 非空的唯一索引
- 如果也没有,则自动生成一个 6 字节隐藏自增主键
二级索引(Secondary Index)
除主键索引外,其余索引统称为 二级索引,也叫 辅助索引 / 非主键索引。
特点:
- 二级索引的 叶子节点存的是主键值
- 通过二级索引找到主键后,再通过主键索引查数据(可能回表)
二级索引的常见类型:
- 唯一索引(UNIQUE):值不能重复,允许为 NULL,主要用于保证数据唯一性。
- 普通索引(INDEX):最常用的索引,不要求唯一,仅用于加速查询。
- 前缀索引(PREFIX):适用于字符串类型,只对字段的前 N 个字符建索引,减小索引体积,提高效率。
- 全文索引(FULLTEXT):用于大文本关键词搜索。MySQL 5.6 以后 InnoDB 也支持。
聚簇索引和非聚簇索引
- 聚簇索引:索引和数据存放在一起(InnoDB 的 主键索引),查一次索引就能拿到数据。
- 非聚簇索引 :索引和数据分开存放(InnoDB 的 二级索引),可能需要 回表查询。
联合索引
INDEX(name, age) 遵循最左前缀原则。
覆盖索引
如果查询的字段 完全被索引覆盖,就可以直接从索引中返回结果,这时候不一定回表。
5. 什么是回表,为何要回表
当使用普通索引(二级索引)查询,且查询字段不在索引中时,数据库需要:
- 先通过普通索引找到主键
- 再通过主键索引查找完整数据
这个过程称为 回表。
6. 选择合适的字段创建索引
- 不为 NULL 的字段:索引字段的数据应该尽量不为 NULL,建议使用 0、1、true、false 这样语义较为清晰的短值或短字符作为替代。
- 被频繁查询的字段:我们创建索引的字段应该是查询操作非常频繁的字段。
- 被作为条件查询的字段:被作为 WHERE 条件查询的字段,应该被考虑建立索引。
- 频繁需要排序的字段:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
- 被经常频繁用于连接的字段:对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。
7. 索引失效
索引失效的本质原因:MySQL 无法通过索引,快速缩小数据扫描范围。
换句话说:能不能用索引,不是看「你建没建」,而是看「优化器能不能利用索引的有序性,减少扫描行数」。
一旦 索引的有序性、可比较性、可裁剪性 被破坏,优化器就会认为走索引比全表扫描更慢,于是选择放弃索引。
常见索引失效的底层原因:
- 使用 SELECT *
- 本质原因:二级索引不包含整行数据,若回表量巨大,优化器会放弃索引。
- 联合索引不遵守最左前缀原则
- 例如
INDEX(a, b, c)查WHERE b = 2。 - 本质原因:B+ 树按 (a, b, c) 排序,a 不确定时,b 是无序的,无法定位。
- 例如
- 在索引列上使用函数 / 计算 / 表达式
- 本质原因:索引列参与运算,导致索引的有序性被破坏。
- 以 % 开头的 LIKE 查询
- 本质原因:前缀未知,索引无法确定起始扫描位置。
- OR 条件中存在未索引列
- 本质原因:索引无法覆盖所有分支条件,合并成本过高。
- IN 取值过多 / NOT IN
- 本质原因:索引选择性过低,过滤效果太差。
- 发生隐式类型转换
- 本质原因:数据库会对列进行函数加工(如 CAST),导致索引失效。
8. 索引优缺点
索引的优点:
- 查询速度起飞:大幅减少磁盘 I/O 次数。
- 保证数据唯一性:如主键索引、唯一索引。
- 加速排序和分组:利用索引的有序性,避免额外排序。
索引的缺点:
- 创建和维护耗时:DML 操作(增删改)时,索引需要同步更新。
- 占用存储空间:索引是物理文件,会额外消耗磁盘。
- 可能被误用或失效:设计不当会导致性能不升反降。
用了索引就一定能提高查询性能吗?
不一定。如果数据量太小,或者查询结果占全表比例过大(20%-30%以上),优化器可能认为全表扫描更快。
二、 事务
1. 事务是什么
1.1 什么是事务
事务是一组逻辑操作的集合,这些操作具有不可分割性,即要么全部成功,要么全部失败。
典型例子:银行转账
textA 账户余额 -100 元 B 账户余额 +100 元这两步操作必须作为一个整体执行。如果第一步成功而第二步失败,钱就会凭空消失,这在金融系统中是绝不允许的。
1.2 为什么需要事务
如果没有事务保护,数据库会面临以下风险:
- 执行中断:操作只执行了一半(导致数据不一致)。
- 并发冲突:多个用户同时读写同一行数据,互相干扰。
- 系统崩溃:系统在数据写入中途宕机,产生残留的中间状态。
事务的本质目标: 保证数据的正确性 和可靠性。
2. 事务的四大特性 (ACID)
ACID 是衡量事务的四个核心维度
2.1 原子性(Atomicity)
一个事务中的所有操作,要么全部完成,要么全部不完成。不存在"执行一半"的情况。如果中途失败,必须回滚(Rollback到事务开始前的状态。
- 实现关键 :使用 Undo Log(回滚日志)。
2.2 一致性(Consistency)
事务执行前后,数据库都必须处于一致的状态。一致性是事务追求的最终目的,它不仅依赖于数据库机制,也依赖于应用逻辑。
- 表现形式:转账前后总金额不变、满足唯一约束、外键约束等。
- 一致性的来源:由原子性、隔离性、持久性共同支撑。
2.3 隔离性(Isolation)
多个事务并发执行时,彼此之间应互不干扰。隔离性防止了多个事务并发执行时由于交叉执行而导致数据的不一致。
- 实现关键 :锁机制 + MVCC(多版本并发控制)。
2.4 持久性(Durability)
一旦事务提交,其结果就会被永久保存到磁盘中。即使 MySQL 服务崩溃或服务器宕机,数据也不会丢失。
- 实现关键 :使用 Redo Log(重做日志)。
3. 事务是如何实现的(底层机制)
3.1 原子性的实现:Undo Log(撤销日志)
1. 什么是「原子性」?
原子性一句话理解:一个事务里的操作,要么全成功,要么全失败,绝不允许"做到一半"。
举个最经典的例子:A 给 B 转账 100 元。如果 A 扣钱成功,B 加钱失败,钱凭空消失了,这就违反了原子性。
2. Undo Log 是干嘛的?
Undo Log 本质是:数据库的"后悔药 / 时光机"。每一条"改数据"的操作,都会提前记录一条"反向操作"。
| 原操作 | Undo Log 里记录的内容 |
|---|---|
| INSERT 一条数据 | DELETE 这条数据 |
| DELETE 一条数据 | INSERT 原数据 |
| UPDATE old -> new | UPDATE new -> old |
3. Undo Log 的工作流程(重点)
我们用一个修改余额的例子:假设原来是 money = 1000,要修改为 900。
- 第一步:先写 Undo Log。还没真正改数据!Undo Log 中记录:把 id=1 的 money 从 900 改回 1000。
- 第二步:再修改数据页。内容变为 money = 900。
- 第三步:事务提交 or 回滚。
- 提交成功:Undo Log 暂时留着(给 MVCC 用)。
- 事务失败 / 回滚:执行 Undo Log,把 900 改回 1000。
4. 为什么一定要"先写 Undo Log"?
因为一旦你先改数据,再写日志:
- 中途宕机
- 日志没写完
- 数据已经乱了
数据库就救不回来了。所以结论是:Undo Log 是原子性的"物理基础",没有 Undo Log,就没有回滚能力。
3.2 持久性的实现:Redo Log(WAL 原则)
1. 什么是「持久性」?
持久性一句话理解:事务一旦提交成功,即使数据库立刻宕机,数据也不能丢。
2. 问题来了:为什么不直接写磁盘?
磁盘 IO 有两个致命问题:
- 慢
- 随机写更慢
而事务提交要求的是:快 + 可靠。
3. Redo Log 是什么?
Redo Log 本质是:"我改了哪些数据"的操作记录。它记录的是:
- 改了哪个页
- 改了什么内容
注意:Redo Log 不等于数据,它是"操作说明书"。
4. WAL(Write-Ahead Logging)原则
核心规则只有一句话:先写日志,再写数据。
事务提交流程如下:
- 修改数据(在内存中)。
- 把修改记录写入 Redo Log(顺序写,极快)。
- 返回:事务提交成功。
- 后台线程慢慢把数据刷到磁盘。
5. 为什么 Redo Log 能保证不丢数据?
假设最极端情况:
- 事务刚提交成功
- Redo Log 已写入磁盘
- 数据页还在内存
- 服务器直接断电
重启后:MySQL 读取 Redo Log,按日志重新"重放操作",数据恢复到提交时的状态。所以结论是:Redo Log = 持久性的底层保障。
3.3 隔离性的实现:锁 + MVCC
1. 什么是「隔离性」?
隔离性一句话理解:多个事务同时执行,互不干扰,看起来像"一个一个执行"。
2. 锁:解决"写冲突"
锁解决的是:"不能同时改同一行数据"。
InnoDB 的常见锁:
- 行锁(重点)
- 表锁
- 间隙锁(防幻读)
但问题是:如果全靠锁,读也要加锁,性能会大幅下降。
3. MVCC:不加锁也能读
MVCC = 多版本并发控制。核心思想:一行数据,不止一个版本。
每行数据隐含:
- 创建版本号
- 删除版本号
- Undo Log 指针
4. 快照读是怎么实现的?
当事务开始时:创建一个 Read View,规定你"能看到哪些事务的数据"。
读数据时:
- 如果当前版本不可见
- 沿着 Undo Log 找旧版本
这就是为什么普通 SELECT 不加锁,却还能读到"正确的旧数据"。
5. 一句话总结隔离性
| 手段 | 解决问题 |
|---|---|
| 锁 | 写写冲突 |
| MVCC | 读写并发 |
3.4 一致性的真正来源
1. 一致性不是"某一种技术"
很多人会问:一致性是用 Undo Log 实现的吗?是用 Redo Log 实现的吗?答案是:都不是单独的。
2. 一致性真正的含义
一致性指的是:数据从一个"合法状态"转换到另一个"合法状态"。
例如:转账前 A+B = 2000,转账后 A+B = 2000。
3. 一致性靠什么保证?
一致性 = 多个因素共同作用的结果:
原子性(不做一半) + 隔离性(事务不互相干扰) + 持久性(提交不丢) + 数据库约束(外键、唯一键、NOT NULL) = 一致性。
4. 一个反例帮你彻底理解
如果:
- 没有原子性:转账做到一半。
- 没有隔离性:多个事务乱读。
- 没有持久性:提交后丢数据。
一致性一定被破坏。所以结论是:一致性是结果,不是实现手段。
4. 并发事务带来的问题
当多个事务同时操作相同数据时,如果隔离级别不够,会产生以下三大问题:
- 脏读 (Dirty Read) :事务 A 读到了事务 B 尚未提交的数据。
- 不可重复读 (Non-repeatable Read) :事务 A 在同一事务内两次读取同一条数据,结果值不同(中途被事务 B 修改并提交了)。
- 幻读 (Phantom Read) :事务 A 按照同一条件查询,两次读取得到的行数不同(中途被事务 B 插入或删除了行)。
5. 事务隔离级别
为了权衡安全性 和并发性能,SQL 标准定义了四个隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| Read Uncommitted (读未提交) | ✅ | ✅ | ✅ | 最高 |
| Read Committed (读已提交) | ❌ | ✅ | ✅ | 中 |
| Repeatable Read (可重复读) | ❌ | ❌ | ⚠️ | 中上 |
| Serializable (可串行化) | ❌ | ❌ | ❌ | 最低 |
- 注意 :MySQL 的 默认隔离级别是 Repeatable Read (RR)。
- 亮点 :InnoDB 存储引擎在 RR 级别下,通过 间隙锁 (Gap Locks) 在很大程度上解决了幻读问题。
6. MySQL 中事务的使用
标准操作流程
sql
-- 1. 开启事务
START TRANSACTION;
-- 2. 执行一系列业务 SQL
UPDATE account SET money = money - 100 WHERE id = 1;
UPDATE account SET money = money + 100 WHERE id = 2;
-- 3. 提交事务(持久化到磁盘)
COMMIT;
-- 或者在出错时回滚
-- ROLLBACK;