MySQL 进阶:UNION、SAVEPOINT、索引原理与索引失效
在日常 Java 后端开发中,MySQL 不只是会写 SELECT、UPDATE 就够了。
真正影响系统稳定性和性能的,往往是这些问题:
UNION和UNION ALL有什么区别?SAVEPOINT到底有什么用?索引为什么能加快查询?
聚簇索引、二级索引、联合索引分别是什么?
什么是回表、覆盖索引?
哪些情况下索引会“失效”?
怎么判断 SQL 到底有没有走索引?
这篇文章把这些知识点串起来,作为一篇 MySQL 基础进阶笔记。
一、UNION 与 SAVEPOINT
1. UNION 与 UNION ALL
UNION 可以理解成集合中的“并集”。
SELECT id FROM table_a
UNION
SELECT id FROM table_b;假设:
table_a: 1, 2, 3
table_b: 2, 3, 4结果:
1
2
3
4因为 UNION 会自动去重。
如果使用:
SELECT id FROM table_a
UNION ALL
SELECT id FROM table_b;结果:
1
2
3
2
3
4区别可以简单记成:
实际开发中,如果业务不要求去重,优先使用 UNION ALL。
2. SAVEPOINT 是什么
事务通常讲究:
要么全部成功,要么全部失败。
但是 MySQL 还提供:
SAVEPOINT用于在事务中设置保存点。
START TRANSACTION;
UPDATE product
SET stock = stock - 1
WHERE id = 100;
SAVEPOINT sp1;
INSERT INTO operation_log(...)
VALUES (...);
ROLLBACK TO SAVEPOINT sp1;
UPDATE orders
SET status = 'PAID'
WHERE id = 10001;
COMMIT;执行:
ROLLBACK TO SAVEPOINT sp1;只会撤销保存点之后的操作,事务本身不会结束。
可以理解成:
事务开始
↓
操作 A
↓
SAVEPOINT
↓
操作 B
↓
B 失败
↓
回滚到 SAVEPOINT
↓
继续操作 C
↓
COMMIT但要注意,SAVEPOINT 只负责局部回滚,它不会自动告诉你哪条数据失败、为什么失败、后续怎么补偿,这些都需要业务代码自行处理。
所以 SAVEPOINT 更适合“明确允许部分成功、部分失败”的业务;如果只要一条失败就要求全部回滚,直接使用 Spring 的 @Transactional 通常更安全。
二、索引为什么能加快查询
假设有一张 user 表:
SELECT *
FROM user
WHERE name = '张三';如果 name 没有索引,MySQL 可能需要逐行扫描,这就是全表扫描。
如果创建:
CREATE INDEX idx_name
ON user(name);MySQL 可以先通过索引快速定位,再读取真正的数据。
InnoDB 中最常见的索引结构是 B+Tree。
[1000 | 5000]
/ | \
<1000 1000~5000 >5000
| | |
继续分叉 继续分叉 继续分叉查询某个值时,不需要从第一行扫描到最后一行,而是:
根节点
↓
中间节点
↓
叶子节点快速定位。
B+Tree 一个节点可以存很多 key,树的高度较低,可以有效减少磁盘 IO,这也是它特别适合数据库索引的重要原因。
三、InnoDB 中几种重要索引
“索引也是一张表”是一个方便理解的说法,但并不严谨。
更准确地说:
索引是数据库为了提高查询效率而维护的额外数据结构。
例如业务表:
id | name | age
---|------|----
1 | 张三 | 20
2 | 李四 | 25
3 | 王五 | 30创建:
CREATE INDEX idx_name
ON user(name);可以简单理解为 MySQL 又维护了一份:
name | 主键ID
-----|------
张三 | 1
李四 | 2
王五 | 3并按 B+Tree 的形式组织。
1. 聚簇索引
InnoDB 中,主键索引通常就是聚簇索引。
CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
age INT
);主键索引 B+Tree 的叶子节点保存的是完整的一行数据。
因此:
SELECT *
FROM user
WHERE id = 100;通过主键找到叶子节点后,完整数据也就拿到了。
2. 二级索引与回表
除了聚簇索引之外的索引,通常叫二级索引。
例如:
CREATE INDEX idx_name
ON user(name);二级索引叶子节点主要保存:
索引字段 + 主键值执行:
SELECT *
FROM user
WHERE name = '张三';流程大致是:
idx_name B+Tree
↓
找到 张三
↓
得到主键 id = 100
↓
查询主键 B+Tree
↓
获取完整数据这就是“回表”。
3. 覆盖索引
如果查询:
SELECT id, name
FROM user
WHERE name = '张三';而 idx_name 本身已经包含 name 和主键 id,那么 MySQL 可以直接从二级索引返回数据,不需要回表。
这就是:
覆盖索引。
四、联合索引与最左匹配
例如:
CREATE INDEX idx_name_age_city
ON user(name, age, city);这不是创建三个独立索引,而是创建一棵 (name, age, city) 组成的 B+Tree。
数据排序大致类似:
张三 18 深圳
张三 20 北京
张三 25 上海
李四 18 深圳
李四 30 广州
王五 20 深圳先按 name 排序,name 相同时再按 age 排序,最后才是 city。
因此会有最左匹配原则。
下面这些通常可以很好利用索引:
WHERE name = '张三';WHERE name = '张三'
AND age = 20;WHERE name = '张三'
AND age = 20
AND city = '深圳';但:
WHERE age = 20;通常无法利用这个联合索引快速定位。
可以把联合索引理解成电话簿。电话簿按“姓 → 名字”排序,知道姓张很好找,但如果只知道名字叫“伟”,就不能很好地利用原来的排序规则。
五、常见索引失效场景
所谓“索引失效”,很多时候并不是索引真的坏了,而是:
SQL 无法有效利用索引,或者优化器判断不走索引成本更低。
1. 联合索引不满足最左匹配
CREATE INDEX idx_name_age_city
ON user(name, age, city);查询:
WHERE age = 20;通常无法使用这个联合索引快速定位。
2. 对索引字段使用函数或计算
有索引:
CREATE INDEX idx_create_time
ON orders(create_time);不推荐:
SELECT *
FROM orders
WHERE DATE(create_time) = '2026-10-07';推荐:
SELECT *
FROM orders
WHERE create_time >= '2026-10-07 00:00:00'
AND create_time < '2026-10-08 00:00:00';类似下面这些也要注意:
WHERE YEAR(create_time) = 2026;
WHERE id + 1 = 100;
WHERE LOWER(username) = 'parker';3. 隐式类型转换
假设:
phone VARCHAR(20)并且:
CREATE INDEX idx_phone
ON user(phone);不推荐:
WHERE phone = 13800138000;推荐:
WHERE phone = '13800138000';Java 项目中也应该尽量保证参数类型与数据库字段类型一致。
4. LIKE 前导 %
有索引:
CREATE INDEX idx_name
ON user(name);这种通常可以利用索引:
WHERE name LIKE '张%';但:
WHERE name LIKE '%张';或者:
WHERE name LIKE '%张%';普通 B+Tree 索引通常无法快速定位。
如果业务大量需要 LIKE '%关键词%',通常应该考虑 Elasticsearch 或 OpenSearch 这类全文检索方案。
5. 范围查询影响联合索引后续列
假设:
INDEX idx_a_b_c(a, b, c)查询:
WHERE a = 1
AND b > 10
AND c = 20;可以简单理解为:
a:用于定位
b:用于范围查询
c:不能像前面的等值条件一样继续缩小 B+Tree 搜索范围但 c 并不是完全没用,MySQL 还可能通过 ICP(索引条件下推)继续过滤。
6. OR、!=、IS NULL 并不是绝对失效
例如:
WHERE name = '张三'
OR description = '测试';如果 name 有索引,而 description 没索引,优化器可能选择全表扫描。
但这不代表 OR 一定导致索引失效;如果多个条件都有合适索引,MySQL 可能使用 Index Merge。
同样:
WHERE status != 1;也不是一定不走索引。
如果这个条件命中了绝大多数数据,走二级索引后还要大量回表,优化器可能认为直接全表扫描更便宜。
IS NULL 也是一样。MySQL 的索引可以包含 NULL,是否使用索引最终还是取决于成本。
7. 查询结果占比太大
例如:
user 表:100万条
gender:
男:50万
女:50万即使创建:
CREATE INDEX idx_gender
ON user(gender);执行:
SELECT *
FROM user
WHERE gender = '男';也可能不走索引,因为二级索引找到 50 万个主键后还要大量回表,成本可能比直接扫表更高。
所以性别、是否删除、是否启用、0/1 状态这类区分度很低的字段,不一定适合单独建立索引。
8. JOIN 字段类型不一致
例如:
table_a.user_id VARCHAR(32)
table_b.user_id BIGINT执行:
SELECT *
FROM table_a a
JOIN table_b b
ON a.user_id = b.user_id;可能发生类型转换,从而影响索引使用。
实际项目中 JOIN 字段最好保持:
数据类型一致
长度一致
字符集一致
排序规则一致尤其应该尽量避免 BIGINT ↔ VARCHAR。
六、如何判断 SQL 是否真正使用索引
不要靠猜,直接使用:
EXPLAIN
SELECT *
FROM user
WHERE name = '张三';重点关注:
type
possible_keys
key
rows
Extra例如:
possible_keys = idx_name
key = NULL表示 MySQL 认为 idx_name 是候选索引,但最终没有选择。
如果:
key = idx_name才说明实际使用了这个索引。
所以日常排查慢 SQL,更推荐这样的思路:
先看 EXPLAIN
↓
再看索引结构
↓
再看数据分布
↓
再看是否大量回表
↓
最后决定是否调整 SQL 或索引最值得记住的几个索引问题,可以浓缩成:
1. 联合索引不满足最左匹配
2. 索引字段上使用函数或计算
3. 隐式类型转换
4. LIKE '%xxx' 前导模糊
5. 范围查询影响联合索引后续列
6. 查询结果太多,优化器主动放弃索引最后把 InnoDB 的索引关系串起来:
InnoDB 表
│
┌──────────┴──────────┐
│ │
主键索引 二级索引
Clustered Index Secondary Index
│ │
B+Tree B+Tree
│ │
叶子节点 = 完整数据 叶子节点 =
索引字段 + 主键
│
↓
得到主键 ID
│
↓
主键索引查询
│
↓
完整数据
(回表)如果二级索引已经包含查询需要的全部字段,就可以避免回表,这就是覆盖索引。
总结
MySQL 索引真正需要理解的,不是单纯背“哪些 SQL 会索引失效”。
更重要的是把这条链路想明白:
索引是什么
↓
B+Tree 为什么适合数据库
↓
聚簇索引和二级索引有什么区别
↓
为什么会产生回表
↓
什么是覆盖索引
↓
联合索引为什么要求最左匹配
↓
优化器为什么有时候主动不走索引
↓
如何通过 EXPLAIN 验证最终可以总结成一句话:
索引的本质,是利用额外的数据结构和存储空间,减少查询过程中需要扫描的数据量。
而 SQL 优化真正要做的,是:
尽可能让 MySQL 用更少的扫描、更少的回表、更少的磁盘 IO,找到需要的数据。
实际开发中,遇到慢 SQL,第一反应不应该是凭经验猜,而应该先执行 EXPLAIN,再结合索引结构、数据分布、查询条件和返回行数进行分析。
