概述
例子:模拟100个用户同时对一个拥有100万行数据的表进行2000次查询,对比无索引和有索引的耗时情况。
mysqlslap --defaults-file=/etc/my.cnf --concurrency=100 --iterations=1 --create-schema='test' --query="select * from test.t100w where k2='MN89'" engine=innodb --number-of-queries=2000 -uroot -p -verbose
无索引时,运行结果:
Benchmark
Running for engine rbose
Average number of seconds to run all queries: 948.820 seconds
Minimum number of seconds to run all queries: 948.820 seconds
Maximum number of seconds to run all queries: 948.820 seconds
Number of clients running queries: 100
Average number of queries per client: 20
有索引时,运行结果:
为test.t100w表中的k2列设置索引:
alter table test.t100w add index idx_k2(k2);
由于数据量较大,增加索引耗时会比较久。需要注意的是,设置索引的语句会导致锁表。
之后再测试一遍:
Benchmark
Running for engine rbose
Average number of seconds to run all queries: 1.589 seconds
Minimum number of seconds to run all queries: 1.589 seconds
Maximum number of seconds to run all queries: 1.589 seconds
Number of clients running queries: 100
Average number of queries per client: 20
对比一下,在设置索引之前,耗时为948.820s,设置索引之后耗时缩减到了1.589s。由此可见,对相关列使用索引,可大幅提高select操作性能。
索引分类
MySQL的索引是在存储引擎层实现的,因此,对各种索引算法的支持情况与存储引擎的类型息息相关。InnoDB引擎默认创建的是BTREE索引。
算法分类
各种常用引擎支持的算法如下表:
|
|
MyISAM引擎 |
InnoDB引擎 |
Memory引擎 |
|
B-Tree索引 |
√ |
√ |
√ |
|
Hash索引 |
× |
× |
√ |
|
R-Tree索引 |
√ |
× |
× |
|
Full-Text索引 |
√ |
√ |
× |
需要主要了解B-Tree索引。
B-Tree索引
B-Tree
在了解B-Tree索引之前,首先要了解什么是B-Tree。
B-Tree是一种常用的数据结构,要详细介绍的话就要从树讲到二叉树再到平衡二叉树之类了,这部分不是这篇笔记要记录的重点,基础差一些的朋友可以补一下数据结构。
B树又称多路平衡查找树,显而易见,它是一种树形结构,主要用于查找算法。下图是一个3阶B树的示意图:
如图,非空m阶B树的特点如下:
1、树中每个节点,至多有m棵子树;
2、非空B树的根节点至少有两棵子树;
3、除根节点以外,每个分支节点至少有⌈m/2⌉棵子树;
4、最后一层方框代表着查找失败的区间,不计入层数。
B树的查找过程:
设目标关键字值为n,在上图的B树中,首先查找根节点,根节点中关键字为20、37,分割出三个区间(-∞, 20)、(20, 37)、(37, +∞),即三棵子树的节点的关键字取值范围,如果n没有命中节点中的关键字,则进入对应取值范围的子树,以此类推,直到查找命中,或进入查找失败的区间。
显然,使用B树查找算法,可以很大程度减少比较次数,从而提高查找效率。
B+树
MySQL的BTREE索引算法,实质上是B+树算法(看了一些视频也有说是B*树算法,但是更多文章说是B+树,这一点暂时存疑,如果我这里认识有错误的话希望有小伙伴能够指正一下。不过B*树也是B+树的变种,差异并不是特别大,根据B+树理解即可)。B+树是B树的变种,3阶B+树示意图如下:
如图,非空m阶B+树的特点如下:
1、每个分支节点至多有m棵子树;
2、非叶根节点至少有两棵子树,其他分支节点至少有⌈m/2⌉棵子树;
3、节点关键字数量与子树数量相等;
4、节点关键字值代表着该关键字对应子节点中值最大的关键字。
可以看到B+树与B树的差异主要有以下几个方面:
1、B树中的分支节点中的关键字数目为其子树数目减1,而B+树中的分支节点中的关键字数目等于其子树数目;
2、B树中每个节点都包含了关键字及指向其对应记录的指针,而B+树中,仅有叶结点包含关键字及指向其对应记录的指针,其余分支节点仅有关键字值
3、B+树的相邻叶子节点依大小顺序链接了起来。
根据以上差异,我们可以总结出B树查找算法与B+树查找算法的区别:
1、在B树中,一旦查询到目标关键字即可获取到指向对应记录的指针,立即停止查找,而若是没有目标关键字,则需要一直遍历到查询树的最底层。而在B+树中,仅有叶子节点包含了指向目标关键字对应的记录信息,因此无论查找成功与否,每次查找都是一条从根节点到叶结点的路径;
2、由于B+树的相邻叶子节点被链接起来了,进行连续查询时,当一条查询命中,若下一查询关键字与该关键字相近,则可直接通过链接访问到相邻的叶子节点,避免再遍历一次查询树。
数据库中的所有数据,都存放在表空间(tablespace)中,而表空间又被划分为不同的段(segment),一个段即对应一个表,而每个表又是由数个连续的页(page)构成,页面大小是固定的,默认为16KB,数据就存放在页中。对于用户而言,一次查询是要读出一行数据,而对于存储引擎而言,无论是要读出多少行数据,都需要从磁盘中读出整张表,再根据这行数据的页偏移量去找到目标数据。
辅助索引B+树的叶子节点中,并没有包含索引键值对应记录的地址,而是存放着其对应记录的聚集索引键值,而聚集索引B+树的叶子节点中,包含着索引键值对应的整行记录。所以使用辅助索引进行查询,需要先搜索辅助索引树在搜索聚集索引树,才能得到查询的记录。
功能分类
从功能上来讲,索引又可分为聚集索引和辅助索引。
聚集索引
表中数据的物理顺序与索引键值的顺序相同,这样的索引即聚集索引。由于表中数据的物理顺序只有一种情况,显然,一张表只能有唯一的聚集索引。
聚集索引树的生成:
1、MySQL会自动选择主键作为聚集索引列,没有主键会选择第一个唯一键作为聚集索引列,如果既没有主键也没有合适的唯一键,则生成一个默认的、6字节自增的隐藏主键,作为聚集索引列;
2、MySQL存储数据时,会按照聚集索引列的值的顺序,将数据有序存放在磁盘上;
3、聚集索引直接将原表数据页作为叶子节点,然后提取索引列进一步生成分直接点和根节点。
辅助索引
辅助索引也称二级索引,与聚集索引对应,索引键值的顺序与数据的物理顺序无关。看到一个很形象的例子,如果把一张表比作字典,那么可以把拼音查找(a ~ z)看作是聚集索引,把部首查找看作是辅助索引。由于索引键值与数据的物理顺序无关,因此一张表可以有多个辅助索引。根据存储引擎可以定义每个表的最大索引数和最大索引长度,每种存储引擎对每个表至少支持16个索引,总索引长度至少为256字节。
辅助索引树的生成:
1、提取索引列的所有值,进行排序
2、将排序好的值,均匀地存放在叶子节点,进一步生成分支节点和根节点
3、叶子节点包含键值和对应数据行的聚集索引键值(页号 + 页面偏移量)
进一步细分,辅助索引又可以分为单列辅助索引、联合索引、唯一索引和前缀索引。主要说一下联合索引。
联合索引
如果经常使用多个约束条件进行查询的话,可以考虑建立联合索引。比如经常使用以下约束条件:
where a = ? and b = ? and c = ? …
可以将a、b、c三列建立为联合索引idx_a_b_c(a, b, c)。对于a、b、c三列,实际查询中可能会有以下几种情况:
1、同时使用了a、b、c列作为约束,进行等值查询
约束条件的顺序不会影响走索引的情况,即无论是where a = ? and b = ? and c = ?还是where c = ? and b = ? and a = ?这样的条件排列,都能够使用到idx_a_b_c这个索引。因为这几个约束条件本身就是顺序无关的,MySQL优化器会对语句进行一次重新排序,使其能够满足索引带来的效率提升。同理也可以解释第二点。
2、只使用了a、b、c部分列作为约束,进行等值查询
优化器依然会对语句中的约束条件做排序,但是索引覆盖的键值长度会止于缺失的那一列。比如无论是where a = ? and c = ?还是where c = ? and a = ?最终都会优化为a=? and c=?的顺序,由于缺失了b列做约束条件,因此只有a列能走idx_a_b_c索引。所以我们在建立联合索引时,应该尽量将重复值少的列放在最左边,力求索引覆盖的列能够过滤掉最多的重复数据。
3、<、>、<=、>=、like这类不等值查询
优化器还是会将约束条件排序,但是索引覆盖的键值长度会止于第一个不等值条件。例如where a = ? and b <= ? and c = ?索引idx_a_b_c覆盖的仅有a、b两列,c列的约束条件不会走索引。
对于前缀索引,需要注意的是,对于InnoDB引擎的表,前缀长度最长是3072字节,前缀的限制应以字节为单位进行测量,而varchar(10)这样的数据类型中定义的是字符数,还需根据字符集来计算其真实占用的字节数。
索引的命令操作
查询索引
desc `tablename`;
show index from `tablename`;
PRI:主键索引
MUL:辅助索引
UNI:唯一索引
创建索引
需要注意的是,为表增加索引的操作将会导致锁表。
单列辅助索引:
alter table `tablename` add index `index_name`(`col_name`);
多列联合索引:
alter table `tablename` add index `index_name`(`col_name_1`, `col_name_2`, …);
唯一索引:
alter table `tablename` add unique `index_name`(`col_name`);
前缀索引:
alter table `tablename` add index `index_name`(`col_name(prefix_len)`);
从执行计划分析索引
在MySQL中,可以通过explain/desc语句,获取到优化器选择的语句执行计划,我们可以以此来分析判断语句的执行效率。接下来通过MySQL提供的练习库sakila库来了解一下执行计划,加深对索引的认识(sakila库可以在官网上搜到)。
首先来看看sakila.city表的表结构:
desc `sakila.city`\G;
*************************** 1. row ***************************
Field: city_id
Type: smallint(5) unsigned
Null: NO
Key: PRI
Default: NULL
Extra: auto_increment
*************************** 2. row ***************************
Field: city
Type: varchar(50)
Null: NO
Key:
Default: NULL
Extra:
*************************** 3. row ***************************
Field: country_id
Type: smallint(5) unsigned
Null: NO
Key: MUL
Default: NULL
Extra:
*************************** 4. row ***************************
Field: last_update
Type: timestamp
Null: NO
Key:
Default: CURRENT_TIMESTAMP
Extra: on update CURRENT_TIMESTAMP
4 rows in set (0.00 sec)
主要关注key值,city_id列key值为PRI,表示该列为主键,country_id列key值为MUL,表示该列有辅助索引。还可以直接查看一下该表的索引信息:
show index from sakila.city\G;
*************************** 1. row ***************************
Table: city
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: city_id
Collation: A
Cardinality: 600
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
*************************** 2. row ***************************
Table: city
Non_unique: 1
Key_name: idx_fk_country_id
Seq_in_index: 1
Column_name: country_id
Collation: A
Cardinality: 109
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
2 rows in set (0.01 sec)
很明显,该表有聚集索引列city_id和辅助索引列country_id。
接下来,看看查询语句select * from sakila.city;语句是如何执行的,可以使用desc或explain获取select语句的执行计划:
explain select * from sakila.city;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | city | NULL | ALL | NULL | NULL | NULL | NULL | 600 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
select_type
select类型,取值有:SIMPLE-不使用表链接或子查询、PRIMARY-外层查询、UNION-union中的第二个或后面的查询语句、SUNQUERY-子查询中的第一个select。
table
输出结果集的表。
type
访问类型,即MySQL在表中查找到该行的方式,常见取值如下(由上至下,性能由最差到最好):
ALL
无索引,直接遍历全表。
index
索引全扫描,遍历整个索引来查询匹配的行。例如:
获取查询city表中所有数据行的city_id列值的语句的执行计划
explain select city_id from sakila.city\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: city
partitions: NULL
type: index
possible_keys: NULL
key: idx_fk_country_id
key_len: 2
ref: NULL
rows: 600
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
city_id列是该表的主键索引列,而上述查询将遍历所有数据行的city_id列,因此会遍历整个索引来查询匹配的行,为index类型。
range
索引范围扫描,常见于<、<=、>、>=、between等操作符。需要注意的是,对于辅助索引列,!=、like ‘%xx%’、not in操作符不走索引,而对于主键索引列则会走range类型。例如:
sakila.store表,有主键索引列store_id和唯一索引列manager_staff_id,分别以这两个索引为条件进行查找
desc select * from sakila.store where manager_staff_id != 1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: store
partitions: NULL
type: ALL
possible_keys: idx_unique_manager
key: NULL
key_len: NULL
ref: NULL
rows: 2
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
desc select * from sakila.store where store_id != 1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: store
partitions: NULL
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 1
ref: NULL
rows: 2
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
ref
辅助索引等值查询。例如:
获取查询city表中所有country_id为1的数据行信息的执行计划
desc select * from sakila.city where country_id = 1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: city
partitions: NULL
type: ref
possible_keys: idx_fk_country_id
key: idx_fk_country_id
key_len: 2
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
country_id是city表的非唯一辅助索引列,因此为ref类型。
eq_ref
多表连接时,主表(from后面的表,又称驱动表)使用主键列或唯一列作为连接条件。例如:
获取查询film表中所有category_id为1的title的执行计划
desc
select film.title
from
sakila.film
join
sakila.film_category
on film.film_id=film_category.film_id
where film_category.category_id=1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_category
partitions: NULL
type: ref
possible_keys: PRIMARY,fk_film_category_category
key: fk_film_category_category
key_len: 1
ref: const
rows: 64
filtered: 100.00
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: sakila.film_category.film_id
rows: 1
filtered: 100.00
Extra: NULL
2 rows in set, 1 warning (0.00 sec)
作为连接条件的film_id列为film表中的主键索引列,因此执行类型为eq_ref。
const/system
主键索引列或唯一索引列的等值查询。例如:
desc select * from sakila.film where film_id=1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
NULL
不用访问表或索引,直接就能得到结果。
通常来说,我们在构造查询语句时应该尽量追求更高效的访问类型,比如完成同一个查询——查询film_category表中所有category_id为1和2的数据行的film_id——我们有如下两种语句:
select film_id from sakila.film_category where category_id in(1, 2)\G;
select film_id from sakila.film_category where category_id=1 union all select film_id from sakila.film_category where category_id=2\G;
前者的访问类型为range,而后者为ref。
possible_key & key
possible_key:查询时可能使用的索引。
key:查询时实际使用的索引。
有些时候,执行计划中索引的使用情况可能跟我们预想的并不一致,比如,film表中的language_id列为辅助索引列,我们往往认为将language_id作为约束条件、查询表中language_id为1的数据行这样的语句将会走索引idx_fk_language_id,实际看一下:
desc select * from sakila.film where language_id=1\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: ALL
possible_keys: idx_fk_language_id
key: NULL
key_len: NULL
ref: NULL
rows: 1000
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
可以发现,possible_key确实是我们期望的idx_fk_language_id,但实际这条语句却并不会走索引,访问类型也是遍历全表的ALL。原因很简单,sakila库的初始数据中,film这张表里所有的数据行,language_id列值都为1,所以查询language_id为1的数据行自然是要遍历全表的。这也可以看出,索引每一次数据插入,都会导致索引重新组织,而对已有的数据的表新增索引列,MySQL则会根据所有数据的索引列值来构建索引,为防止数据更新打乱了索引构建的过程,这个过程中肯定是会锁表的,所以,新增索引要找合适的时机,而创建索引则不要过度,否则也会影响数据写入的效率。
key_len
使用到的索引长度。需要注意的是,索引在创建时,会为键值预留全部的空间,比如,对于字符集为utf8mb4的表,int类型占4字节,那么键值为int类型的索引列会有4字节的预留空间。特别需要注意的是varchar/char类型,在定义列时这两个类型后的括号中的数字,是对字符长度的限制,如varchar(10)是指该列为最多10个字符的可变字符串类型,而utf8mb4中英文数字占1字节,汉字占4字节,索引会预留最大字节长度即认为所有字符都是汉字来预留其键值空间。看以下例子:
先创建个测试表:
create table test.test_key_len(
id int primary key,
c1 varchar(6) not null,
c2 varchar(6),
c3 char(6) not null,
c4 char(6)
) charset utf8mb4;
为c1,c2,c3,c4列分别创建好索引,然后插入数据。接下来就来获取几个查询语句的执行计划:
desc select * from test.test_key_len where c1='xxx'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test_key_len
partitions: NULL
type: ref
possible_keys: idx_c1
key: idx_c1
key_len: 26
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
c1列键值为varchar(6),按上述内容推断,MySQL会为其预留24字节的空间,而varchar类型还有1字节的起始符和1字节的结束符,因此key_len共有26字节。
desc select * from test.test_key_len where c2='xxx'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test_key_len
partitions: NULL
type: ref
possible_keys: idx_c2
key: idx_c2
key_len: 27
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
c2列与c1列的区别就在于c2列未指定非空,因此在c1列的基础上,还需要额外1字节标记该索引键值是否为空。
desc select * from test.test_key_len where c3='xxx'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test_key_len
partitions: NULL
type: ref
possible_keys: idx_c3
key: idx_c3
key_len: 24
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
char类型不存在起始列的情况,因此对于c3列,索引键值预留空间是24字节。可想而知,没有指定非空的c4列,其key_len值自然就是25字节了。
优化相关
要对SQL进行优化,势必要定位执行效率低下的SQL。
对于突发性的效率降低甚至是Hang机,可以使用show processlist;语句定位出耗时过长的会话来定位出影响系统性能的SQL;对于某一时间内、持续性的效率低下,则需要通过慢查询日志来定位(需要将slow-query-log参数置为1,MySQL会将超过long_query_time参数所设阈值的SQL写入slow_query_log_file参数指定的文件中)。慢查询日志只会在查询结束后才会生成。
得到效率低下的SQL语句后,就可以通过desc/explain命令来获取其执行计划并进行具体分析了,大多数情况下具体分析后都可以依靠建索引或是改语句来解决。
索引设计要点
1、要在条件列(常用于where子句中做约束条件的列)上创建索引,而非查询列(常用于select关键字后做选择列表中的列);
2、尽量使用唯一索引,或是重复值较少的列上创建索引,力求筛选出最少的结果;
3、对字符串列创建索引,应尽量使用短索引。较小的索引涉及的磁盘IO较少,较短的值比较起来更快,更为重要的是,对于较短的键值,索引高速缓存中的块能容纳更多的键值,MySQL也能在内存中容纳更多的值;
4、通常情况,对于数据量较小的表,除主键外,不需要额外创建索引。只有对于数据量庞大的表而言,扫描全表对于系统会造成巨大的负担,因此才能发挥出索引的效果。对于大表,还需要持续统计操作频率较高的SQL,并对其进行分析,用于对索引进行改进;
5、不要过度索引。修改表内容时,索引也会进行更新,甚至会需要重构,索引过多反而会在数据写入时使表锁住更长的时间;
6、索引维护应避开业务繁忙期。