vault backup: 2026-05-21 12:20:54
This commit is contained in:
@@ -12,6 +12,17 @@ create time: 2026-05-16 00:00
|
||||
> [!QUESTION] 为什么不能直接靠"写了索引就一定能用到"?
|
||||
> 因为 MySQL 使用的是 **Cost-Based Optimizer (CBO)**:优化器会根据统计信息(行数、页大小、聚簇因子等)自行决定最优执行路径。有时优化器认为全表扫描比走索引更快(比如要查整张表 80% 的数据),这时即使有索引也不会使用。`EXPLAIN` 就是用来观察和优化器"想法"是否一致的镜子。
|
||||
|
||||
> [!TIP] EXPLAIN 输出的阅读顺序
|
||||
> 面对一屏 EXPLAIN 输出,可以按"从大到小"的优先级快速判断:
|
||||
> 1. **type** — 最快判断索引使用情况(ALL = 🔴 全表扫描)
|
||||
> 2. **key** — 确认实际命中的索引名
|
||||
> 3. **rows** — 预估扫描行数,决定是否需要优化
|
||||
> 4. **Extra** — 有没有 filesort / temporary 等额外开销
|
||||
> 5. **key_len** — 联合索引用了几列?只用了前缀?
|
||||
> 6. **ref** — 索引在跟什么做比较?
|
||||
>
|
||||
> 先扫 type + key 就能判断"这 SQL 有没有大问题",再看 rows + Extra 判断"问题有多严重"。
|
||||
|
||||
## 基本用法
|
||||
|
||||
```sql
|
||||
@@ -27,15 +38,65 @@ EXPLAIN FORMAT=JSON SELECT * FROM users WHERE email = 'a@x.com';
|
||||
EXPLAIN ANALYZE SELECT ...; -- MySQL 8.0.18+
|
||||
```
|
||||
|
||||
## 关键字段速查
|
||||
## 输出字段全览
|
||||
|
||||
| 字段 | 含义 | 关注点 |
|
||||
|------|------|--------|
|
||||
| **type** | JOIN 类型 | 是否走索引(ref/range 优,ALL 差)|
|
||||
| **key** | 实际使用的索引 | NULL = 没用索引 |
|
||||
| **rows** | 预估扫描行数 | 越小越好 |
|
||||
| **Extra** | 附加信息 | 重点关注 Using where/index/filesort/temporary |
|
||||
| **cost** | 预估成本(JSON 格式) | optimizer_cost 越低越好 |
|
||||
> [!NOTE] 核心思路
|
||||
> EXPLAIN 输出的每一行对应一张表的访问方式。多表 JOIN 时会输出多行,**id 相同时,行号越小(越靠上)越先执行**——第一行是驱动表,后续行是被驱动表。
|
||||
|
||||
| 字段 | 含义 | 重要程度 | 关注点 |
|
||||
|------|------|:--------:|--------|
|
||||
| **id** | 查询编号 | ⚪ 了解 | 相同 id = 同层查询,不同 id = 嵌套子查询 |
|
||||
| **select_type** | 查询类型 | 🟡 辅助 | SIMPLE/PRIMARY/SUBQUERY/DERIVED,判断复杂度 |
|
||||
| **table** | 访问的表 | ⚪ 了解 | 可能是别名或 `<derivedN>` 物化表 |
|
||||
| **type** | 访问类型 | 🔴 必看 | **索引使用的第一指标**(const > ref > range > ALL) |
|
||||
| **possible_keys** | 可能用到的索引 | 🟡 辅助 | NULL = 没有候选索引,需创建 |
|
||||
| **key** | 实际使用的索引 | 🔴 必看 | NULL = 优化器放弃索引,选了全表扫描 |
|
||||
| **key_len** | 索引使用字节数 | 🟡 辅助 | 判断联合索引是否被充分利用 |
|
||||
| **ref** | 索引比较对象 | 🟡 辅助 | const/列名/func,理解 JOIN 驱动关系 |
|
||||
| **rows** | 预估扫描行数 | 🔴 必看 | 越小越好,`rows × filtered%` ≈ 进入下一步的行数 |
|
||||
| **filtered** | WHERE 过滤率 (%) | 🟡 辅助 | 100% = 索引完全满足条件,越低说明回表后还要过滤 |
|
||||
| **Extra** | 附加信息 | 🔴 必看 | filesort/temporary = 🔴;Using index = 🟢 覆盖索引 |
|
||||
|
||||
## select_type:查询层级识别
|
||||
|
||||
`select_type` 告诉你这一行代表的是什么级别的查询——最外层、子查询、还是 UNION?
|
||||
|
||||
| select_type | 含义 | 典型场景 |
|
||||
|-------------|------|----------|
|
||||
| **SIMPLE** | 简单查询(无子查询/UNION) | `SELECT * FROM t WHERE id = 1` |
|
||||
| **PRIMARY** | 最外层查询 | 包含子查询时,外层标记为 PRIMARY |
|
||||
| **SUBQUERY** | SELECT 列表或 WHERE 中的子查询 | `WHERE id IN (SELECT ...)` |
|
||||
| **DEPENDENT SUBQUERY** | 依赖外层结果的关联子查询 | `WHERE EXISTS (SELECT ... FROM t2 WHERE t2.id = t1.id)` |
|
||||
| **DERIVED** | FROM 子句中的派生表 | `FROM (SELECT ...) AS tmp` |
|
||||
| **UNION** | UNION 中第二个及之后的 SELECT | `SELECT ... UNION SELECT ...` |
|
||||
| **MATERIALIZED** | 物化子查询(8.0+) | 优化器将子查询结果存入临时表 |
|
||||
|
||||
```sql
|
||||
-- 一个包含子查询的查询
|
||||
EXPLAIN
|
||||
SELECT u.username, order_count
|
||||
FROM users u
|
||||
JOIN (
|
||||
SELECT user_id, COUNT(*) AS order_count
|
||||
FROM orders
|
||||
GROUP BY user_id
|
||||
) AS oc ON u.id = oc.user_id
|
||||
WHERE u.id IN (SELECT user_id FROM vip_users);
|
||||
```
|
||||
|
||||
```
|
||||
+----+-------------+------------+--------+---------------+---------+---------+------+------+-------+
|
||||
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|
||||
+----+-------------+------------+--------+---------------+---------+---------+------+------+-------+
|
||||
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 1000 | | ← FROM 子句的派生表
|
||||
| 1 | PRIMARY | u | eq_ref | PRIMARY | PRIMARY | 4 | oc.user_id | 1 | |
|
||||
| 2 | DERIVED | orders | index | idx_user_id | idx_user_id | 4 | NULL | 100K | Using index |
|
||||
| 3 | SUBQUERY | vip_users | ALL | NULL | NULL | NULL | NULL | 500 | |
|
||||
+----+-------------+------------+--------+---------------+---------+---------+------+------+-------+
|
||||
```
|
||||
|
||||
> [!QUESTION] 为什么 DERIVED 表的 rows 是 1000?
|
||||
> 因为派生表 `oc` 是先物化到临时表的,MySQL 对物化表的行数统计通常不精确。这也是为什么**尽量避免在大表上使用 FROM 子查询**——物化过程本身就有开销。MySQL 8.0 的 Merged Derived Table 优化可以将简单的派生表"合并"回外层查询,减少物化。
|
||||
|
||||
## type 详解(最重要)
|
||||
|
||||
@@ -72,6 +133,90 @@ flowchart LR
|
||||
> [!WARNING] 不要盲目追求 ref 以上级别
|
||||
> `range` 在某些场景(如范围不大)完全可以接受。关键是看 `rows` 字段和 `Extra` 的组合。
|
||||
|
||||
## key_len:索引使用了多少?
|
||||
|
||||
`key_len` 表示 MySQL 在索引中实际使用的**字节数**。对于联合索引,它是判断"索引被用了几列"的最直接证据。
|
||||
|
||||
> [!QUESTION] 联合索引 `idx_a_b_c (a, b, c)`,EXPLAIN 显示 key_len = 8,用了几列?
|
||||
> 需要知道各列的数据类型才能算出来。key_len 的计算规则如下:
|
||||
|
||||
### 计算规则
|
||||
|
||||
| 类型 | 字节数 | 说明 |
|
||||
|------|--------|------|
|
||||
| `INT` | 4 | 固定 4 字节 |
|
||||
| `BIGINT` | 8 | 固定 8 字节 |
|
||||
| `CHAR(n)` | n × 字符集字节数 | utf8mb4 = 每字符 4 字节 |
|
||||
| `VARCHAR(n)` | n × 字符集字节数 + **2** | 额外 2 字节存长度 |
|
||||
| `DATE` | 3 | 固定 3 字节 |
|
||||
| `TIMESTAMP/DATETIME` | 5/8 | 固定长度 |
|
||||
| **NULLABLE 列** | +1 | 所有类型额外 +1 字节标记 NULL |
|
||||
|
||||
> [!TIP] 速算公式
|
||||
> `key_len = 各列字节数之和`,其中每列 = **类型基础长度 + (VARCHAR 额外 2) + (可 NULL 额外 1)**
|
||||
>
|
||||
> 反过来看:如果 key_len 比你预期的短,说明联合索引**只用到了前缀几列**。
|
||||
|
||||
### 实战计算
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL, -- utf8mb4
|
||||
age INT,
|
||||
email VARCHAR(128) DEFAULT NULL,
|
||||
INDEX idx_name_age_email (name, age, email)
|
||||
);
|
||||
```
|
||||
|
||||
| 使用方式 | key_len 计算 | 说明 |
|
||||
|----------|-------------|------|
|
||||
| `WHERE name = 'Tom'` | 64 × 4 + 2 = **258** | VARCHAR(64) utf8mb4,NOT NULL 无额外字节 |
|
||||
| `WHERE name = 'Tom' AND age = 20` | 258 + 4 = **262** | age 是 INT,NOT NULL |
|
||||
| `WHERE name = 'Tom' AND age = 20 AND email = 'a@b.c'` | 262 + 128 × 4 + 2 + 1 = **777** | email 可 NULL,额外 +1 |
|
||||
|
||||
```sql
|
||||
EXPLAIN SELECT * FROM users WHERE name = 'Tom' AND age = 20;
|
||||
-- key: idx_name_age_email, key_len: 262
|
||||
-- → 说明索引只用到了前 2 列(name + age),没用到 email
|
||||
```
|
||||
|
||||
> [!NOTE] 核心用法
|
||||
> 看到 `key_len` 后,对照表结构算一下"如果用满所有列应该是多少字节"。差得远 = 索引没充分利用,可能需要调整查询条件或索引顺序。
|
||||
|
||||
## ref 字段:索引在跟谁比?
|
||||
|
||||
`ref` 显示索引的查找值来自哪里——是常量、函数还是另一张表的列。
|
||||
|
||||
| ref 值 | 含义 | 示例 |
|
||||
|--------|------|------|
|
||||
| **const** | 与常量比较 | `WHERE id = 1` |
|
||||
| **func** | 与函数结果比较 | `WHERE created_at = NOW()` |
|
||||
| **db.table.column** | 与另一张表的列比较(JOIN) | `ON u.id = o.user_id` → ref 显示 `o.user_id` |
|
||||
| **NULL** | 无法使用等值比较 | range 扫描(如 `WHERE id > 100`)|
|
||||
|
||||
```sql
|
||||
EXPLAIN SELECT u.username, o.amount
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
WHERE o.status = 'pending';
|
||||
```
|
||||
|
||||
```
|
||||
+----+-------------+-------+------+---------------+--------------+---------+------------------+------+-------+
|
||||
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|
||||
+----+-------------+-------+------+---------------+--------------+---------+------------------+------+-------+
|
||||
| 1 | SIMPLE | o | ref | idx_status | idx_status | 130 | const | 5000 | Using where |
|
||||
| 1 | SIMPLE | u | eq_ref | PRIMARY | PRIMARY | 8 | test.o.user_id | 1 | |
|
||||
+----+-------------+-------+------+---------------+--------------+---------+------------------+------+-------+
|
||||
```
|
||||
|
||||
- 第一行:`o` 表用 `idx_status` 索引,ref = `const`(`status = 'pending'` 是常量)
|
||||
- 第二行:`u` 表用主键,ref = `test.o.user_id`(被驱动表的列来自 `o.user_id`)
|
||||
|
||||
> [!TIP] ref 帮你理解 JOIN 驱动关系
|
||||
> 当 ref 显示另一张表的列名时,说明当前表是**被驱动表**。驱动表的行数(rows)越小,被驱动表的回表次数就越少——这就是为什么**小表驱动大表**的原则。
|
||||
|
||||
## Extra 关键字解读
|
||||
|
||||
> [!SUCCESS] Using index — 覆盖索引(Index Only Scan)
|
||||
@@ -85,6 +230,35 @@ flowchart LR
|
||||
> [!TIP] Using index condition — 索引下推(ICP)
|
||||
> MySQL 5.6+ 引入,在存储引擎层预过滤,减少回表次数 ✅
|
||||
|
||||
#### ICP 深入理解:下推 vs 不下推
|
||||
|
||||
```sql
|
||||
-- 假设有联合索引 idx_status_city (status, city)
|
||||
EXPLAIN SELECT * FROM users
|
||||
WHERE status = 'active' AND city LIKE '北京%';
|
||||
```
|
||||
|
||||
**Without ICP(MySQL 5.6 之前)**:存储引擎只用 `status` 过滤,拿到所有 active 用户的主键 → 逐行回表 → Server 层再过滤 `city LIKE '北京%'`。假设 active 用户有 10 万,就要回表 10 万次。
|
||||
|
||||
**With ICP(MySQL 5.6+)**:存储引擎在索引层**直接过滤** `city LIKE '北京%'`,只把满足两个条件的行回表。可能只回表 2000 次。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["存储引擎层<br/>idx_status_city"] --> B{"ICP 可用?"}
|
||||
|
||||
B -->|No ICP| C["只用 status 定位<br/>回表 100K 行"]
|
||||
C --> D["Server 层过滤 city<br/>丢弃 98K 行 ⚠️"]
|
||||
|
||||
B -->|With ICP| E["status 定位 + city 预过滤<br/>索引层过滤后只剩 2K 行"]
|
||||
E --> F["只回表 2K 行 ✅"]
|
||||
|
||||
style D fill:#EE5A24,color:#fff
|
||||
style F fill:#00D866,color:#fff
|
||||
```
|
||||
|
||||
> [!NOTE] ICP 的适用条件
|
||||
> ICP 只在**联合索引**且 WHERE 条件包含**索引列但不满足最左前缀**时才有意义。比如 `idx(a, b)`,`WHERE a > 1 AND b = 2` 中 `b = 2` 就是 ICP 下推的候选条件。如果只查 `WHERE a = 1`,ICP 无从发挥。
|
||||
|
||||
### 其他 Extra 信息速查
|
||||
|
||||
| 关键词 | 含义 | 处理 |
|
||||
@@ -151,37 +325,115 @@ ORDER BY created_at DESC;
|
||||
|
||||
## 实战案例分析
|
||||
|
||||
下面用一个完整的调优案例,把前面所有知识点串起来。
|
||||
|
||||
### 场景:订单列表页越来越慢
|
||||
|
||||
```sql
|
||||
-- 原始查询(慢)
|
||||
EXPLAIN SELECT u.username, o.amount
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
WHERE o.created_at >= '2026-01-01'
|
||||
ORDER BY o.created_at DESC
|
||||
-- 表结构
|
||||
CREATE TABLE orders (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
amount DECIMAL(10,2),
|
||||
created_at DATETIME NOT NULL,
|
||||
INDEX idx_user_id (user_id)
|
||||
) ENGINE=InnoDB;
|
||||
-- 表中有 100 万行数据
|
||||
|
||||
-- 慢查询:某用户最近的已支付订单
|
||||
EXPLAIN SELECT *
|
||||
FROM orders
|
||||
WHERE user_id = 42
|
||||
AND status = 'paid'
|
||||
AND created_at >= '2026-01-01'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
-- 典型 bad result:
|
||||
-- type: ALL, rows: 1000000, Extra: Using where; Using filesort; Using temporary
|
||||
-- → 全表扫描 + 临时表 + 文件排序
|
||||
### Step 1:看 type + key — 全表扫描?
|
||||
|
||||
-- 修复方案
|
||||
-- 1. 创建复合索引
|
||||
ALTER TABLE orders ADD INDEX idx_created_user (created_at, user_id);
|
||||
```
|
||||
+----+-------------+--------+------+---------------+------------+---------+-------+--------+---------------------------+
|
||||
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|
||||
+----+-------------+--------+------+---------------+------------+---------+-------+--------+---------------------------+
|
||||
| 1 | SIMPLE | orders | ref | idx_user_id | idx_user_id| 8 | const | 98000 | Using where; Using filesort |
|
||||
+----+-------------+--------+------+---------------+------------+---------+-------+--------+---------------------------+
|
||||
```
|
||||
|
||||
-- 2. 查询改写(反向排序优化)
|
||||
-- 先用索引定位 top 20 的 pk,再 JOIN 拿数据
|
||||
SELECT u.username, o.amount
|
||||
FROM orders o
|
||||
JOIN users u ON u.id = o.user_id
|
||||
WHERE o.created_at >= '2026-01-01'
|
||||
ORDER BY o.created_at DESC
|
||||
- type = ref(走了索引 ✅),但 rows = 98000(user_id=42 有 9.8 万条订单,太多)
|
||||
- Extra = `Using where; Using filesort` 🔴 — 回表后还要过滤 status + created_at,再排序
|
||||
|
||||
> [!QUESTION] 问题出在哪?
|
||||
> `idx_user_id` 是单列索引,只帮你定位 user_id。`status` 和 `created_at` 的过滤都在回表后才做,9.8 万次回表 + filesort = 慢。
|
||||
|
||||
### Step 2:创建联合索引 — 关注 key_len
|
||||
|
||||
```sql
|
||||
ALTER TABLE orders ADD INDEX idx_uid_status_created (user_id, status, created_at);
|
||||
```
|
||||
|
||||
```sql
|
||||
EXPLAIN SELECT *
|
||||
FROM orders
|
||||
WHERE user_id = 42
|
||||
AND status = 'paid'
|
||||
AND created_at >= '2026-01-01'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
-- 新的 EXPLAIN:
|
||||
-- type: range on orders, ref on users
|
||||
-- key: idx_created_user
|
||||
-- Extra: Using index condition; Using where
|
||||
-- → 索引范围扫描 + 快速消除 filesort
|
||||
```
|
||||
+----+-------------+--------+-------+---------------------------------------------+-------------------------+---------+------+------+----+
|
||||
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|
||||
+----+-------------+--------+-------+---------------------------------------------+-------------------------+---------+------+------+----+
|
||||
| 1 | SIMPLE | orders | range | idx_user_id, idx_uid_status_created | idx_uid_status_created | 97 | NULL | 1200 | Using where |
|
||||
+----+-------------+--------+-------+---------------------------------------------+-------------------------+---------+------+------+----+
|
||||
```
|
||||
|
||||
**逐字段分析**:
|
||||
|
||||
| 字段 | 变化 | 说明 |
|
||||
|------|------|------|
|
||||
| **type** | ref → range | 用 created_at 范围扫描,精确了 |
|
||||
| **key** | idx_user_id → idx_uid_status_created | 用上了新索引 ✅ |
|
||||
| **key_len** | 8 → 97 | 8 = 只用了 user_id(BIGINT);97 = user_id(8) + status(VARCHAR(20)×4+2+1=83) + created_at(DATETIME=5+1=6) → **三列全用上** ✅ |
|
||||
| **rows** | 98000 → 1200 | 索引直接过滤到 1200 行,减少了 **98.8%** |
|
||||
| **Extra** | Using filesort 消失 ✅ | created_at 在索引末尾,DESC 排序直接倒序扫描 |
|
||||
|
||||
> [!NOTE] 关键决策点
|
||||
> 这里 ORDER BY created_at DESC 恰好可以用到索引的倒序扫描(InnoDB 支持 Reverse Index Scan),所以 **filesort 消失了**。如果 ORDER BY 的列不在索引中,filesort 仍会出现。
|
||||
|
||||
### Step 3:EXPLAIN ANALYZE 确认
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT *
|
||||
FROM orders
|
||||
WHERE user_id = 42
|
||||
AND status = 'paid'
|
||||
AND created_at >= '2026-01-01'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
```
|
||||
-> Limit: 20 row(s)
|
||||
-> Index range scan on orders using idx_uid_status_created
|
||||
(actual time=0.089..0.142 rows=20 loops=1)
|
||||
```
|
||||
|
||||
- **rows=20**:只扫了 20 行就满足 LIMIT,配合索引的有序性,几乎零开销
|
||||
- **actual time < 0.2ms**:从秒级优化到了亚毫秒级 🚀
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Step 1<br/>rows: 98K<br/>filesort 🔴"] --> B["Step 2<br/>rows: 1.2K<br/>无 filesort ✅"]
|
||||
B --> C["Step 3<br/>rows: 20<br/>0.14ms 🚀"]
|
||||
|
||||
style A fill:#EE5A24,color:#fff
|
||||
style B fill:#FF9F43,color:#000
|
||||
style C fill:#00D866,color:#fff
|
||||
```
|
||||
|
||||
## EXPLAIN FORMAT=JSON 精读
|
||||
@@ -244,6 +496,39 @@ flowchart LR
|
||||
style D fill:#C44569,color:#fff
|
||||
```
|
||||
|
||||
## FORMAT=TREE:现代可读格式
|
||||
|
||||
MySQL 8.0.16+ 引入了 `FORMAT=TREE`,输出**缩进树状结构**,比传统表格和 JSON 都更直观。
|
||||
|
||||
```sql
|
||||
EXPLAIN FORMAT=TREE
|
||||
SELECT u.username, o.amount
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
WHERE o.created_at >= '2026-01-01'
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
```
|
||||
-> Limit: 20 row(s)
|
||||
-> Nested loop inner join (cost=530 rows=20)
|
||||
-> Index range scan on o using idx_created_user (cost=530 rows=5000)
|
||||
-> Single-row index lookup on u using PRIMARY (id=o.user_id) (cost=0.25 rows=1)
|
||||
```
|
||||
|
||||
### 三种格式怎么选?
|
||||
|
||||
| 格式 | 适用场景 | 优点 | 缺点 |
|
||||
|------|----------|------|------|
|
||||
| **默认表格** | 快速筛查 | 一行一表,一目了然 | 缺少 cost、缺少嵌套信息 |
|
||||
| **FORMAT=JSON** | 精细分析 | 信息最全,可编程解析 | 冗长,层级关系不直观 |
|
||||
| **FORMAT=TREE** | 日常推荐 | **层级清晰 + 含 cost + 易读** | 无法看到 possible_keys |
|
||||
| **ANALYZE** | 深度诊断 | 含真实执行时间 | 会实际执行 SQL |
|
||||
|
||||
> [!TIP] 推荐工作流
|
||||
> 日常排查先用 `FORMAT=TREE` 快速定位瓶颈(哪一步 cost 最高),需要细节时切到 `FORMAT=JSON` 看 key_len、possible_keys 等。
|
||||
|
||||
## EXPLAIN ANALYZE:看到真实执行代价
|
||||
|
||||
`EXPLAIN ANALYZE` 是 MySQL 8.0.18+ 引入的功能——它会**真正执行一次 SQL**,然后返回实际运行时间、每行的实际扫描数等统计信息。
|
||||
@@ -321,8 +606,28 @@ ANALYZE TABLE orders;
|
||||
SET GLOBAL innodb_stats_auto_recalc = ON;
|
||||
```
|
||||
|
||||
## 核心记忆点
|
||||
|
||||
> [!SUMMARY] EXPLAIN 六字口诀:型键行推排临
|
||||
> 1. **型**(type):判断索引使用等级,ALL 是底线问题
|
||||
> 2. **键**(key):确认命中哪个索引,NULL 需要警觉
|
||||
> 3. **行**(rows):预估扫描行数,rows × filtered 决定实际负载
|
||||
> 4. **推**(ICP):Using index condition = 存储引擎层预过滤,减少回表
|
||||
> 5. **排**(filesort):ORDER BY 无法利用索引排序时触发
|
||||
> 6. **临**(temporary):GROUP BY / DISTINCT 产生临时表
|
||||
|
||||
> [!CHECKLIST] EXPLAIN 审查清单
|
||||
> - [ ] `type` 不是 ALL(或 rows 可接受)
|
||||
> - [ ] `key` 不是 NULL(有索引可用)
|
||||
> - [ ] `key_len` 覆盖了联合索引的关键列
|
||||
> - [ ] `Extra` 没有 filesort(或 ORDER BY 有索引支撑)
|
||||
> - [ ] `Extra` 没有 temporary(或 GROUP BY 走了索引)
|
||||
> - [ ] `rows` × `filtered%` 在可接受范围
|
||||
> - [ ] 预估 rows 与实际偏差不大(必要时 `ANALYZE TABLE`)
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hhs/MySQL/03-索引与查询优化/12-B+Tree 索引原理]] — type=ALL vs type=range 的底层差异
|
||||
- [[hhs/MySQL/03-索引与查询优化/16-慢查询日志分析]] — 如何用 pt-query-digest 配合 EXPLAIN
|
||||
- [[hhs/MySQL/03-索引与查询优化/14-联合索引与最左前缀]] — 索引设计不当导致的 EXPLAIN 问题
|
||||
- [[hhs/MySQL/03-索引与查询优化/13-聚簇索引与二级索引]] — 回表原理与 Extra 中 Using index 的底层原因
|
||||
|
||||
Reference in New Issue
Block a user