0
0
0

MySQL 进阶:UNION、SAVEPOINT、索引原理与索引失效

2023-01-07
2026-10-07
文章摘要
|

在日常 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

是

相对较慢

UNION ALL

否

通常更快

实际开发中,如果业务不要求去重,优先使用 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,再结合索引结构、数据分布、查询条件和返回行数进行分析。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论