Update MySQL.md

补充 type=index 的回表、filesort 的排序缓冲、行构造器游标分页的执行计划与附录实测,调整索引选择比例的表述。
This commit is contained in:
CyC2018
2026-10-31 00:23:18 +08:00
parent cb78efbc7d
commit c6a820fba5
+14 -3
View File
@@ -307,7 +307,7 @@ WHERE user_id = 5 AND status > 0 AND create_time > '2025-01-05' -- key_len = 5
另外,范围条件带等号(>=、<=、BETWEEN)时,后面的列还能参与确定扫描的起点和终点。例如 user_id BETWEEN 1001 AND 1006 AND status = 2 的 key_len 为 5,关闭索引下推后读了 100 个索引项;改为 user_id > 1000 AND user_id < 1007 后 key_len 为 4,要读 120 个(见[附录 A6](#a6-联合索引的扫描范围))。起点和终点之间的索引项仍然要逐条判断 status。
需要注意的是,是否使用索引是优化器按成本估算决定的。走二级索引的每一行都要回表,匹配的行越多回表越多,超过一定比例后全表扫描反而更快:`create_time > '2025-01-23'` 匹配 9.9 万行,走 idx_create_time;`create_time > '2025-01-02'` 匹配 191 万行,就选择了全表扫描。IN 也一样,`user_id IN` 5 个值、1 万个值时都走索引,2 万个值时就改为全表扫描。因此同一种写法有时走索引有时不走,可能是因为匹配的数据量不同,也可能是统计信息不准(见[分析原因](#2-分析原因))。
需要注意的是,是否使用索引是优化器按成本估算决定的。走二级索引的每一行都要回表,匹配的行越多回表越多,超过一定比例后全表扫描反而更快。这个比例并不固定(早期版本按 30% 判断,现在由代价估算决定):`create_time > '2025-01-23'` 匹配 9.9 万行,走 idx_create_time;`create_time > '2025-01-02'` 匹配 191 万行,就选择了全表扫描。IN 也一样,`user_id IN` 5 个值、1 万个值时都走索引,2 万个值时就改为全表扫描。因此同一种写法有时走索引有时不走,可能是因为匹配的数据量不同,也可能是统计信息不准(见[分析原因](#2-分析原因))。
## 二、查询性能优化 🔥🔥
@@ -331,7 +331,7 @@ Explain 用来查看优化器选定的执行计划,语句本身并不执行,
- id 和 table:每张表一行,有子查询、UNION 时 id 不同;连接查询中 id 相同的几行按 MySQL 读取的顺序排列,上面的是驱动表;
- select_type:查询类型,有简单查询、联合查询、子查询等;
- type:访问类型,从好到差常见的有 system、const、eq_ref、ref、range、index、ALL。system 表示表中只有一行(InnoDB 表上一般见不到),const 表示通过主键或唯一索引最多匹配一行,eq_ref 表示连接时用被驱动表的主键或唯一索引匹配,ref 表示通过普通索引等值匹配,range 表示索引范围扫描,index 表示扫描整个索引树,ALL 表示全表扫描;
- type:访问类型,从好到差常见的有 system、const、eq_ref、ref、range、index、ALL。system 表示表中只有一行(InnoDB 表上一般见不到),const 表示通过主键或唯一索引最多匹配一行,eq_ref 表示连接时用被驱动表的主键或唯一索引匹配,ref 表示通过普通索引等值匹配,range 表示索引范围扫描,index 表示按索引顺序扫描整个索引树(扫描的是二级索引、Extra 中又没有 Using index 时,每一行还要回表,可能比 ALL 更慢),ALL 表示全表扫描;
- possible_keys 和 key:可以使用的索引和实际使用的索引。possible_keys 中有索引而 key 为 NULL,说明优化器认为走索引的代价更高;
- key_len:使用的索引长度,联合索引可以据此算出用到了前几列,见 [索引失效](#索引失效)。可以为 NULL 的列多 1 字节,变长的列多 2 字节,例如可以为 NULL 的 utf8mb4 列 order_no VARCHAR(32),key_len 为 32 × 4 + 2 + 1 = 131;
- ref:与索引比较的是常量(const)还是其它表的列;
@@ -401,7 +401,7 @@ Count: 2 Time=0.70s (1s) Lock=0.00s (0s) Rows=0.5 (1), root[root]@localhost
- 没有走索引,或者只用到了联合索引的一部分:见 [索引失效](#索引失效);
- 走了索引但回表太多:改为覆盖索引,或者用 [深分页](#深分页) 中的延迟关联;
- 出现 Using filesort、Using temporary:让排序和分组的列利用索引的顺序,见 [索引列的顺序](#3-索引列的顺序);
- 出现 Using filesort、Using temporary:让排序和分组的列利用索引的顺序,见 [索引列的顺序](#3-索引列的顺序)。filesort 在排序缓冲中排序,缓冲最大为 sort_buffer_size(默认 256 KB),放不下时分块排序后写入临时文件再归并,所以先让索引满足排序,再考虑调大这个参数;
- 连接查询中被驱动表的连接列没有索引:外层的每一行都要扫描一遍被驱动表,应在连接列上建索引;MySQL 8.0.18 起这种情况改用哈希连接,但仍要读完整张被驱动表;
- Lock_time 大,或者同一条语句的耗时忽长忽短:通常是在等待行锁或元数据锁,见 [MySQL 中的锁](数据库系统原理.md#mysql-中的锁);
- 统计信息不准导致选错了索引:执行 ANALYZE TABLE 更新统计信息,必要时用 FORCE INDEX 指定索引;
@@ -499,6 +499,8 @@ LIMIT 10;
排序列的值可能重复,所以要再按主键排序,并把主键也作为游标的一部分,否则时间相同的行会被跳过或者重复返回。游标分页的代价是不能直接跳到任意一页,只能逐页翻,适合 App 下拉加载、按时间浏览记录这类场景。必须跳页的场景用延迟关联,并限制翻页的深度,例如只允许翻到前 100 页。
需要注意的是,条件写成行构造器 (create_time, id) > ('2025-01-12 13:46:39', 982321) 时结果相同,但 MySQL 8.0 不对它做范围扫描:type 为 index,从索引开头逐行回表,读了 100 万行才找到这 10 行,耗时约 7 s(见[附录 A5](#a5-深分页的耗时))。
三种写法返回的 10 行相同,读取的数据和耗时如下(MySQL 8.0,数据全部在内存中,取 5 次中最快的一次,见[附录 A5](#a5-深分页的耗时)),作为对比,LIMIT 0, 10 只需 0.05 ms:
<div align="center"> <img src="pics/deep-pagination.png" width="720px"/> </div><br>
@@ -938,6 +940,15 @@ COUNT(*) 130.38 ms
按 (create_time, id) 排序后第 1000000 行为 id = 982321、create_time = '2025-01-12 13:46:39',这就是游标分页示例中使用的游标。
游标条件改用行构造器时(MySQL 8.0.44,执行 5 次)返回的 10 个 id 相同,执行计划与耗时如下:
```text
WHERE (create_time, id) > ('2025-01-12 13:46:39', 982321) 7156 ms
EXPLAIN: type=index, key=idx_create_time, Extra=Using where
EXPLAIN ANALYZE: Index scan on orders using idx_create_time (actual rows=1e+6)
OR 写法: type=range, Index range scan on orders using idx_create_time (actual rows=10)
```
### A6 联合索引的扫描范围
orders 表中 user_id 有 10 万个不同的值,每个用户 20 行,同一个用户的订单 status 相同(user_id % 4)。