介绍:
以下所有的介绍都是按照InnoDB存储引擎进行介绍的。
为了减少磁盘随机读取次数,InnoDB 采用页而不是行的粒度来保存数据,即数据被分成若干页,以页为单位保存在磁盘中。InnoDB 的页大小,一般是 16KB。
各个数据页组成一个双向链表,每个数据页中的记录按照主键顺序组成单向链表;每一个数据页中有一个页目录,方便按照主键查询记录。
页目录通过槽把记录分成不同的小组,每个小组有若干条记录。如图所示,记录中最前面的小方块中的数字,代表的是当前分组的记录条数,最小和最大的槽指向 2 个特殊的伪记录。
有了槽之后,我们按照主键搜索页中记录时,就可以采用二分法快速搜索,无需从最小记录开始遍历整个页中的记录链表。
举一个例子,如果要搜索主键(PK)=15 的记录:
先二分得出槽中间位是 (0+6)/2=3,看到其指向的记录是 12<15,所以需要从 #3 槽后继续搜索记录;
再使用二分搜索出 #3 槽和 #6 槽的中间位是 (3+6)/2=4.5 取整 4,#4 槽对应的记录是 16>15,所以记录一定在 #4 槽中;
再从 #3 槽指向的 12 号记录开始向下搜索 3 次,定位到 15 号记录。
CREATE TABLE `person` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`score` int(11) NOT NULL,
`create_time` timestamp NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
KEY `name_score` (`name`,`score`) USING BTREE,
KEY `create_time` (`create_time`) USING BTREE
数据库基于成本决定是否走索引
MySQL 在查询数据之前,会先对可能的方案做执行计划,然后依据成本决定走哪个执行计划。这里的成本,包括 IO 成本和 CPU 成本:
IO 成本,是从磁盘把数据加载到内存的成本。默认情况下,读取数据页的 IO 成本常数是 1(也就是读取 1 个页成本是 1)。
CPU 成本,是检测数据是否满足条件和排序等 CPU 操作的成本。默认情况下,检测记录的成本是 0.2。
分析一下 全表扫描的开销:
首先全表扫描只需要扫描聚集索引就可以 ,所以主要是以下两个因子影响开销:
聚簇索引占用的页面数,用来计算读取数据的 IO 成本;
表中的记录数,用来计算搜索的 CPU 成本。
SHOW TABLE STATUS LIKE 'person'
总行数是 100086 行,我们根据这个值估算 CPU 成本,是 100086*0.2=20017 左右。数据长度是 4734976 字节。
对于 InnoDB 来说,这就是聚簇索引占用的空间,等于聚簇索引的页面数量 * 每个页面的大小。InnoDB 每个页面的大小是 16KB,大概计算出页面数量是 289,
因此 IO 成本是 289 左右。所以,全表扫描的总成本是 20306 左右。
不是万能的索引
EXPLAIN SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-01-24 05:00:00'
可以看到明明有索引却没有走索引
只要把 create_time 条件中的 5 点改为 6 点就变为走索引了,并且走的是 create_time 索引而不是 name_score 联合索引:
分析原因:mysql 经过执行开销分析以后认为全表扫描的开销比走索引的开销要小一些,所以就全表扫描了 。
在 MySQL 5.6 及之后的版本中,我们可以使用 optimizer trace 功能查看优化器生成执行计划的整个过程。
有了这个功能,我们不仅可以了解优化器的选择过程,更可以了解每一个执行环节的成本,然后依靠这些信息进一步优化查询。
如下代码所示,打开 optimizer_trace 后,再执行 SQL 就可以查询 information_schema.OPTIMIZER_TRACE 表查看执行计划了,最后可以关闭 optimizer_trace 功能:
SET optimizer_trace="enabled=on";
SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-01-24 05:00:00';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";
使用 name_score 对 name84059<name 条件进行索引扫描需要扫描 25362 行,成本是 30435,因此最终没有选择这个方案。这里的 30435 是查询二级索引的 IO 成本和 CPU 成本之和,再加上回表查询聚簇索引的 IO 成本和 CPU 成本之和,我就不再具体分析了:
{
"index": "name_score",
"ranges": [
"name84059 < name"
],
"rows": 25362,
"cost": 30435,
"chosen": false,
"cause": "cost"
},
使用 create_time 进行索引扫描需要扫描 23758 行,成本是 28511,同样因为成本原因没有选择这个方案:
{
"index": "create_time",
"ranges": [
"0x5e2a79d0 < create_time"
],
"rows": 23758,
"cost": 28511,
"chosen": false,
"cause": "cost"
}
最终选择了全表扫描方式作为执行计划。可以看到,全表扫描 100086 条记录的成本是 20306,和我们之前计算的一致,显然是小于其他两个方案的 28511 和 30435:
{
"considered_execution_plans": [{
"table": "`person`",
"best_access_path": {
"considered_access_paths": [{
"rows_to_scan": 100086,
"access_type": "scan",
"resulting_rows": 100086,
"cost": 20306,
"chosen": true
}]
},
"rows_for_plan": 100086,
"cost_for_plan": 20306,
"chosen": true
}]
},