Mysql快速查询的秘籍--B+树索引的使用

上篇 Mysql快速查询的秘籍–B+树索引的理解

一、索引的代价

  • 空间上的代价
    每建立一个索引都要为它建立一棵 B+ 树,每一棵 B+ 树的每一个节点都是一个数据页,一个页默认会占用 16KB 的存储空间,一棵很大的 B+ 树由许多数据页组成。
  • 时间上的代价
    每次对表中的数据进行增、删、改操作时,都需要去修改各个 B+ 树索引。
    增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位,页面分裂、页面回收等操作来维护好节点和记录的排序。

一个表上索引建的越多,就会占用越多的存储空间,在增删改记录的时候性能就越差。

二、B+树索引适用的条件

创建一个表:

CREATE TABLE person_info(
	id INT NOT NULL auto_increment,
	name VARCHAR(100) NOT NULL,
	birthday DATE NOT NULL,
	phone_number CHAR(11) NOT NULL,
	country varchar(100) NOT NULL,
	PRIMARY KEY (id),
	KEY idx_name_birthday_phone_number (name, birthday, phone_number)
);
  • InnoDB 存储引擎会自动为 id 列建立聚簇索引。
  • 二级索引idx_name_birthday_phone_number,它是由3个列组成的联合索引。所以在这
    个索引对应的 B+ 树的叶子节点处存储的用户记录只保留name、birthday、phone_number这三个列的值以及主键id的值,并不会保存country列的值。
  • 一个表中有多少索引就会建立多少棵B+树,person_info表会为聚簇索引和idx_name_birthday_phone_number索引建立2棵 B+ 树。

先按照name列的值进行排序。
如果name列的值相同,则按照birthday列的值进行排序。
如果birthday列的值也相同,则按照phone_number的值进行排序。

在这里插入图片描述

1. 全值匹配

如果我们的搜索条件中的列和索引列一致的话,这种情况就称为全值匹配。

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';

问:WHERE子句中的几个搜索条件的顺序对查询结果有什么影响么?
答:没影响。 MySQL有一个查询优化器,会分析这些搜索条件并且按照可以使用的索引中列的顺序来决定先使用哪个搜索条件,后使用哪个搜索条件。

2. 匹配左边的列

我们的搜索语句中也可以不用包含全部联合索引中的列,只包含左边的就行

SELECT * FROM person_info WHERE name = 'Ashburn';
#或者包含多个左边的列也行
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27';

问:为什么搜索条件中必须出现左边的列才可以使用到这个 B+ 树索引呢?

#比如下边的语句就用不到这个B+树索引
SELECT * FROM person_info WHERE birthday = '1990-09-27';

答:为B+树的数据页和记录先是按照name列的值排序的,在name列的值相同的情况下才使用birthday列进行排序,也就是说name列的值不同的记录中birthday的值可能是无序的。

如果我们想使用联合索引中尽可能多的列,搜索条件中的各个列必须是联合索引中从最左边连续的列

3. 匹配列前缀

对于字符串来说,它的排序是比较规则都是逐个比较字符的大小。

也就是说这些字符串的前n个字符,也就是前缀都是排好序的,所以对于字符串类型的索引列来说,我们只匹配它的前缀也是可以快速定位记录的。

比方说我们想查询名字以 ‘As’ 开头的记录,那就可以这么写查询语句:

SELECT * FROM person_info WHERE name LIKE 'As%';

如果只给出后缀或者中间的某个字符串,MySQL 就无法快速定位记录位置了,因为字符串中间有 ‘As’ 的字符串并没有排好序,所以只能全表扫描了:

SELECT * FROM person_info WHERE name LIKE '%As%';

有时候我们有一些匹配某些字符串后缀的需求,比方说某个表有一个 url 列,该列中存储了许多url,假设已经对该 url 列创建了索引,如果我们想查询以com为后缀的网址的话可以这样写查询条件: WHERE url LIKE '%com',但是这样的话无法使用该url列的索引。我们可以把后缀查询改写成前缀查询,不过我们就得把表中的数据全部逆序存储一下,也就是说我们可以这样保存url列中的数据:在这里插入图片描述
这样查找时就可以用到索引了:

WHERE url LIKE 'moc%' 

4. 匹配范围值

索引的B+树,所有记录都是按照索引列的值从小到大的顺序排好序的,所以这极大方便我们查找索引列的值在某个范围内的记录。

SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';

由于 B+ 树中的数据页和记录是先按 name 列排序的,所以我们上边的查询过程其实是这样的:

  • 找到 name 值为 Asa 的记录。
  • 找到 name 值为 Barlow 的记录。
  • 由于所有记录都是由链表连起来的(记录之间用单链表,数据页之间用双链表),所以他们之间的记录很容易取出来。
  • 找到这些记录的主键值,再到聚簇索引中回表查找完整的记录。

在使用联合进行范围查找的时候需要注意,如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到B+树索引。

#这个查询中通过name进行范围查找的记录中可能并不是按照birthday列进行排序的,所以在搜索条件中继续以birthday列进行查找时是用不到这个B+树索引的。
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' AND birthday > '1980-01-01';

5. 精确匹配某一列并范围匹配另一列

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday > '1980-01-01' AND birthday< '2000-12-31' AND phone_number > '15100000000';
  • 对name列进行精确查找,可以使用B+树索引。
  • 由于 name 列是精确查找,所以通过 name =‘Ashburn’ 条件查找后得到的结果的name值都是相同的,它们会再按照birthday的值进行排序。所以此时对birthday列进行范围查找是可以用到B+树索引的。
  • 通过birthday的范围查找的记录birthday的值可能不同,所以这个条件无法再利用B+树索引了。

6. 用于排序

我们经常需要对查询出来的记录通过ORDER BY子句按照某种规则进行排序,一般情况下,我们只能把记录都加载到内存中,再用一些排序算法在内存中对这些记录进行排序。

#查询的结果集需要先按照name值排序,如果记录的name值相同,则需要按照birthday来排序,如果birthday的值相同,则需要按照phone_number排序。
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;

ORDER BY 的子句后边的列的顺序也必须按照索引列的顺序给出。
ORDER BY name、ORDER BY name, birthday这种匹配索引左边的列的形式可以使用部分的B+树索引。

不可以使用索引进行排序的几种情况

  1. ASC、DESC混用
    对于使用联合索引进行排序的场景,我们要求各个排序列的排序顺序是一致的,也就是要么各个列都是 ASC 规则排序,要么都是 DESC 规则排序。
  2. WHERE子句中出现了非排序使用到的索引列,那么排序依然是使用不到索引的。
  3. 用来排序的多个列不是一个索引里的,这种情况也不能使用索引进行排序。
  4. 使用索引进行排序操作,必须保证索引列是以单独列的形式出现,而不能是复杂表达式
#不能使用索引进行排序
SELECT * FROM person_info ORDER BY UPPER(name) LIMIT 10;

7. 用于分组

例如:

SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number
  1. 先把记录按照name值进行分组,所有name值相同的记录划分为一组。
  2. 将每个name值相同的分组里的记录再按照birthday的值进行分组,将birthday值相同的记录放到一个小分组里,所以看起来就像在一个大分组里又化分了好多小分组。
  3. 再将上一步中产生的小分组按照phone_number的值分成更小的分组,所以整体上看起来就像是先把记录分成一个大分组,然后把大分组分成若干个小分组 ,然后把若干个小分组再细分成更小的小分组。

如果没有索引的话,这个分组过程全部需要在内存里实现,而如果有了索引的话,恰巧这个分组顺序又和我们的B+树中的索引列的顺序是一致的,而我们的 B+ 树索引又是按照索引列排好序的,就可以直接使用B+树索引进行分组。

分组列的顺序也需要和索引列的顺序一致,也可以只使用索引列中左边的列进行分组。

三、回表的代价

需要回表的记录越多,使用二级索引的性能就越低,甚至让某些查询宁愿使用全表扫描也不使用二级索引。

问:什么时候采用全表扫描的方式,什么时候使用采用二级索引 + 回表的方式去执行查询呢?
答:查询优化器会事先对表中的记录计算一些统计数据,然后再利用这些统计数据根据查询的条件来计算一下需要回表的记录数,需要回表的记录数越多,就越倾向于使用全表扫描,反之倾向于使用二级索引 + 回表的方式。一般情况下,限制查询获取较少的记录数会让优化器更倾向于选择使用二级索引 + 回表的方式进行查询。

比如:

SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow' LIMIT 10;
#添加了 LIMIT 10 的查询更容易让优化器采用二级索引 + 回表的方式进行查询。
SELECT * FROM person_info ORDER BY name, birthday, phone_number;
#由于查询列表是 * ,所以如果使用二级索引进行排序的话,需要把排序完的二级索引记录全部进行回表操作
#这样操作的成本还不如直接遍历聚簇索引然后再进行文件排序低
#所以优化器会倾向于使用全表扫描的方式执行查询。

#如果我们加了LIMIT子句,比如:
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;
#这样需要回表的记录特别少,优化器就会倾向于使用二级索引 + 回表的方式执行查询。

为了彻底告别回表操作带来的性能损耗,建议最好在查询列表里只包含索引列。

如果我们只查询 name , birthday , phone_number 这三个索引列的值,所以在通过idx_name_birthday_phone_number 索引得到结果后就不必到聚簇索引中再查找记录的剩余列,也就是country列的值了,这样就省去了回表操作带来的性能损耗。我们把这种只需要用到索引的查询方式称为索引覆盖

不鼓励用 * 号作为查询列表,最好把需要查询的列依次标明。

四、注意事项

  • 只为用于搜索、排序或分组的列创建索引。
  • 为列的基数大的列创建索引。
  • 索引列的类型尽量小。
  • 可以只对字符串值的前缀建立索引。
  • 只有索引列在比较表达式中单独出现才可以适用索引。
  • 为了尽可能少的让聚簇索引 发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT属性。
  • 定位并删除表中的重复和冗余索引。
  • 尽量使用覆盖索引进行查询,避免 回表 带来的性能损耗。

猜你喜欢

转载自blog.csdn.net/myjess/article/details/115566452