1. 索引的代价
1.1 空间上的代价
每建立一个索引都要为它建立一棵B+
树,每一棵B+
树的每一个节点都是一个数据页,一个页默认会占用16KB
的存储空间,一棵很大的B+
树由许多数据页组成
1.2 时间上的代价
每次对表中的数据进行增、删、改操作时,都需要去修改各个B+
树索引。
B+
树每层节点都是按照索引列的值从小到大的顺序排序而组成了双向链表。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位,页面分裂、页面回收啥的操作来维护好节点和记录的排序。
2. B+树索引适用的条件
2.1 全值匹配
搜索条件中的列和索引列一致的话,这种情况就称为全值匹配
<aside> 💡 即使搜索列的顺序和建联合索引时的顺序不一致也不影响,因为MySQL有查询优化器,会分析这些搜索条件并且按照可以使用的索引中列的顺序来决定先使用哪个搜索条件,后使用哪个搜索条件
</aside>
2.2 匹配左边的列
比如有a\b\c三个列建立的联合索引,使用 where a = ?
或 where a = ? and b = ?
或 where a = ? and b = ? and c = ?
这三种方式都可以使用到这个联合所以来查询。
为什么只能从左边匹配
因为B+
树的数据页和记录先是按照a
列的值排序的,在a
列的值相同的情况下才使用B列进行排序,也就是说a
列的值不同的记录中b
的值可能是无序的。
<aside> ⚠️ 如果在abc三列所作联合所以的情况下,使用 a
和 c
作为条件查询,掠过了 b
条件,这种情况下只能在 a
条件使用索引查询,而其中的 c
不会使用索引查询,因为:a
值相同的记录按照 b
列进行排序,而只有 b
相同才会使用 c
列进行排序
</aside>
2.3 匹配列前缀
SELECT *
FROM person_info
WHERE name like '张%';
如果name列有索引,以上SQL,根据匹配列前缀的原则同样会走索引。
因为一个排好序的字符串列其实有这样的特点:
- 先按照字符串的第一个字符进行排序。
- 如果第一个字符相同再按照第二个字符进行排序。
- 如果第二个字符相同再按照第三个字符进行排序,依此类推。
2.4 匹配范围值
B+树所有记录都是按照索引列的值从小到大进行排序,所以极大的方便查找索引列的值在某个范围内的记录。
<aside> ⚠️ 联合索引只有最左侧的值才能用到B+树索引
</aside>
2.5 精确匹配某一列并范围匹配另外一列
对于同一个联合索引来说,虽然对多个列都进行范围查找时只能用到最左边那个索引列,但是如果左边的列是精确查找,则右边的列可以进行范围查找
2.6 用于排序
索引本身就是一个排好序的结构,使用索引列排序可以避免文件排序,文件排序非常慢
<aside> 👉 有的时候可能查询的结果集太大以至于不能在内存中进行排序的话,还可能暂时借助磁盘的空间来存放中间结果,排序操作完成后再把排好序的结果集返回到客户端
</aside>
2.6.1 使用联合索引进行排序注意事项
<aside> ⚠️ ORDER BY
的子句后边的列的顺序也必须按照索引列的顺序给出,如果给出ORDER BY b, c, a
的顺序,那也是用不了B+
树索引
</aside>
2.6.2 不可以使用索引进行排序的几种情况
- ASC、DESC 混用
- 排序列包含非同一个索引的列
- 排序列使用了复杂的表达式
2.7 用于分组
SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday,
phone_number
这个查询语句相当于做了3次分组操作:
- 先把记录按照
name
值进行分组,所有name
值相同的记录划分为一组。 - 将每个
name
值相同的分组里的记录再按照birthday
的值进行分组,将birthday
值相同的记录放到一个小分组里,所以看起来就像在一个大分组里又化分了好多小分组。 - 再将上一步中产生的小分组按照
phone_number
的值分成更小的分组,所以整体上看起来就像是先把记录分成一个大分组,然后把大分组
分成若干个小分组
,然后把若干个小分组
再细分成更多的小小分组
。
然后针对那些小小分组
进行统计,比如在我们这个查询语句中就是统计每个小小分组
包含的记录条数。如果没有索引的话,这个分组过程全部需要在内存里实现,而如果有了索引的话,恰巧这个分组顺序又和我们的B+
树中的索引列的顺序是一致的,而我们的B+
树索引又是按照索引列排好序的,这不正好么,所以可以直接使用B+
树索引进行分组。
和使用B+
树索引进行排序是一个道理,分组列的顺序也需要和索引列的顺序一致,也可以只使用索引列中左边的列进行分组。
3. 回表的代价
在使用二级索引进行范围查询,所查询的所有记录都需要根据主键id进行回表,这是相当耗费性能的。二级索引做范围查询的时候采用的是顺序IO,而通过范围查询结果的主键id并不是顺序的,所以根据这些并不连续的id
值到聚簇索引中访问完整的用户记录可能分布在不同的数据页中,这样读取完整的用户记录可能要访问更多的数据页,这种读取方式我们也可以称为随机I/O
。
需要回表的记录越多,使用二级索引的性能就越低
3.1 覆盖索引
通过查询结果列为索引列的方式查询称之为覆盖索引,这种方式不需要回表的,比如:
select a,b,c from tablex where a = ? and b = ? and c = ?
4. 如何挑选索引
4.1 只为用于搜索、排序或分组的列创建索引
只为出现在WHERE
子句中的列、连接子句中的连接列,或者出现在ORDER BY
或GROUP BY
子句中的列创建索引。
4.2 考虑列的基数
列的基数
指的是某一列中不重复数据的个数,比方说某个列包含值2, 5, 8, 2, 5, 8, 2, 5, 8
,虽然有9
条记录,但该列的基数却是3
。也就是说,在记录行数一定的情况下,列的基数越大,该列中的值越分散,列的基数越小,该列中的值越集中。这个列的基数
指标非常重要,直接影响我们是否能有效的利用索引。
<aside> 💡 最好为那些列的基数大的列建立索引,为基数太小列的建立索引效果可能不好。
</aside>
4.3 索引列的类型尽量小
比如我们能使用INT
就不要使用BIGINT
,能使用MEDIUMINT
就不要使用INT
~ 这是因为:
- 数据类型越小,在查询时进行的比较操作越快
- 数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘
I/O
带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。
4.4 索引字符串值的前缀
使用很长的字符串做索引列的问题:
-
B+
树索引中的记录需要把该列的完整字符串存储起来,而且字符串越长,在索引中占用的存储空间越大。 - 如果
B+
树索引中索引列存储的字符串很长,那在做字符串比较时会占用更多的时间。
为了解决以上问题,我们可以使用字符串列的前n个字符作为索引值,比如:
CREATE TABLE person_info(
name VARCHAR(100) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country varchar(100) NOT NULL,
KEY idx_name_birthday_phone_number (name(10), birthday, phone_number)
);
在查找记录时虽然不能精确的定位到记录的位置,但是能定位到相应前缀所在的位置,然后根据前缀相同的记录的主键值回表查询完整的字符串值,再对比就好了。这样只在B+
树中存储字符串的前几个字符的编码,既节约空间,又减少了字符串的比较时间,还大概能解决排序的问题
4.4.1 索引列前缀对排序的影响
字符串前缀作为索引列对排序有很大的影响,因为无法对完整的字符串进行排序,也就是使用索引列前缀的方式无法使用索引排序,这种情况会全表扫描,使用的是文件排序。
4.5 让索引列在比较表达式中单独出现
举个例子:
WHERE my_col * 2 < 4
这个情况的条件语句无法走索引,因为my_col并不是以单独列的形式出现的。
WHERE my_col < 4/2
而这种方式是可以的
4.6 主键插入顺序
如果主键插入的顺序不是递增的,会出现页面分裂和记录移位,这就意味着性能损耗。
<aside> 💡 如果主键插入的顺序不是递增的,会出现页面分裂和记录移位,这就意味着性能损耗。
</aside>
5. 总结
-
B+
树索引在空间和时间上都有代价,所以没事儿别瞎建索引。 B+
树索引适用于下边这些情况:
- 全值匹配
- 匹配左边的列
- 匹配范围值
- 精确匹配某一列并范围匹配另外一列
- 用于排序
- 用于分组
- 在使用索引时需要注意下边这些事项:
- 只为用于搜索、排序或分组的列创建索引
- 为列的基数大的列创建索引
- 索引列的类型尽量小
- 可以只对字符串值的前缀建立索引
- 只有索引列在比较表达式中单独出现才可以适用索引
- 为了尽可能少的让
聚簇索引
发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT
属性。 - 定位并删除表中的重复和冗余索引
- 尽量使用
覆盖索引
进行查询,避免回表
带来的性能损耗。