MySQL详解之深入浅出索引
MySQL详解之深入浅出索引
MySQL有两种存储引擎:MyISAM和InnoDB。下面我们主要介绍InnoDB
类型的存储引擎。
索引的作用:提高数据查询效率。
为什么要使用索引
因为索引可以使得我们避免全表扫描去查找数据,提高查找效率。
索引的常见模型
常见的索引模型:
- 哈希表
- 有序数组
- 搜索树
哈希表
哈希表是一种以键 - 值(key-value)存储数据的结构,我们只要输入待查找的值即 key,就可以找到其对应的值即 Value。哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。
如图:
缺点:
- 仅仅能满足“=”,“IN”,不能使用范围查询
- 无法被用来避免数据的排序操作
- 不能利用部分索引建查询
- 不能避免表扫描
- 遇到大量Hash值相等的情况后性能并不一定就会比B+ Tree索引高
哈希表这种结构在新增时会很快,但是因为不是有序的,所以哈希索引做区间查询的速度是很慢的。所以适用于只有等值查询的场景,比如Redis以及其他一些NoSQL引擎。
有序数组
这种索引结构支持范围查询,因为它是根据索引递增顺序保存的,如果我们要查找其中的某个值,根据二分查找可以很快的得到,时间复杂度是O(log(N))。同时在做区间查询的时候,查询效率也是很优秀的。如果仅仅看查询效率,有序数据就是最好的数据结构了。但是,在需要更新数据的时候就麻烦了,因为在向中间插入一条记录时,需要挪动后面的所有记录,成本太高了。
所以有序数组在等值查询和范围查询场景中的性能就都非常优秀。
搜索树
这里先简单介绍一下B Tree和B+ Tree。
B Tree
如图所示:
定义:
- 根节点至少包括两个孩子
- 树中每个节点最多含有m个孩子(m>=2)
- 除根节点和叶节点外,其他每个节点至少有ceil(m/2)个孩子(ceil表示向上取整)
- 所有叶子节点都位于同一层
PS:图中第二行第二个节点的叶子节点下面只有一个子节点,错误!
B+ Tree
B+树是B树的辩题,其定义基本与B树相同,除了:
- 非叶子节点的子树指针与关键字个数相同
- 非叶子节点的子树指针P[i],指向关键字值[K[i],K[i+1])的子树
- 非叶子节点仅用来索引,数据都保存在叶子节点中
- 所有叶子节点均有一个链指针指向下一个叶子节点
经过比较,可以得知:B+ Tree更适合用来存储索引
- B+树的磁盘读写代码更低
- B+树的查询效率更加稳定(O(log(n)))
- B+树更有利于对数据库的扫描
这也是为什么现在数据库主要选择B+树做索引的原因。
二叉树的搜索效率是最高的,但是实际上大多数的数据库存储并不使用二叉树,其原因是,索引不止存在内存中,还要写到磁盘上。
BitMap
位图索引,这种索引很少数据库支持,oracle是支持这种索引的,它一般适用于该字段的值就是几个值的情况,比如性别,颜色等等。只做了解。
InnoDB的索引模型
在InnoDB
中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表
。又因为前面我们提到的,InnoDB
使用了B+
树索引模型,所以数据都是存储在B+
树中的。每一个索引在InnoDB里面对应一棵B+树。
根据叶子节点的内容,索引类型分为:
- 主键索引
- 非主键索引
主键索引的叶子节点存的是整行数据。在InnoDB
里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在InnoDB
里,非主键索引也被称为二级索引(secondary index)。
基于主键索引和普通索引的查询有什么区别?
基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
我们讨论下面一个案例:
分析一下哪些场景下建表时需要使用自增主键,哪些场景下不应该?
- 使用自增主键:
自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
- 使用业务逻辑字段做主键:
往往不容易保证有序插入,这样写数据成本相对较高。
- 另外我们从存储空间的角度来看
假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?
由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间就越小。
所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。
B+ 树能够很好地配合磁盘的读写特性,减少单次查询的磁盘访问次数。
重建索引
重建普通索引可以达到省空间的目的;但是如果重建主键索引,则相当于整个表重建,因为InnoDB是一种索引组织表,也就是说表是根据主键顺序以索引形式存放的。
重建表可以使用以下语句:alter table T engine=InnoDB
,其中T为表名。
覆盖索引
我们先看下面这个例子:
有一个用户表User
,初始化语句如下:
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `age_inx` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `user` VALUES (1, '张三', 10);
INSERT INTO `user` VALUES (2, '李四', 30);
INSERT INTO `user` VALUES (3, '王五', 35);
INSERT INTO `user` VALUES (4, '赵六', 40);
INSERT INTO `user` VALUES (5, '小张', 45);
主键为:id,在字段age上新建索引为age_inx
。
那么如果执行select * from user where age between 30 and 40
会执行几次树的搜索,会扫描多少行?
现在我们一起来看看这条SQL查询语句的执行流程:
1.在
age
索引树上找到age=30的记录,取得id=2;
2.再到id索引树查到id=2对应的记录;
3.在age
索引树上找到age=35的记录,取得id=3;
4.再到id索引树查到id=3对应的记录;
5.在age
索引树上找到age=40的记录,取得id=4;
6.再到id索引树查到id=4对应的记录;
7.在age
索引树取下一个值age=45,不满足条件,循环结束。
在这个过程中,回到主键索引树搜索的过程,我们成为回表
。在这个例子中,由于查询结果所需要的数据只在主键索引上有,所以不得不回表。这个时候我们可以对语句做如下优化:select id from user
,这时只需要查id的值,而id的值已经在age索引树上了。因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引age已经覆盖了
我们的查询需求,我们成为覆盖索引
。
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
最左前缀原则
B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。
基于上面对最左前缀索引的说明,我们来讨论一个问题:在建立联合索引的时候,如何安排索引内的字段顺序。
索引的复用能力
,因为可以支持最左前缀,所以当已经有了 (a,b) 这个联合索引后,一般就不需要单独在 a 上建立索引了。因此,第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
最左前缀可以用于在索引中定位记录。
索引下推
在MySQL5.6之前,只能一个个回表到主键索引上找出数据行,再对比字段值
在MySQL5.6引入的索引下推优化,可以在索引遍历过程中,对索引中包含的字段优先做判断,直接过滤掉不满足条件的记录,减少回表次数。
索引是建立得越多越好吗?
- 数据量小的表不需要建立索引,建立会增加额外的索引开销
- 数据变更需要维护索引,因为更多的索引意味着更多的维护成本
- 更多的索引意味着也需要更多的空间
总结
在满足语句需求的情况下, 尽量少地访问资源是数据库设计的重要原则之一。我们在使用数据库的时候,尤其是在设计表结构时,也要以减少资源消耗作为目标。