目录
一个简单的索引方案
在上节中我们知道,为了快速定位一条记录在页中的位置而设立了页目录,那么为了快速定位记录所在的数据页,我们也可以建立一个目录,这个目录也就是索引:
- 下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值
- 给所有的页建立一个目录项,目录项包括两个部分:页的用户记录中最小的主键值和页号
在上图中,比如我们想查找主键值为20
的记录,具体查找过程分两步:
- 先从目录项中根据二分法快速确定出主键值为
20
的记录在目录项3
中(因为12 < 20 < 209
),它对应的页是页9;
- 再根据上节说的在页中查找记录的方式去
页9
中定位具体的记录。
InnoDB中的索引方案
上边之所以称为一个简易的索引方案,是因为我们为了在根据主键值进行查找时使用二分法快速定位具体的目录项而假设所有目录项都可以在物理存储器上连续存储,但实际上存储所有的目录项需要非常大的存储空间,并且我们对记录的增删容易导致牵一发而动全身。所以在InnoDB中,复用了之前存储用户记录的数据页来存储目录项,为了和用户记录做一下区分,我们把这些用来表示目录项的记录称为目录项记录。目录项记录
的record_type
值为1,并且只有主键值和页的编号两个列,另外,只有在存储目录项记录
的页中的主键值最小的目录项记录
的min_rec_mask
值为1
,其他别的记录的min_rec_mask
值都是0。
如上图所示,不论是存放用户记录的数据页,还是存放目录项记录的数据页,我们都把它们存放到B+
树这个数据结构中了。从图中可以看出,实际用户记录其实都存放在B+树的叶子节中,而目录项记录均存放在非叶子节点中。
InnoDB中规定最下边的那层为第0
层,假设所有存放用户记录的叶子节点代表的数据页可以存放100条用户记录,所有存放目录项记录的内节点代表的数据页可以存放1000条目录项记录,那么如果B+
树有2层,最多能存放1000×100=100000
条记录;如果B+
树有4层,最多能存放1000×1000×1000×100=100000000000
条记录。实际上我们的记录并不会有这么多,所以一般情况下B+树不会超过四层,也就是说通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),然后在页面内也可以通过二分法实现快速定位记录。
聚簇索引
上面所说的B+树本身就是一个索引,聚簇索引具备两个特点:
- 使用记录主键值的大小进行记录和页的排序:页内的记录是按照主键的大小顺序排成一个单向链表;每一层的页与页之间根据用户/目录项记录的主键大小顺序排成一个双向链表;
B+
树的叶子节点存储的是完整的用户记录。
在InnoDB
存储引擎中,聚簇索引
就是数据的存储方式(所有的用户记录都存储在了叶子节点
),InnoDB会自动创建聚簇索引。
二级索引
聚簇索引
只能在搜索条件是主键值时才能发挥作用,如果我们想以别的列作为搜索条件,可以为该列建一棵B+树:
- 使用记录
索引
列的大小进行记录和页的排序; B+
树的叶子节点存储的是索引列和主键值;- 目录项记录只有索引列、主键值和页号(主键值是为了保证除了页号外字段的唯一性)。
如果我们为c2列建立索引,需要查找c2
列的值为某个值
的记录,查找过程为:确定目录项记录
页 → 通过目录项记录
页确定用户记录真实所在的页 → 在真实存储用户记录的页中定位到具体的记录 → 根据主键值去聚簇索引中再查找一遍完整的用户记录(回表)。这种按照非主键列
建立的B+
树需要一次回表
操作才可以定位到完整的用户记录,所以这种B+
树也被称为二级索引。
联合索引
我们也可以同时为多个列建立索引,以多个列的大小为排序规则建立的B+树称为联合索引,以为c2
和c3
列建立索引为例:
B+
树叶子节点处的用户记录由c2
、c3
和主键c1
列组成;- 每条
目录项记录
都由c2
、c3
、页号
这三个部分组成,各条记录先按照c2
列的值进行排序,如果记录的c2
列相同,则按照c3
列的值进行排序。
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(10), birthday, phone_number)
);
- 全值匹配
全值匹配即搜索条件中的列和索引列一致,注意WHERE
子句中的几个搜索条件的顺序并不影响执行过程。
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';
- 匹配左边的列
搜索语句中可以不包含全部联合索引中的列,只包含左边的就行,比如:
SELECT * FROM person_info WHERE name = 'Ashburn';
但需要特别注意的是,如果我们想使用联合索引中尽可能多的列,搜索条件中的各个列必须是联合索引中从最左边连续的列,比如下面的这个语句就用不到B+树索引:
SELECT * FROM person_info WHERE name = 'Ashburn' AND phone_number = '15123983239';
- 匹配列前缀
对于字符串类型的索引列来说,只匹配它的前缀也是可以快速定位记录的,比如:
SELECT * FROM person_info WHERE name LIKE 'As%';
- 匹配范围值
查找索引列的值在某个范围内的记录,比如:
SELECT * FROM person_info WHERE name > 'Asa' AND name < 'Barlow';
上边语句的查询过程其实是:找到name
值为Asa
的记录 → 找到name
值为Barlow
的记录 → 由于所有记录都是由链表连起来的,他们之间的记录可以很容易地取出来 → 找到这些记录的主键值,再到聚簇索引中回表
查找完整的记录。
但如果对多个列同时进行范围查找的话,只有对索引最左边的那个列进行范围查找的时候才能用到B+
树。
- 精确匹配某一列并范围匹配另一列
对于同一个联合索引来说,虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找,则右边的列可以进行范围查找,比如:
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday > '1980-01-01' AND birthday < '2000-12-31' AND phone_number > '15100000000';
- 用于排序
如果ORDER BY
子句里使用到了索引列,就有可能省去在内存或文件中排序的步骤,比如:
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;
在联合索引中需要注意的是,ORDER BY
子句后边的列的顺序必须要按照索引列的顺序给出。
- 用于分组
有时候为了方便统计表中的一些信息,会把表中的记录按照某些列进行分组,如果分组顺序又和B+
树中的索引列的顺序是一致的,就可以直接使用B+
树索引进行分组,比如:
SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number;
回表的代价
由上面的内容我们知道,对于二级索引,如果我们需要查找用户的完整记录,需要进行回表操作。但是需要回表的记录越多,使用二级索引的性能就越低,甚至让某些查询宁愿使用全表扫描也不使用二级索引
。一般情况下,通过Limit语句来限制查询获取较少的记录数,这样可以让优化器更倾向于选择使用二级索引 + 回表
的方式进行查询。
另外,为了彻底告别回表
操作带来的性能损耗,我们最好在查询列表里只包含索引列,也就是覆盖索引。
使用索引的注意事项
- 只为用于搜索、排序或分组的列创建索引;
- 考虑列的基数(某一列中不重复数据的个数):最好为那些列的基数大的列建立索引,为基数太小列的建立索引效果可能不好;
- 索引列的类型(类型表示的数据范围的大小)尽量小:节省存储空间,加快查询效率;
- 索引字符串值的前缀;
- 让索引列在比较表达式中单独出现:如果索引列比较表达式中是以某个表达式,或者函数调用形式出现的话,是用不到索引的。
- 为了尽可能少的让
聚簇索引
发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT
属性; - 定位并删除表中的重复和冗余索引;
- 尽量使用聚簇索引进行查询,避免回表带来的性能损耗
声明:本博客纯粹为读书笔记,如想详细了解MySQL相关知识请访问《MySQL是怎么运行的:从根儿上理解MySQL》原作者撰写资料