1.认识索引
1.1索引是什么
索引是数据库为了加快数据查询而引入的一种数据结构.
它的核心是通过维护有序的数据引用,让数据库查找的时候可以像二分查找一样快
1️⃣ 如果被问"那为什么索引能加快查询?"
可以答:
因为索引结构是有序的(例如 B+ 树),数据库可以通过二分或层级查找快速定位数据,而不是逐行扫描。
2️⃣ 如果被问"那为什么索引会影响写入?"
可以答:
因为每次插入、删除或更新数据时,数据库必须同步更新索引结构,保持有序性。
3️⃣ 如果被问"索引一定能提升性能吗?"
可以答:
不一定。索引适合高频查询字段,但在小表或频繁更新的表上,反而可能降低整体性能。
1.2索引的划分
一般分为聚集索引和二级索引。聚集索引决定数据的物理存储顺序,通常是主键;二级索引独立于数据文件,存储键值和主键指针。
聚集索引与二级索引的区别是什么?
聚集索引与二级索引的主要区别在于数据存储的位置。
聚集索引:它的叶子节点直接存储实际的数据行。数据表的记录按照聚集索引的顺序进行存储,因此每个表只能有一个聚集索引,通常是主键。
二级索引:它的叶子节点存储的是指向聚集索引的指针(即主键),而不是实际的数据行。当查询时,二级索引首先返回主键,然后根据主键去聚集索引中查找实际的数据行。
1.3MySQL 中所有索引类型与语法
MySQL 支持 5 种主要类型的索引:
| 索引类型 | 说明 | 示例语法 |
|---|---|---|
| 普通索引(INDEX / KEY) | 最常见,无唯一性要求 | CREATE INDEX idx_name ON users(name); |
| 唯一索引(UNIQUE INDEX) | 列值必须唯一,可为 NULL | CREATE UNIQUE INDEX idx_email ON users(email); |
| 主键索引(PRIMARY KEY) | 表中唯一且非空(自动创建) | ALTER TABLE users ADD PRIMARY KEY (id); |
| 全文索引(FULLTEXT INDEX) | 用于全文搜索(仅 CHAR/VARCHAR/TEXT) | CREATE FULLTEXT INDEX idx_content ON articles(content); |
| 空间索引(SPATIAL INDEX) | 用于几何数据(仅 MyISAM / InnoDB 支持部分) | CREATE SPATIAL INDEX idx_location ON maps(geo_point); |
cpp
CREATE [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name
ON table_name (column1 [ASC|DESC], column2 [ASC|DESC], ...);
或者用 ALTER TABLE 语句:
cpp
ALTER TABLE table_name
ADD [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name (column1, column2, ...);
1.4索引失效的总体规律
索引的核心作用是通过有序结构(B+树)来减少扫描行数。
但如果 SQL 写法、数据类型、或者查询条件破坏了这种"可预测性",MySQL 优化器就可能放弃使用索引,改用全表扫描(type=ALL)。
| 编号 | 原因 | 示例 | 说明与优化建议 |
|---|---|---|---|
| 1 | 在索引列上进行函数或表达式计算 | WHERE YEAR(create_time)=2024 |
MySQL 无法利用索引值比较,只能逐行计算。 ✅ 改写为:WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31' |
| 2 | 索引列进行了隐式类型转换 | WHERE id = '123' (id 为 INT) |
类型不同会导致索引失效。 ✅ 保持类型一致,例如 WHERE id = 123。 |
| 3 | 在索引列上使用通配符前缀的 LIKE | LIKE '%abc' |
% 开头无法使用索引。 ✅ 改为 LIKE 'abc%' 或用 全文索引 (FULLTEXT)。 |
| 4 | 使用 OR 连接不同字段的条件 | WHERE name='张三' OR age=20 |
若其中一个字段无索引,则全表扫描。 ✅ 改为 UNION ALL 或确保所有字段都有索引。 |
| 5 | 索引字段参与运算或使用负号等操作 | WHERE id + 1 = 10 |
运算使得优化器无法利用索引。 ✅ 改为 WHERE id = 9。 |
| 6 | 不符合联合索引的最左前缀原则 | 索引为 (a,b,c),条件为 WHERE b=1 AND c=2 |
联合索引只会用到最左连续列。 ✅ 改为 WHERE a=? AND b=?,或调整索引顺序。 |
| 7 | 在 WHERE 中使用 IS NULL 或 IS NOT NULL |
WHERE age IS NULL |
IS NULL 可用索引,IS NOT NULL 一般无法使用。 ✅ 避免过多使用 IS NOT NULL。 |
| 8 | 在范围查询后再使用索引列 | WHERE a > 10 AND b = 5 (索引 a,b) |
a 是范围条件,b 的索引失效。 ✅ 尽量把等值条件放在前面。 |
| 9 | 使用 NOT、!=、<>、NOT IN 等 | WHERE age != 18 |
索引跳跃性太大,优化器倾向全表扫描。 ✅ 尽量改为正向条件或范围查询。 |
| 10 | 使用 OR 包含不同表列时 | WHERE t1.id = 1 OR t2.name = 'Tom' |
无法在多个表索引上联合优化。 ✅ 拆分查询或使用 UNION。 |
| 11 | 字符串不加引号 | WHERE name = 123 |
MySQL 自动转换导致索引失效。 ✅ 文本值一定要加引号。 |
| 12 | 数据分布极度不均 | gender、status 这种区分度低的列 |
即使有索引,MySQL 优化器可能认为全表扫描更快。 ✅ 不为低选择性列建索引。 |
| 13 | 使用 OR 且混合索引与非索引列 | WHERE name='Tom' OR score>90 |
部分条件不能使用索引 → 全表扫描。 ✅ 拆分为两条查询再 UNION ALL。 |
| 14 | ORDER BY 与索引顺序不一致 | ORDER BY b,a 而索引为 (a,b) |
会出现 Using filesort。 ✅ 与索引顺序保持一致。 |
| 15 | 使用 SELECT * 并非覆盖索引 | SELECT * 会导致回表 |
✅ 只查询索引列,形成覆盖索引。 |
2.执行计划(EXPLAIN)
2.1认识执行计划(EXPLAIN)
EXPLAIN 是 MySQL 提供的分析工具,用来查看一条 SQL 语句的执行计划。
它展示了优化器如何执行查询,比如:
每个表的访问顺序;
使用了哪些索引;
连接类型(ALL、index、ref、const 等);
预估扫描的行数(rows);
以及可能的过滤条件(filtered)等。
通过 EXPLAIN,我们可以判断 SQL 是否走索引、是否存在全表扫描,从而优化查询性能。
2.2执行计划(EXPLAIN)中各个变量的含义

MySQL EXPLAIN 字段详细说明表
| 字段名 | 含义 | 常见取值/示例 | 详细解释与优化提示 |
|---|---|---|---|
| id | 查询的执行顺序标识 | 1, 2, 3... | SQL 中每个 SELECT 都会分配一个 id。 数值越大,越先执行;同值表示并行执行。 建议: 通过 id 分析子查询执行顺序。 |
| select_type | 查询类型 | SIMPLE、PRIMARY、SUBQUERY、DERIVED、UNION | - SIMPLE:无子查询或 UNION; - PRIMARY:最外层查询; - SUBQUERY:SELECT 中包含的子查询; - DERIVED:FROM 子句中的子查询(派生表); - UNION:UNION 中的第二个及后续查询。 |
| table | 当前访问的表 | students、orders 等 | 表示当前执行计划中操作的表名或别名。 如果是临时表或派生表 ,这里会显示 <derivedX>。 |
| partitions | 表分区信息 | NULL、p0,p1 | 如果表有分区,这里显示被访问的分区;否则为 NULL。 |
| type | 连接类型(重要性能指标) | const、eq_ref、ref、range、index、ALL | 性能排序(由优到劣): system > const > eq_ref > ref > range > index > ALL。 • const:主键或唯一索引等值查询,只匹配一行。 • ALL:全表扫描,需重点优化! |
| possible_keys | 可能使用的索引 | PRIMARY, idx_name | MySQL 认为可能可用的索引。可通过 EXPLAIN EXTENDED 查看更精确估计。 |
| key | 实际使用的索引 | PRIMARY、idx_major | 显示真正被使用的索引名称。若为 NULL,说明没用上索引。 |
| key_len | 索引长度(字节) | 4、10、767 | 表示 MySQL 实际使用索引的字节数。 例如 INT=4 字节,VARCHAR(10)=10 字节。 |
| ref | 与索引列比较的对象 | const、func、列名 | 显示索引列与哪个值进行比较。const 表示常量值,如 WHERE id = 1。 |
| rows | 预估扫描的行数 | 1、10、10000 | MySQL 预估要读取的行数(非实际值)。行数越少,查询越高效。 |
| filtered | 过滤比例(百分比) | 100.00、10.00 | 表示通过条件筛选后,预计保留的行数比例。 越接近 100 越好。 |
| Extra | 额外信息 | Using where、Using index、Using temporary、Using filesort | 显示执行的额外操作: • Using where:使用 WHERE 条件过滤; • Using index:覆盖索引(不访问表); • Using temporary:使用临时表; • Using filesort:需要额外排序(通常需优化)。 |
常见 type 值性能对比表
| type 值 | 说明 | 性能等级 |
|---|---|---|
| system | 表中只有一行记录 | ✅ 最优 |
| const | 主键或唯一索引查单行 | ✅ 很快 |
| eq_ref | 唯一索引等值连接 | ✅ 优秀 |
| ref | 非唯一索引等值连接 | ⚙️ 一般 |
| range | 索引范围查询(BETWEEN, >, < 等) | ⚙️ 可接受 |
| index | 全索引扫描 | ❌ 较慢 |
| ALL | 全表扫描 | 🚫 最差,需优化 |
3.优化SELECT
使用SELECT语句查询数据库中的数据是最频繁的操作,优化查询是使用数据库的重中之重,在查
询数据时常用的条件查询、范围查询、表连接查询都可以进行优化,同样对于查询结果的处理,比如
排序、分组、去重、限制也都可以进行优化。还有一点,程序员编写SQL语句时,出于可读性考虑和
自身水平问题,提交到MySQL服务器的SQL风格各不相同,这样可能导致执行效率参差不齐,MySQL
内置的优化器可以帮助我们完成一部分优化操作,但了解具体的优化方法有助于写出高性能的SQL语
句,下面分别讨论不同场景下的查询优化方法。
3.1基础优化原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 1. 查询字段精简 | 避免 SELECT *,只取需要的列 |
✅ SELECT name, age FROM students ❌ SELECT * FROM students |
| 2. 尽量过滤数据量 | 尽早在 SQL 中减少结果集 | 使用 WHERE 限定范围 |
| 3. 控制结果集大小 | 分页、限制输出 | LIMIT 100、LIMIT 10 OFFSET 20 |
🔹 原则:取最少的数据、查最小的范围、用最合适的索引。
3.2 WHERE 条件优化
1.避免在索引列上使用函数或计算
cpp
-- ❌ 索引失效
SELECT * FROM users WHERE YEAR(create_time) = 2024;
-- ✅ 推荐
SELECT * FROM users WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31';
2.避免隐式类型转换
cpp
-- ❌ id 是 INT,右边是字符串
WHERE id = '100';
-- ✅
WHERE id = 100;
3.避免使用 OR 连接不同字段
cpp
-- ❌ 索引失效
WHERE id = 1 OR name = 'Tom';
-- ✅ 改写
WHERE id = 1
UNION ALL
SELECT * FROM users WHERE name = 'Tom';
4.利用索引最左前缀原则
cpp
-- ❌ 索引失效
WHERE id = 1 OR name = 'Tom';
-- ✅ 改写
WHERE id = 1
UNION ALL
SELECT * FROM users WHERE name = 'Tom';
3.3索引使用优化
| 优化方式 | 说明 |
|---|---|
| 单列索引 | 为高频过滤字段建立索引 |
| 复合索引 | 常用于多条件查询,遵循"最左前缀原则" |
| 覆盖索引 | 查询列完全被索引包含,避免回表 |
| 合理排序索引 | 对 ORDER BY 字段建立联合索引 |
| 避免低选择性列建索引 | 如 gender、status 等字段不宜建索引 |
📘 示例:
cpp
CREATE INDEX idx_name_age ON students(name, age);
SELECT name, age FROM students WHERE name='张三' AND age=20;
➡️ 可使用覆盖索引,无需访问表数据。
3.4排序与分组优化(ORDER BY / GROUP BY)
1.ORDER BY 与索引顺序一致
cpp
-- 有联合索引 idx(a,b)
SELECT * FROM t ORDER BY a,b; ✅ 可用索引
SELECT * FROM t ORDER BY b,a; ❌ 索引失效
2.避免文件排序 (Using filesort )
若 EXPLAIN 的 Extra 出现 "Using filesort",代表额外排序,可通过调整索引解决。
3.GROUP BY 优化
- 若可能,使用索引字段分组;
- 禁止在 GROUP BY 前进行函数操作。
3.5 JOIN 优化
1.确保关联字段类型一致、且有索引
cpp
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id; ✅
2.小表驱动大表(Nested Loop Join 原理)
cpp
SELECT * FROM small_table s
JOIN big_table b ON s.id = b.sid;
3.控制 JOIN 数量
超过 3 个以上 JOIN 通常性能下降,应考虑中间表或预处理。
3.6子查询优化
| 不推荐 | 推荐 |
|---|---|
| 使用嵌套子查询 | 改写为 JOIN 或 EXISTS |
📘 示例:
cpp
-- ❌ 子查询效率低
SELECT name FROM student WHERE id IN (SELECT student_id FROM score);
-- ✅ JOIN 优化
SELECT s.name FROM student s JOIN score sc ON s.id = sc.student_id;
3.7 分页优化
1.大偏移分页性能差
cpp
SELECT * FROM orders LIMIT 100000, 20; -- ❌
2.推荐基于索引定位分页
cpp
SELECT * FROM orders WHERE id > 上次最大ID LIMIT 20;
3.或使用覆盖索引 + 子查询
cpp
SELECT o.*
FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000,20) t
ON o.id = t.id;
3.7 IS NULL优化
1.普通写法:IS NULL 查询可使用索引 ✅
cpp
SELECT * FROM students WHERE age IS NULL;
- 说明:
如果字段 age 上存在索引(如 INDEX idx_age(age)),
则 IS NULL 查询是可以使用索引的。
MySQL 的 B+Tree 索引会存储 NULL 值,并且将它们放在排序的最前端。
因此查询 IS NULL 时,可以快速定位这些记录。
- 优化效果:
通过索引直接找到所有 age 为 NULL 的行,无需全表扫描。
2.IS NOT NULL 通常无法使用索引 ❌
cpp
SELECT * FROM students WHERE age IS NOT NULL;
- 原因:
IS NOT NULL 会匹配大量记录(几乎整个表),
优化器会判断全表扫描比走索引更快,因此通常放弃索引。
- 解决方案:
若 NULL 值比例较高,可尝试:
cpp
SELECT * FROM students FORCE INDEX(idx_age) WHERE age IS NOT NULL;
或通过默认值替代 NULL(如 age=0);
或添加辅助标志位字段(如 has_age TINYINT(1)),再对其建立索引。
| 查询语句 | 索引使用 | 原因 | 建议 |
|---|---|---|---|
WHERE age IS NULL |
✅ 可能使用 | B+Tree 中保存 NULL 值 | 可直接使用索引 |
WHERE age IS NOT NULL |
⚠️ 一般不使用 | 范围过大,全表扫描更快 | 用默认值或辅助字段优化 |
4.深入理解索引与查询优化
4.1 索引底层原理与结构
索引的核心结构是 B+Tree。
MySQL 之所以采用 B+Tree,是因为它能在磁盘上高效地实现范围查找和顺序扫描。
B+Tree 的特性:
- 所有数据都存储在叶子节点,内部节点只存储键值和指针;
- 叶子节点之间通过链表相连,便于范围查询;
- 每个节点对应磁盘页,减少磁盘 I/O 次数;
- 高度平衡(一般 2~4 层即可存储上百万行)。
📘 示意图简述:
根节点
├── 分支节点(索引项+指针)
│ ├── 叶子节点(存放实际数据或主键引用)
│ └── 叶子节点(顺序链接)
👉 因为 B+Tree 节点是有序的,所以数据库可以像二分查找那样快速定位记录。
4.2 B+Tree 与 Hash 索引对比
| 特性 | B+Tree 索引 | Hash 索引 |
|---|---|---|
| 查询类型 | 范围、排序、高效等值查询 | 仅支持等值查询 |
| 是否有序 | ✅ 有序 | ❌ 无序 |
| 支持范围查询 | ✅ 支持 | ❌ 不支持 |
| 适用场景 | 通用(大多数表) | 内存型表(Memory 引擎) |
| MySQL 支持 | InnoDB、MyISAM 等 | MEMORY 引擎 |
📘 结论:
大多数业务场景使用 B+Tree 索引 ,因为它在排序、范围查询上优势明显。
Hash 仅在非常特定的内存表中使用。
4.3 覆盖索引与回表
覆盖索引(Covering Index):
当查询的列 全部被索引覆盖 时,无需访问表数据,直接从索引中返回结果。
📘 示例:
cpp
CREATE INDEX idx_name_age ON students(name, age);
-- ✅ 覆盖索引查询(不需要回表)
SELECT name, age FROM students WHERE name = '张三';
-- ❌ 非覆盖索引(需要回表)
SELECT * FROM students WHERE name = '张三';
判断方式:
- EXPLAIN 的 Extra 字段显示 Using index,说明是覆盖索引。
优势:
- 避免二次 I/O;
- 显著提升查询性能。
4.4 索引维护与监控
索引不是"建了就完事",需要定期维护与监控。
✅ 查看表索引信息
cpp
SHOW INDEX FROM students;
✅ 检查 SQL 是否走索引
cpp
EXPLAIN SELECT * FROM students WHERE id=1;
✅ 分析索引命中率
cpp
SHOW STATUS LIKE 'Handler_read%';
| 指标 | 含义 |
|---|---|
Handler_read_key |
通过索引读取的次数 |
Handler_read_rnd_next |
全表扫描的次数 |
| 👉 Handler_read_key 越高越好,Handler_read_rnd_next 越低越好。 | |
| ✅ 重建索引统计信息 |
cpp
ANALYZE TABLE students;
OPTIMIZE TABLE students;
4.5 慢查询日志优化流程
开启慢查询日志:
cpp
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1; -- 记录超过1秒的SQL
查看慢查询日志路径:
cpp
SHOW VARIABLES LIKE 'slow_query_log_file';
分析工具:
- mysqldumpslow:官方工具,汇总慢查询;
- pt-query-digest:Percona 提供,统计最耗时 SQL。
📘 优化流程建议:
-
开启慢查询日志;
-
找出执行最慢的 SQL;
-
使用 EXPLAIN 分析;
-
优化语句或添加索引;
-
重新验证执行时间。
4.6 索引选择性与优化器决策
索引选择性 = 不同值数量 ÷ 总行数。
选择性越高,索引越有效。
📘 示例:
- gender 字段只有 "男/女" → 选择性低;
- email 字段几乎唯一 → 选择性高。
优化器会根据选择性判断是否走索引。
当字段区分度过低时(例如 99% 都为同一个值),MySQL 可能选择 全表扫描。
4.7 复合索引规则总结
| 索引定义 | 查询条件 | 是否用上索引 | 说明 |
|---|---|---|---|
| (a, b, c) | WHERE a=1 | ✅ | 使用第一列 |
| (a, b, c) | WHERE a=1 AND b=2 | ✅ | 使用前两列 |
| (a, b, c) | WHERE b=2 | ❌ | 不满足最左前缀 |
| (a, b, c) | WHERE a>1 AND b=2 | ⚠️ | 范围查询后 b 列失效 |
口诀: 等值条件在前,范围条件在后;使用时遵守"最左前缀原则"。
4.8 查询优化流程图
cpp
SQL语句
↓
EXPLAIN 分析执行计划
↓
判断是否走索引 / 是否回表
↓
优化 WHERE / JOIN / ORDER BY
↓
调整或添加索引
↓
验证性能差异(执行时间、rows、type)
↓
记录慢查询日志,持续优化