Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

后端工程 / Backend Engineering

1. Backend 后端技术全景学习路线 / A Complete Backend Engineering Learning Path

2. 分布式系统——为什么单体时代过去了 / Distributed Systems Beyond the Monolith

3. 并发编程——为什么你的库存总是扣成负数 / Concurrency Control and Inventory Consistency

4. MySQL 优化——为什么你的查询总是那么慢 / MySQL Query and Storage Optimization

5. 架构模式——什么时候该用 CQRS / Architecture Patterns and When to Use CQRS

6. 系统设计——如何设计一个每秒 10 万订单的秒杀系统 / Designing a High-Throughput Flash-Sale System

7. 数据库工程化——在线表结构变更怎么不停服 / Database Engineering and Online Schema Changes

8. 可观测性——出了问题怎么快速定位 / Observability and Rapid Production Diagnosis

本页目录

MySQL 优化——为什么你的查询总是那么慢 / MySQL Query and Storage Optimization ​

📅 创建时间:2026-05-08 🏷️ 标签:#MySQL #慢查询 #EXPLAIN #索引优化 #执行计划 #锁 📚 前ore置知识:[[00-backend-overview]] [[/03-web/06-databases-and-data-access/01-mysql]](MySQL 基础) 📚 相关知识:[[08-concurrency]](并发安全) [[10-cache-strategy]](缓存一致性)


场景:用户说"商品列表页加载要 5 秒" ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  运营后台,点击"商品列表"。                             │
│                                                             │
│  页面加载:5.2 秒                                        │
│                                                             │
│  监控显示:                                               │
│  接口耗时:5.1 秒                                        │
│  其中 MySQL 查询耗时:5.0 秒                             │
│                                                             │
│  EXPLAIN一看:                                            │
│  SELECT * FROM products WHERE status = 'active'           │
│    ORDER BY created_at DESC LIMIT 20 OFFSET 10000;        │
│                                                             │
│  → 全表扫描!30 万行全部扫描!                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这一章,我们从慢查询出发,理解 MySQL 的查询优化。


第1节:慢查询分析——先找到是谁 ​

开启慢查询日志 ​

sql
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 超过 1 秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 查看当前正在执行的查询
SHOW PROCESSLIST;
-- 或
SELECT * FROM information_schema.processlist WHERE command != 'Sleep';
1
2
3
4
5
6
7
8
9
10
11
12
13

EXPLAIN——查询执行计划 ​

sql
EXPLAIN SELECT
    u.id, u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
GROUP BY u.id
HAVING order_count > 5
ORDER BY order_count DESC
LIMIT 10;
1
2
3
4
5
6
7
8
9

EXPLAIN 输出解读 ​

┌─────────────────────────────────────────────────────────────┐
│                 EXPLAIN 字段解读                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  id: 执行顺序,id 越大越先执行                        │
│                                                             │
│  select_type: 查询类型                                  │
│  SIMPLE:简单查询                                       │
│  PRIMARY:最外层查询                                     │
│  DERIVED:子查询(FROM 里的子查询)                  │
│  SUBQUERY:独立子查询                                   │
│  UNION:UNION 操作                                      │
│                                                             │
│  type(重要!从好到坏):                            │
│  const > eq_ref > ref > range > index > ALL           │
│                                                             │
│  const:主键/唯一索引等值查询(最快)               │
│  eq_ref:JOIN 时,被驱动表用主键/唯一索引          │
│  ref:非唯一索引等值查询                             │
│  range:索引范围查询(> < BETWEEN)               │
│  index:全索引扫描(比 ALL 好,但仍需遍历索引)      │
│  ALL:全表扫描(最慢,尽量避免!)                 │
│                                                             │
│  key:实际使用的索引                                    │
│  key_len:索引长度,越短越好(节省空间)               │
│  rows:预计扫描行数,越少越好                         │
│  Extra:额外信息                                        │
│  Using filesort:文件排序(需要优化!)               │
│  Using temporary:使用临时表(需要优化!)           │
│  Using index:覆盖索引,无需回表                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

第2节:索引失效——为什么有索引却不用 ​

场景:商品列表页按状态查询,用了索引还是慢 ​

sql
-- 创建了索引
CREATE INDEX idx_status_created ON products(status, created_at);

-- 查询
SELECT * FROM products
WHERE status = 'active'
  AND created_at > '2026-01-01'
ORDER BY created_at DESC
LIMIT 20;
1
2
3
4
5
6
7
8
9

索引失效的常见场景 ​

┌─────────────────────────────────────────────────────────────┐
│              索引失效的 7 种场景                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 函数/计算:                                          │
│  WHERE YEAR(created_at) = 2026  ← 索引字段用了函数      │
│  ✅ WHERE created_at >= '2026-01-01'                   │
│                                                             │
│  2. 类型转换:                                            │
│  WHERE phone = 13800138000  ← phone 是 VARCHAR,          │
│                                 MySQL 自动 CAST           │
│  ✅ WHERE phone = '13800138000'                          │
│                                                             │
│  3. 最左前缀原则(复合索引):                          │
│  复合索引:(status, created_at)                        │
│  ❌ WHERE created_at > '2026-01-01'  ← 跳过 status      │
│  ✅ WHERE status = 'active'                              │
│  ✅ WHERE status = 'active' AND created_at > '...'      │
│  ❌ WHERE created_at > '...' AND status = 'active'      │
│  ⚠️  MySQL 8.0+ 可以优化器重排,但不保证顺序           │
│                                                             │
│  4. OR 连接:                                            │
│  ❌ WHERE status = 'active' OR stock = 0  ← OR 会导致索引失效 │
│  ✅ 拆成 UNION:WHERE status = 'active' UNION WHERE stock = 0 │
│                                                             │
│  5. LIKE 前缀匹配:                                      │
│  ❌ WHERE name LIKE '%手机'  ← 不能用 B+ 树           │
│  ✅ WHERE name LIKE '手机%'  ← 可以用前缀索引         │
│  ✅ 全文索引:WHERE MATCH(name) AGAINST('手机')        │
│                                                             │
│  6. IS NULL / IS NOT NULL:                            │
│  ⚠️ MySQL 5.7:IS NULL 可能走索引,IS NOT NULL 不一定│
│  ✅ 尽量给字段设默认值,避免 NULL                      │
│                                                             │
│  7. NOT IN / NOT EXISTS:                               │
│  ❌ WHERE status NOT IN ('deleted', 'archived')         │
│  ✅ WHERE status NOT IN ('deleted', 'archived')         │
│  ⚠️ 大数据量下慎用,改为 > 或 BETWEEN                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

第3节:分页优化——为什么越往后翻页越慢 ​

场景:商品列表第 10000 页 ​

sql
-- 第 1 页:快
SELECT * FROM products
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;
-- 耗时:0.01 秒

-- 第 10000 页:慢
SELECT * FROM products
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 20 OFFSET 200000;
-- 耗时:2.5 秒

-- 为什么慢?
-- OFFSET 200000:MySQL 先扫描前 200020 行,丢弃前 200000 行
-- 数据量越大,OFFSET 越大,越慢
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

方案一:延迟关联 ​

sql
-- 优化:先查 ID,再关联查询
SELECT p.*
FROM products p
INNER JOIN (
    SELECT id FROM products
    WHERE status = 'active'
    ORDER BY created_at DESC
    LIMIT 20 OFFSET 200000
) AS t ON p.id = t.id;
-- 优势:子查询只查主键 ID(不回表),快很多
1
2
3
4
5
6
7
8
9
10

方案二:游标分页(推荐) ​

sql
-- 上一页最后一条的 created_at = 2026-05-01 12:00:00, id = 12345

SELECT * FROM products
WHERE status = 'active'
  AND (created_at < '2026-05-01 12:00:00'
       OR (created_at = '2026-05-01 12:00:00' AND id < 12345))
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- 优势:
-- 1. 每页查询都走索引,耗时稳定
-- 2. 数据不新增时,不会有遗漏/重复
-- 3. 适合超深分页场景

-- 缺点:
-- 1. 不能跳页(只能上一页/下一页)
-- 2. 需要前端传递上一页的游标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

第4节:JOIN 优化——小表驱动大表 ​

问题:LEFT JOIN 的顺序影响性能 ​

sql
-- 错误写法:大表在前面
SELECT * FROM orders o
LEFT JOIN users u ON o.user_id = u.id  -- orders 是大表
WHERE u.city = '北京';
-- MySQL 先扫描 orders,再去 users 匹配 → 慢

-- 正确写法:小表在前面
SELECT * FROM users u
LEFT JOIN orders o ON o.user_id = u.id  -- users 是小表
WHERE u.city = '北京';
-- MySQL 先扫描 users,再用 id 找 orders → 快

-- 或者用 STRAIGHT_JOIN 强制顺序
SELECT STRAIGHT_JOIN *
FROM users u
JOIN orders o ON o.user_id = u.id;
-- 强制按照 SQL 写的顺序执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

多表 JOIN 的优化原则 ​

┌─────────────────────────────────────────────────────────────┐
│              多表 JOIN 优化原则                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 驱动表选择:                                         │
│  驱动表 = 先扫描的表(尽量选小表)                       │
│  驱动表的每行都要去被驱动表匹配                         │
│  → 驱动表越小,总匹配次数越少                            │
│                                                             │
│  2. 尽量在被驱动表的 JOIN 字段上建索引:               │
│  users.id 是主键(自带索引)                             │
│  orders.user_id 建了索引 → JOIN 时可以用索引             │
│                                                             │
│  3. 避免 SELECT *:                                       │
│  只查需要的字段                                            │
│  → 减少回表次数                                           │
│  → 减少网络传输量                                         │
│                                                             │
│  4. JOIN 字段类型要一致:                                │
│  ❌ WHERE o.user_id = u.phone  ← INT  JOIN VARCHAR       │
│  ✅ WHERE o.user_id = u.id  ← INT  JOIN INT             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

第5节:锁——并发下的数据安全 ​

场景:两个人同时买同一件商品 ​

sql
-- 线程 A:查库存
SELECT stock FROM products WHERE id = 1;  -- stock = 1
-- 线程 B:查库存
SELECT stock FROM products WHERE id = 1;  -- stock = 1

-- 线程 A:扣库存
UPDATE products SET stock = stock - 1 WHERE id = 1;  -- stock = 0
-- 线程 B:扣库存
UPDATE products SET stock = stock - 1 WHERE id = 1;  -- stock = -1,超卖了!
1
2
3
4
5
6
7
8
9

锁的分类 ​

┌─────────────────────────────────────────────────────────────┐
│                 MySQL 锁分类                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  按粒度:                                               │
│  • 表锁:锁整张表(MyISAM 默认)                        │
│  • 行锁:只锁一行(InnoDB 支持)                        │
│                                                             │
│  按类型:                                               │
│  • 共享锁(S):读锁,多个事务可以同时持有               │
│  • 排他锁(X):写锁,独占                               │
│                                                             │
│  InnoDB 行锁的四种类型:                                │
│  • Record Lock:只锁主键/唯一索引对应的记录             │
│  • Gap Lock:锁两个索引值之间的间隙(防止插入新数据)  │
│  • Next-Key Lock:Record Lock + Gap Lock(临键锁)    │
│  • Insert Intention Lock:插入新数据前的间隙锁          │
│                                                             │
│  隔离级别 vs 锁行为:                                   │
│  • READ UNCOMMITTED:不加锁(脏读)                    │
│  • READ COMMITTED:Record Lock(不可重复读)           │
│  • REPEATABLE READ(默认):Next-Key Lock(解决幻读)  │
│  • SERIALIZABLE:全表锁                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

悲观锁 vs 乐观锁 ​

sql
-- 悲观锁:先加锁再操作(适合写多的场景)
BEGIN;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;  -- 加排他锁
-- 其他事务的 SELECT FOR UPDATE 会阻塞
IF stock > 0 THEN
    UPDATE products SET stock = stock - 1 WHERE id = 1;
END IF;
COMMIT;
-- 问题:并发时,后面的事务要等前面的事务释放锁

-- 乐观锁:版本号控制(适合读多的场景)
-- 表设计:ALTER TABLE products ADD COLUMN version INT DEFAULT 0;
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1
  AND version = 5;  -- 只有版本号是 5 才能更新

-- 影响行数 = 0?说明其他事务先更新了,version 变了
-- 影响行数 = 1?说明更新成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

升华:MySQL 优化的顺序 ​

┌─────────────────────────────────────────────────────────────┐
│              MySQL 优化顺序(从高到低)                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 索引优化(效果最明显)                                │
│  → EXPLAIN 找全表扫描,加索引                           │
│                                                             │
│  2. SQL 改写                                             │
│  → 避免 SELECT *、减少 JOIN、改写分页                   │
│                                                             │
│  3. 表结构优化                                           │
│  → 字段类型、垂直拆分、冷热分离                       │
│                                                             │
│  4. 架构优化                                             │
│  → 读写分离、分库分表、引入缓存                       │
│                                                             │
│  一句话:先问自己"索引建对了吗",再想"架构要不要改"。  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

"AI 可查 vs 必须理解"清单 ​

AI 可查:
✅ EXPLAIN 字段的每个值是什么意思
✅ 各种 SQL 函数对索引的影响
✅ 具体的 JOIN 语法优化

必须理解:
🔴 type 字段从好到坏的排序(const > ... > ALL)
🔴 最左前缀原则和 OR 导致索引失效的原因
🔴 OFFSET 分页慢的原因和游标分页的原理
🔴 乐观锁 vs 悲观锁的适用场景
🔴 RR 隔离级别下 Next-Key Lock 如何解决幻读
1
2
3
4
5
6
7
8
9
10
11

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇3. 并发编程——为什么你的库存总是扣成负数 / Concurrency Control and Inventory Consistency
下一篇5. 架构模式——什么时候该用 CQRS / Architecture Patterns and When to Use CQRS

持续记录,持续成长

Copyright © Tidenflow