20 KiB
innodb索引与算法
innodb存储引擎支持如下集中常见的索引:
- B+树索引
- 对于B+树索引,其只能找到被查找数据所在的页,然后数据库把页读入到内存中,再在内存中进行查找
- 全文索引
- 哈希索引
- innodb中哈希索引是自适应的,innodb存储引擎会根据目前表的使用情况自动生成哈希索引
索引算法
二分查找
如前所示,mysql数据页中存在page directory,而page directory中slots是按照主键顺序来进行排放的。
对于数据的查找,是通过对page directory中的槽进行二分查找得到的。
B+树索引
inoodb中索引大多是B+树的实现,且具有高扇出的特性。故而,在数据库中,B+树的高度一般都在2~4层,即在查找某一行记录时,最多只需要2~4次IO。
innodb中B+树索引可以分为聚簇索引和辅助索引,其都是基于B+树实现,所有的数据都存放在叶子节点中。
聚簇索引和辅助索引的区别在于叶子节点中是否存放的是整行的记录信息。
聚簇索引
在innodb中,表是由索引组织的,即表中的数据按照主键的顺序存放。
聚簇索引是按照每张表的主键构建一颗B+树,而叶子节点存放的则是整张表的行记录数据,故而,聚簇索引的叶子节点也被称为数据页。
和B+树一样,每个数据页(叶子节点)都通过双向链表来进行链接。
每张表只能拥有一个聚簇索引,并且,在多数情况下,查询优化器倾向于采用聚簇索引。因为聚簇索引能够直接在叶子节点获取到行记录。
非数据页
在非数据页中,存放的是key以及指向数据页的页号。
聚簇索引的存储并非是物理上连续的,而是通过双向链表来维护逻辑上的顺序。
数据页与数据页之间通过双向链表来维护顺序排序,而位于同一个页中的记录之间也是通过双向链表来维护排序的。
对于聚簇索引,其按照主键排序进行查找或按主键进行范围查找时,查询速度很快。
辅助索引
对于辅助索引,叶子节点并不包含行记录的全部数据。在辅助索引的叶子节点中,叶子节点除了包含辅助索引的key外,每个叶子节点中的索引行还包含一个书签(bookmark)。bookmark可以告知inodb存储引擎哪里能找到和索引相对应的行数据。
实际上,innodb存储引擎的辅助索引其bookmark就是相应行数据的聚簇索引key`。
辅助索引并不会影响行数据在聚簇索引上的组织,故而每张表可以含有一个聚簇索引和多个辅助索引。
辅助索引查找
当通过辅助索引来查找数据时,innodb会遍历辅助索引的B+树数据结构,并从叶子节点中获取指向的主键索引值,之后再通过主键索引来查找到完整的行记录。
辅助索引查找分析
如果聚簇索引和辅助索引的B+树高度同样为3,那么当根据辅助索引来查找关联的主键时,需要花费3次IO。此时,再根据主键来从聚簇索引中获取对应行数据,需要另外3次IO。
故而,根据辅助索引来获取行记录,大概需要6次IO。
索引创建
通过alter table语法,可以选择索引的创建方式:
alter table tbl_name
| add {index|key} [index_name]
[index_type] (index_col_name, ...)
algorithm [=] {default|inplace|copy}
lock [=] {default|none|shared|exclusive}
algorithm
通过algorithm可以指定创建或删除索引的算法:
- copy:通过创建临时表的方式,定义新的表结构,并将原表中的数据移动到新的表,最后删除原表,并将临时表重命名
- 该算法将会耗费大量事件,并且在执行时表不可被访问
- inplace:索引创建和删除操作不采用临时表
- default: 按照
old_alter_table参数来判断是通过inplace还是copy来创建或删除索引,默认old_alter_table为off,即是采用inplace算法
lock
lock部分代表索引创建或删除时,对表的加锁情况:
- none:执行索引的创建或删除操作时,对目标表不添加任何的锁,在执行过程中事务仍然能够进行读写操作,不会阻塞。该加锁情况能够获取最大程度的并发度
- share:在执行过程中,对表添加s锁,
s锁会和x锁以及ix锁发生冲突,但是能够兼容s锁和is锁。- 故而,在加锁方式为share的场景下,执行索引的创建或删除时,其他事务仍然能够执行读取操作,但是写操作则是会被阻塞。
- 如果存储引擎不支持share模式,会返回相应的错误信息
- exclusive:在exclusive模式下,执行索引的创建和删除操作时,会对目标表添加一个x锁,此时,其他事务对该表的读和写操作都不会被执行
- default:
- default模式下首先会判断当前操作能否使用none
- 如果不能,则判断是否能够使用shared
- 如果仍然不能,那么最后会判断能否使用exclusive
online DDL
innodb存储引擎实现online DDL的原理是在执行索引的创建或删除操作时,将insert, update, delete这些DML写操作的日志写入到一个缓存中,等到索引创建完成后,再将这些DML语句重做到表上(日志内容类似于redo log)。
该缓存的大小可以通过innodb_online_alter_log_max_size来进行控制,默认大小为128M。
如果再执行索引创建和更新的表比较大,并且在online alter执行过程中存在大量的写事务,那么当innodb_online_alter_log_max_size指定的空间不足以存放时,会抛出错误。
如果发生上述错误,可以考虑增加inodb_online_alter_log_max_size参数的值;也可以设置alter table的lock模式为share,那么在执行过程中不会有写事务的发生,不需要进行DML日志的记录。
Cardinality
并不是所有的字段都适合创建B+树索引。例如对于性别字段、类型字段等,它们的选择范围都很小,这种字段不适合创建B+树索引。
性别字段中只有
M和F两种可选值,故而执行select * from student where sex = 'F'这类sql可能会得到表中50%的数据,这种情况下为性别字段添加B+树索引完全没有必要。
而对于bigint类型的id字段,其选择范围很广,且没有重复,这类字段适合使用B+树索引。
Cardinality
通过show index语句可以查看索引是否拥有高选择性,Cardinality列代表索引中不重复记录数量的预估值。
Cardinality是一个预估值,并不是准确的值,且Cardinality / rows_in_table的值应该尽可能的接近1。如果该值非常小,那么用户应考虑该索引的创建是否有必要。
innodb存储引擎的Cardinality
在生产环境下,索引的操作可能十分频繁,如果每次操作都对cardinality进行统计,那么将会带来巨大的开销。故而,数据库针对Cardinality的统计都是通过采样来进行的。
在innodb中,针对cardinality统计信息的更新发生在两个操作中:insert, update。innodb针对cardinality的更新策略为:
- 表中1/16的数据已经发生变化
- 自上一次统计cardinality信息时起,表中1/16的数据已经发生过变化,此时需要重新统计cardinality
- stat_modified_counter >= 2000,000,000
- 如果针对表中某一行数据进行频繁的更新,那么该场景不会适配第一种更新策略,故而在innodb内部维护了一个计数器stat_modified_counter,用于标识发生变化的次数,当计数器的值大于
2000,000,000时,也需要更新cardinality
- 如果针对表中某一行数据进行频繁的更新,那么该场景不会适配第一种更新策略,故而在innodb内部维护了一个计数器stat_modified_counter,用于标识发生变化的次数,当计数器的值大于
Cardinality采样
默认情况下,innodb会针对8个叶子节点进行采样,采样过程如下:
- 取得B+树索引中叶子节点的数量,记为A
- 随机获取索引8个叶子节点,统计每个页不同记录的个数,记为P1, P2, ... , P8
- 根据采样信息给出Cardinality的预估值
(P1 + P2 + ... + P8) * A/8
索引使用
联合索引
联合索引代表对表上的多个列进行索引。
示例如下:
create table t (
id bigint not null auto_increment,
a int not null,
b int not null,
primary key(id),
index idx_a_b (a, b)
);
对于上述联合索引(a,b),各查询语句使用索引的情况如下:
where a = xxx and b = xxx:- 该语句会使用联合索引
where a = xxx:- 该语句同样可以使用联合索引
where b = xxx:- 该语句无法使用联合索引
覆盖索引
innodb存储引擎支持覆盖索引,即通过辅助索引即可得到查询记录,无需再次查询聚簇索引。
通常来说,辅助索引中叶子节点的记录大小要远小于聚簇索引中叶子节点的记录大小。故而,在范围统计的场景下(
select count(1)),辅助索引叶子节点中,一个页包含更多的记录,表扫描时需要读取的页数页更少。故而,在部分场景下,使用覆盖索引能够节省大量的io操作。
对于辅助索引而言,其叶子节点的记录中包含(primary key1, primary key2, ..., key1, key2)等信息,故而,如下语句都可以通过一次辅助联合索引查询来完成:
select key2 from table where key1 = xxx- 会命中辅助索引,且辅助索引中包含key2的值,故而无需再次查询聚簇索引
select primary key2,key2 from table where key1 = xxxselect primary key1,key2 from table where key1 = xxxselect primary key1, primary key2, key2 from key1 = xxx
覆盖索引对于统计场景的好处
例如,对于表buy_log
create table buy_log (
userid int unsigned not null,
buy_date date,
index `idx_userid` (userid),
index `idx_userid_buy_date` (userid, buy_date)
);
预置如下数据:
insert into buy_log(userid, buy_date)
values (1, '2022-01-01'),
(2,'2022-03-04'),
(3, '2024-12-31'),
(4, '2024-07-11'),
(5, '2025-01-01');
执行explain select count(*) from buy_log;语句时,由于where条件为空,其并不会命中索引,但是语句执行的结果如下所示:
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | buy_log | null | index | null | idx_userid | 4 | null | 5 | 100 | Using index |
上述返回结果中,possible_keys列为null,但是实际执行时优化器却选择了idx_userid索引,Extra列值为Using index,代表优化器选择了覆盖索引。
由于覆盖索引中记录大小更小,故而使用覆盖索引能够节省io,提升性能。
并且,对于explain select count(*) from buy_log where buy_date >= '2023-01-01' and buy_date <= '2024-01-01';语句,其返回结果如下
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | buy_log | null | index | idx_userid_buy_date | idx_userid_buy_date | 8 | null | 5 | 20 | Using where; Using index |
正常来说,idx_userid_buy_date索引组成为(userid, buy_date),当根据buy_date来进行查找时,不应该命中索引。
但是,由于辅助索引中同一页包含记录更多,故而上述语句仍然使用了idx_userid_buy_date覆盖索引。
优化器选择不使用索引
在某些情况下,当使用explain语句进行分析时,会发现优化器没有选择索引去查找数据,而是通过全盘扫描聚簇索引。这种情况通常发生在范围查找,join连接等操作时。
辅助索引不能覆盖
对于辅助索引不能进行覆盖的场景,优化器仅当查询少量数据时选择辅助索引。
优化器不选择辅助索引的原因是,根据辅助索引再次查询聚簇索引时,会带来大量的随机读。由于辅助索引和聚簇索引的排序不同,在辅助索引中连续的数据,很可能在聚簇索引中是分散的。如果使用辅助索引,那么在根据主键查询聚簇索引时,可能带来大量的随机读,主键可能随机分布在各个数据页中,这样会带来大量的io。
故而,当辅助索引需要读取大量的记录,并且辅助索引不能覆盖时,优化器会倾向直接在聚簇索引中进行顺序查找。顺序读通常要远快于随机读。
如果mysql server实例部署在固态硬盘上,随机读取的速度很快,同时确认使用辅助索引的性能更佳,可以使用
force index来强制使用某个索引,使用示例如下:select * from orderdetails force index (orderID) where orderid > 10000 and order id < 102000
索引提示
mysql支持索引提示,如下两种场景可能会用到索引提示:
- mysql错误的选择了某个索引,导致sql语句运行较慢
- 某个sql可选择索引非常多,此时优化器选择执行计划的耗时可能大于sql语句执行的耗时
mysql数据库中,索引提示的语法如下;
table_name [[as] alias] [index_hint_list]
-- 其中,index_hint_list的含义如下
index_hint_list:
index_hint[, index_hint]...
-- index_hint的含义则如下:
index_hint:
USE {INDEX|KEY}
[{FOR {JOIN | ORDER BY | GROUP BY}}] ([index_list])
| IGNORE {INDEX | KEY}
[{FOR {JOIN | ORDER BY | GROUP BY}}] (index_list)
| FORCE {INDEX|KEY}
[{FOR {JOIN | ORDER BY | GROUP BY}}] (index_list)
-- index_list含义如下:
index_list:
index_name, [, index_name]...
创建t_demo示例如下:
create table t_demo (
a int not null,
b int not null,
key idx_a (a),
key idx_b (b)
);
insert into t_demo(a,b)
values
(1,1),
(1,2),
(2, 3),
(2, 4),
(1,2);
Use index
执行如下语句explain select * from t_demo where a=1 and b = 2;,其返回执行计划如下:
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | t_demo | null | index_merge | idx_a,idx_b | idx_b,idx_a | 4,4 | null | 1 | 83.33 | Using intersect(idx_b,idx_a); Using where; Using index |
易知优化器选择了使用idx_a和idx_b两个索引来完成该查询,且Extra列包含了Using intersect(b,a),代表查询结果根据两个索引得到的结果进行求交集的数学运算。
可以通过Use Index来提示使用idx_a索引:
explain select * from t_demo use index (idx_a) where a=1 and b = 2;
得到结果如下:
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | t_demo | null | ref | idx_a | idx_a | 4 | const | 3 | 20 | Using where |
上述结果显示执行方案选用了idx_a。
在使用
use index语法之后,仍然有可能出现优化器没有使用该索引的情况,此时可以使用force index语法,来强制指定使用的索引。
Multi-Range Read
mysql支持Multi-Range Read优化,用于减少随机的磁盘访问,将随机访问转化为较为顺序的数据访问。对于io-bound的sql查询,其能带来性能的较大提升。
mutli-range read优化适用于range, ref, eq_ref类型的查询。
multi-range read优势
- MRR能够令数据访问变得较为顺序,在查询辅助索引时,对于得到的查询结果按照主键进行排序,并按照主键排序的顺序进行书签查找
- 减少缓冲池中页被替换的次数
- 批量处理对于key的查询操作
范围查询和join查询优化
对于innodb的范围查询和join查询,MRR工作方式如下
- 将查询到的辅助索引键值存放在缓冲中,此时缓存中的数据按照辅助索引排序
- 将缓存中的键值按照rowId进行排序
- 根据rowId的排序来实际访问数据文件
当innodb的缓冲池不足以存放表中所有数据时,频繁的离散读写会导致缓冲池中的页被换出,又被不断读入。
如果按照主键顺序进行访问,可以将该类行为降至最低,页面不会被换出缓冲区后又被读入。
拆分键值对
除了上述优化外,对某些范围查询,mrr还能够进行拆分优化,这样在拆分过程中直接能过滤掉一些不符合查询条件的数据。
例如:
select * from t where key_part1 >= 1000 and key_part1 < 2000 and key_part2 = 1000;
表t存在(key_part1, key_part2)的联合索引。
未开启MRR
如果未开启mrr,那么此时查询类型为RANGE,mysql优化器会将key_part位于[1000, 2000)范围内的全部数据都取出,即是key_part2不为1000。在去除后,再按key_part2条件对取出数据进行过滤。
这将会导致有大量无用数据被取出。如果存在大量数据且key_part2不为1000,那么使用mrr将会有大量性能提升。
开启MRR优化
在使用了MRR优化后,优化器会先对查询条件进行拆分,将key_part1 >= 1000 and key_part1 < 2000 and key_part2 = 1000条件拆分为(1000, 1000), (1001, 1000), ..., (1999, 1000),在根据拆分后的条件进行查询。
mrr控制
是否开启mrr可以通过参数optimizer_switch中的flag来控制,当mrr为on时,代表启用mrr优化。
mrr_cost_based标记代表是否通过cost based的方式来选择是否启用mrr。
如果将mrr设置为on,mrr_cost_based设置为off,代表总是启用mrr优化。
read_rnd_buffer_size
read_rnd_buffer_size用来控制键值的缓冲区大小,当大于该值时,则执行器则对已经缓存的数据根据rowId来进行排序,并通过rowId来获取数据。该值默认为256K.
Index Condition Pushdown(ICP)
可以通过optimizer_switch中的flag来控制ICP是否开启,如index_condition_pushdown为on,代表icp开启。
例如,表people包含(zip_code, last_name, first_name)索引,执行如下语句
select * from people where zipcode = '95054' and last_name like '%asahi%' and address like '%street%';
关闭ICP
如果ICP优化没有开启,那么数据库首先会根据索引查询zipcode为95054的记录,然后查询出后再根据查询出的结果过滤where的后两个条件last_name like '%asahi%' and address like '%street%'。
开启ICP
当开启ICP后,数据库会将where的部分过滤条件放在存储引擎层,在索引获取数据同时就会进行where条件的过滤。