SQL调优专题笔记:打造你的数据库性能优化思维体系

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 NULLIS 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 数据分布极度不均 genderstatus 这种区分度低的列 即使有索引,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 studentsSELECT * FROM students
2. 尽量过滤数据量 尽早在 SQL 中减少结果集 使用 WHERE 限定范围
3. 控制结果集大小 分页、限制输出 LIMIT 100LIMIT 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 字段建立联合索引
避免低选择性列建索引 genderstatus 等字段不宜建索引

📘 示例:

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。

📘 优化流程建议:

  1. 开启慢查询日志;

  2. 找出执行最慢的 SQL;

  3. 使用 EXPLAIN 分析;

  4. 优化语句或添加索引;

  5. 重新验证执行时间。

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)
   ↓
记录慢查询日志,持续优化
相关推荐
三无少女指南3 小时前
在 Ubuntu 上使用 Docker 部署思源笔记:一份详尽的实践教程以及常见错误汇总
笔记·ubuntu·docker
IvorySQL3 小时前
数据库内核的降维观测方法
数据库·postgresql
金仓拾光集3 小时前
__金仓数据库平替MongoDB全栈安全实战:从文档存储到多模一体化的演进之路__
数据库·安全·mongodb
金仓拾光集3 小时前
金仓数据库替代MongoDB实战:医疗设备日志实时监控场景的国产化平替实践
数据库·mongodb·数据库平替用金仓·金仓数据库
流烟默3 小时前
MongoDB中全文索引基础篇
数据库·mongodb
海边夕阳20064 小时前
数据源切换的陷阱:Spring Boot中@Transactional与@DS注解的冲突博弈与破局之道
java·数据库·spring boot·后端·架构
路弥行至4 小时前
C语言入门教程 | 第七讲:函数和程序结构完全指南
c语言·经验分享·笔记·其他·算法·课程设计·入门教程
胜天半月子4 小时前
性能测试 | 性能测试工具JMeter直连数据库和逻辑控制器的使用
数据库·测试工具·jmeter·性能测试
weixin_46684 小时前
Redis主从复制
数据库·redis·缓存