Files
rikako-note/mysql/mysql文档/mysql_索引.md
2025-02-15 16:28:43 +08:00

16 KiB
Raw Blame History

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 tablelock模式为share那么在执行过程中不会有写事务的发生不需要进行DML日志的记录。

Cardinality

并不是所有的字段都适合创建B+树索引。例如对于性别字段、类型字段等它们的选择范围都很小这种字段不适合创建B+树索引。

性别字段中只有MF两种可选值,故而执行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

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 = xxx
  • select primary key1,key2 from table where key1 = xxx
  • select 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_aidx_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语法,来强制指定使用的索引。