MySQL是怎样运行的:(7)B+树索引的使用
Session 7 B+树索引的使用
一个表上索引建的越多,就会占用越多的存储空间,在增删改记录的时候性能就越差。为了能建立又好又少的索引,我们需要了解这些索引在哪些条件下起作用的。
本章的辅助表:
1 | CREATE TABLE person_info( |
全值匹配
我们的搜索条件中的列和索引列一致的话,这种情况就称为全值匹配。
1 | SELECT * FROM person_info WHERE birthday = '1990-09-27' AND phone_number = '15123983239' AND name = 'Ashburn'; |
WHERE
子句中的几个搜索条件的顺序对查询结果没有影响(查询优化器的作用)。
匹配左边的列
如果我们想使用联合索引中尽可能多的列,搜索条件中的各个列必须是联合索引中从最左边连续的列(索引列的前缀列)。
比如对于联合索引(A, B, C),只有搜索列是A、A,B、A,B,C才会用到索引。
匹配列前缀
所以对于字符串类型的索引列来说,我们只匹配它的前缀也是可以快速定位记录的,比方说我们想查询名字以'As'
开头的记录,那就可以这么写查询语句:
1 | SELECT * FROM person_info WHERE name LIKE 'As%'; |
对如下的URL表
1 | +----------------+ |
改为
1 | +----------------+ |
就可以用WHERE url LIKE 'moc%'
来查询了。
匹配范围值
如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到B+
树索引。
1 | SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' AND birthday > '1980-01-01'; |
这个查询中通过name
进行范围查找的记录中可能并不是按照birthday
列进行排序的,所以在搜索条件中继续以birthday
列进行查找时是用不到这个B+
树索引的。
精确匹配某一列并范围匹配另外一列
对于同一个联合索引来说,虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找(也就是使用=),则右边的列可以进行范围查找,比方说这样:
1 | SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday > '1980-01-01' AND birthday < '2000-12-31' AND phone_number > '15100000000'; |
用于排序
我们在写查询语句的时候经常需要对查询出来的记录通过ORDER BY
子句按照某种规则进行排序。有的时候可能查询的结果集太大以至于不能在内存中进行排序的话,还可能暂时借助磁盘的空间来存放中间结果,排序操作完成后再把排好序的结果集返回到客户端。在MySQL
中,把这种在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:filesort
),跟文件
这个词儿一沾边儿,就显得这些排序操作非常慢了
但是如果ORDER BY
子句里使用到了我们的索引列,就有可能省去在内存或文件中排序的步骤。
1 | SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10; |
因为这个B+
树索引本身就是按照上述规则排好序的,所以直接从索引中提取数据,然后进行回表
操作取出列。
不可以使用索引进行排序的几种情况
- ASC、DESC混用
要求各个排序列的排序顺序是一致的
- WHERE子句中出现非排序使用到的索引列
1 | SELECT * FROM person_info WHERE country = 'China' ORDER BY name LIMIT 10; /* 因为country不在联合索引中,所以不会用到索引 */ |
- 排序列使用了复杂的表达式
1 | SELECT * FROM person_info ORDER BY UPPER(name) LIMIT 10; |
使用了UPPER
函数修饰过的列就不是单独的列了,无法使用索引进行排序。
用于分组
1 | SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number |
和使用B+
树索引进行排序是一个道理,分组列的顺序也需要和索引列的顺序一致,也可以只使用索引列中左边的列进行分组。
回表的代价
1 | SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow'; |
由于索引idx_name_birthday_phone_number
对应的B+
树中的记录首先会按照name
列的值进行排序,所以值在Asa
~Barlow
之间的记录在磁盘中的存储是相连的,集中分布在一个或几个数据页中,我们可以很快的把这些连着的记录从磁盘中读出来,这种读取方式我们也可以称为==顺序I/O==。从二级索引获取到的记录的id
字段的值可能并不相连,而在聚簇索引中记录是根据id
(也就是主键)的顺序排列的,所以根据这些并不连续的id
值到聚簇索引中访问完整的用户记录可能分布在不同的数据页中,这样读取完整的用户记录可能要访问更多的数据页,这种读取方式我们也可以称为==随机I/O==。一般情况下,顺序I/O比随机I/O的性能高很多。
需要回表的记录越多,使用二级索引的性能就越低,甚至让某些查询宁愿使用全表扫描也不使用二级索引
,比如上述的SQL语句,假如name
值在Asa
~Barlow
之间的用户记录数量占全部记录数量90%,意味着90%多的id
值需要回表,那还不如全表扫描。
查询优化器会事先对表中的记录计算一些统计数据,计算需要回表的记录数。需要回表的记录数越多,就越倾向于使用全表扫描,反之倾向于使用二级索引 + 回表
的方式。
1 | SELECT * FROM person_info ORDER BY name, birthday, phone_number; # 全表扫描 |
覆盖索引
为了彻底告别回表
操作带来的性能损耗,我们建议:最好在查询列表里只包含索引列,比如这样:
1 | SELECT name, birthday, phone_number FROM person_info |
只需要用到索引的查询方式称为索引覆盖
。相应的排序操作也优先使用覆盖索引
的方式进行查询。
我们很不鼓励用*
号作为查询列表,最好把我们需要查询的列依次标明。
如何挑选索引
在使用索引时需要注意下面这些事项:
只为用于搜索(WHERE)、排序或分组的列创建索引(出现在查询列表中的列就没必要建立索引了)
为列的基数大的列创建索引
列的基数
指的是某一列中不重复数据的个数。在记录行数一定的情况下,列的基数越大,该列中的值越分散,列的基数越小,该列中的值越集中索引列的类型尽量小
以整数类型为例,有TINYINT
、MEDIUMINT
、INT
、BIGINT
这么几种类型(1~4字节)。在表示的整数范围允许的情况下,尽量让索引列使用较小的类型。数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘I/O
带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。这个建议对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值,其他所有的二级索引的节点处都会存储一份记录的主键值,如果主键适用更小的数据类型,也就意味着节省更多的存储空间和更高效的I/O
。
- 可以只对字符串值的前缀建立索引
在二级索引的记录中只保留字符串前几个字符,只对字符串的前几个字符进行索引,这样既节约空间,又减少了字符串的比较时间。比方说我们在建表语句中只对name
列的前10个字符进行索引可以这么写
1 | CREATE TABLE person_info( |
name(10)
就表示在建立的B+
树索引中只保留记录的前10
个字符的编码,这种只索引字符串值的前缀的策略是我们非常鼓励的,尤其是在字符串类型能存储的字符比较多的时候。
- 只有索引列在比较表达式中单独出现才可以适用索引
WHERE my_col * 2 < 4
WHERE my_col < 4/2
第1个WHERE
子句中会依次遍历所有的记录,计算这个表达式的值是不是小于4
第2个WHERE
子句中my_col
列并是以单独列的形式出现的,这样的情况可以直接使用B+
树索引。
所以结论就是:如果索引列在比较表达式中不是以单独列的形式出现,而是以某个表达式,或者函数调用形式出现的话,是用不到索引的。
- 为了尽可能少的让
聚簇索引
发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT
属性。
减少类似以下情形的发生:
- 定位并删除表中的重复和冗余索引
1 | CREATE TABLE person_info( |
- 尽量使用
覆盖索引
进行查询,避免回表
带来的性能损耗。