MYSQL B+树索引实战

索引的代价

空间上的代价

innerdb引擎下,一个索引都对应一颗B+树,树中每个节点都是一个数据页,一个页默认大小为16K的存储空间,所以一个索引也会占用磁盘空间的。

时间上的代价

索引是对数据的排序,那么当对表中的数据进行增、删、改操作时,都需要去维护修改内容涉及到的B+树索引。所以在进行增、删、改操作时可能需要额外的时间进行一些记录移动、数据页分裂、数据页回收等操作来维护好排序。
因此索引不一定是越多越好,如果建了许多索引,当个对数据操作时,都需要消耗一定的时间处理对应的索引,有时候可能会适得其反。

B+树索引实战(一)

准备表和数据

表和索引:
create table t1(a int PRIMARY KEY,b int,c int,d int ,e VARCHAR(20)) ENGINE=INNODB;
create INDEX idx_t1_bcd on t1(b,c,d);

准备表数据:
INSERT INTO `test_db`.`t1` (`a`, `b`, `c`, `d`, `e`) VALUES ('1', '1', '1', '1', 'A');
INSERT INTO `test_db`.`t1` (`a`, `b`, `c`, `d`, `e`) VALUES ('2', '2', '2', '2', 'B');
INSERT INTO `test_db`.`t1` (`a`, `b`, `c`, `d`, `e`) VALUES ('3', '3', '3', '3', 'C');
INSERT INTO `test_db`.`t1` (`a`, `b`, `c`, `d`, `e`) VALUES ('4', '3', '1', '1', 'D');
INSERT INTO `test_db`.`t1` (`a`, `b`, `c`, `d`, `e`) VALUES ('5', '2', '3', '5', 'E');
INSERT INTO `test_db`.`t1` (`a`, `b`, `c`, `d`, `e`) VALUES ('6', '6', '4', '4', 'F');
INSERT INTO `test_db`.`t1` (`a`, `b`, `c`, `d`, `e`) VALUES ('7', '7', '5', '5', 'G');
INSERT INTO `test_db`.`t1` (`a`, `b`, `c`, `d`, `e`) VALUES ('8', '8', '8', '8', 'H');

全值匹配

查询的条件和索引中列顺序一致,这种情况成为全值索引,如下:

select *  from t1 where a=1 and b=1 and c=1;

查询优化器,会分析查询条件并按照可使用的索引中列的顺序决定先使用哪个条件查询。

最左匹配原则(匹配左边的列)

联合索引构建的也是一个B+树,只不过每一个索引节点中存储的是构成联合索引的多个列的值。MySQL会优先根据索引中最左列的值来构建索引B+树,当该列值相同时,会再根据后一个列的值排序,依次重复下去,知道构建完整个树。
一下sql可以使用索引:
select * from t1 where b=1;
select * from t1 where b=1 and c =1;
因为以上sql匹配联合索引的列顺序,下面这个sql就无法使用索引:
select * from t1 where c=1;
因为联合索引是优先根据a列排序,在b列相同时,才会根据c列排序。但是当b 列不相同时,c列很可能是乱序的,因此跳过b直接根据c列查询是无法使用索引的。

匹配列前缀

如果查询条件中使用xxx%,这种开头固定的模糊匹配查询,也可以使用索引,只不过在匹配索引条件时,因为索引树是以及排序好的,只比较前面确定的内容的长度也是可以使用索引的,如果我们为e列添加索引:

select * from t1 where e like 'aaa%';

但是如果只给出中间,或者后缀的某些字符串,是无法使用索引的,如:

select * from t1 where e like '%aaa%';

因为查询条件的开头不明确,因为无法匹配排序,只能够权标扫描。
但是如果有些数据结尾时确定的,可以将数据内容反序,然后再存储数据,此时相当于前缀是确定,因此可以使用索引,比如 url:
www.baidu.com
www.qq.com
当反序后:
moc.udiab.www
moc.qq.www
像这种都是以com结尾的url,反序后可以使用索引。

匹配范围值

 select  * from t1 where b>1 and b<2000;

由于B+是从左到右,从大到小依次排序好的,所以上面的sql查询过程其实是这样的:
1、根据索引找到最左侧b=1的数据节点
2、根据索引找到最最侧值为2000的节点
3、由于所有的记录都是有链表连起来的(索引记录节点直接使用单项链表,数据节点之间使用双向链表),索引他们之间的记录都很容易取到
4、找得到这些记录的主键值,再到聚簇索引(主键索引)中回表查找到完整的记录。

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

select * from t1 where b > 1 and c > 1;

上边这个查询可以分成两个部分:
1、通过条件b > 1来对b进行范围,查找的结果可能有多条b值不同的记录,
2. 对这些b值不同的记录继续通过c > 1继续过滤。
这样子对于联合索引来说,只能用到b列的部分,而用不到c列的部分,因为只有b值相同的情况下才能用c列的值进行排序,而这个查询中通过b进行范围查找的记录中可能并不是按照c列进行排序的,所以在搜索条件中继续以c列进行查找时是用不到这个B+树索引的。

精确匹配最左列,并范围匹配另外一列

对于同一个联合索引来说,虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找,则右边的列可以进行范围查找,比方说这样:

select * from t1 where b=1 and c>10;

排序

select * from t1 order by b, c, d;

这个查询的结果集需要先按照b值排序,如果记录的b值相同,则需要按照c来排序,如果c的值相同,则需要按照d排序。因为这个B+树索引本身就是按照上述规则排好序的,所以直接从索引中提取数据,然后进行回表操作取出该索引中包含的列就好了;

分组

select b, c, d, count(*) from t1 group by b, c, d;

这个查询语句相当于做了3次分组操作:
1. 先把记录按照b值进行分组,所有b值相同的记录划分为一组。
2. 将每个b值相同的分组里的记录再按照c的值进行分组,将title值相同的记录放到一个分组里。
3. 再将上一步中产生的分组按照d的值分成更小的分组。

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

使用联合索引对数据进行排期和分组注意事项

对于联合索引有个一点需要注意,ORDER BY的子句后边的列的顺序也必须按照索引列的顺序给出,如果给出order by c, b, d 的顺序,那也是用不了B+树索引。
同理, order by b҅order by b, c 这种匹配索引左边的列的形式可以使用部分的B+树索引。当联合索引左边列的值为常量,也可以使用后边的列进行排序,比如这样:

select * from t1 where b = 1 order by c, d;

这个查询能使用联合索引进行排序是因为b列的值相同的记录是按照c, d排序的。

不可以是用联合索引排序和分组的情况

ASC和DESC混合使用

对于使用联合索引进行排序的场景,我们要求各个排序列的排序顺序是一致的,也就是要么各个列都是ASC规则排序,要么都是DESC规则排序
注意:ORDER BY子句后的列如果不加ASC或者DESC默认是按照ASC排序规则排序的,也就是升序排序的。

select * from t1 order by b ASC, c DESC;

这个sql是无法使用索引的

如何建立索引

根据索引选择性

索引的选择性(Selectivity),是指不重复的索引值数量(也叫基数,Cardinality)与表记录数的比值:
选择性 = 基数 / 记录数
选择性的取值范围为(0, 1],选择性越高的索引价值越大。如果选择性等于1,就代表这个列的不重复值和表记录数是一样的,那么对这个列建立索引是非常合适的,如果选择性非常小,那么就代表这个列的重复值是很多的,不适合建立索引。

考虑前缀索引

用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时因为索引key变短而减少了索引文件的大小和维护开销。
employees表只有一个索引,那么如果我们想按名字搜索一个人,就只能全表扫描了:

EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';

那么可以对或建立索引,看下两个索引的选择性:

SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees;  -- 0.0042
SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM  employees.employees;  -- 0.9313

显然选择性太低,选择性很好,但是first_name和last_name加起来长度为30,有没有兼顾长度和选择性的办法?可以考虑用first_name和last_name的前几个字符建立索引,例如,看看其选择性:

SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees; -- 0.7879

选择性还不错,但离0.9313还是有点距离,那么把last_name前缀加到4:

SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees; -- 0.9007

这时选择性已经很理想了,而这个索引的长度只有18,比短了接近一半,建立前缀索引的方式为:

ALTER TABLE employees.employees ADD INDEX `first_name_last_name4` (first_name, last_name(4));

前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于覆盖索引。

总结

1、索引列的类型尽量小:减少索引本身存储的开销。
2、利用索引字符串值的前缀:减少索引长度,减少索引本身开销,同时索引选择性基本接近,适当情况可以考虑使用。
3、主键自增:在数据插入时直接在索引B+树后追加,减少查询,比较的定位插入位置的开销。
4、定位并删除表中的重复和冗余索引:索引中的每个节点索引值都是唯一、不重复的,否则B+无法进行构建或者查询。如A,B相等,使用哪个?,但是实际上,对非主键索引而言,MySQL会在索引列后面加上主键索引值,避免重复的情况出现。这样其实还是增加了额外的开销。
5、尽量使用覆盖索引进行查询,避免回表带来的性能损耗:

你可能感兴趣的:(性能调优)