2. Mysql 索引
本篇学习总结是参考自小林的mysql文档资料。
![img](https://i-blog.csdnimg.cn/blog_migrate/2c468a9afeb3060875c8901c888c58bf.png)
索引只能定位到页,再通过页里的页目录去定位到某条具体的记录。
innodb 索引页长这样:
- 每个节点都是一个数据页
- 只有叶子节点(最底层的节点)才存放了数据,非叶子节点(其他上层节)仅用来存放目录项作为索引。
- 所有节点按照索引键大小排序,构成一个双向链表,便于范围查询
我们再看看 B+ 树如何实现快速查找主键为 6 的记录,以上图为例子:
- 从根节点开始,通过二分法快速定位到符合页内范围包含查询值的页,因为查询的主键值为 6,在[1, 7)范围之间,所以到页 30 中查找更详细的目录项;
- 在非叶子节点(页30)中,继续通过二分法快速定位到符合页内范围包含查询值的页,主键值大于 5,所以就到叶子节点(页16)查找记录;
- 接着,在叶子节点(页16)中,通过槽查找记录时,使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到主键为 6 的记录。
这次主要介绍了索引的原理、分类和使用。我把重点总结在了下面这个表格
![](https://i-blog.csdnimg.cn/blog_migrate/2d564f4939f2f8c307b730e6553c6730.png)
2.1 什么是索引?
数据库中,索引的定义就是帮助存储引擎快速获取数据的一种数据结构,形象的说就是索引是数据的目录。
所谓的存储引擎,说白了就是如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法。
下图是 MySQL 的结构图,索引和数据就是位于存储引擎中:
mysql 创建索引的方式:(132条消息) MySQL几种创建索引的方式_mysql怎么创建索引_不会秃头的小齐的博客-CSDN博客
key 和 index 关键字都可以用来创建索引。
2.2 索引的分类
我们可以按照四个角度来分类索引。
- 按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。
- 按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
- 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
- 按「字段个数」分类:单列索引、联合索引。
接下来,按照这些角度来说说各类索引的特点。
1 按数据结构分类(B+Tree 重点)
从数据结构的角度来看,MySQL 常见索引有 B+Tree 索引、HASH 索引、Full-Text 索引。每一种存储引擎支持的索引类型不一定相同,B+Tree 索引类型(InnDB 引擎支持)是 MySQL 存储引擎采用最多的索引类型。
因为表的数据都是存放在聚集索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚集索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个。
InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引:
- 如果有主键,默认会使用主键作为聚簇索引的索引键;
- 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
- 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键;
其它索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的是 B+Tree 索引。
B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。
假设有一个商品表如下,其中id为主键:
![img](https://cdn.xiaolincoding.com//mysql/other/824c43b801c64e81acb0a9b042d50311.png)
主键索引的 B+Tree 如图所示(图中叶子节点之间是单向链表,但是实际上是双向链表):
![主键索引 B+Tree](https://i-blog.csdnimg.cn/blog_migrate/6cd22da9b23166ba469763aca5abe3ac.png)
B+Tree 的每一个节点就是一个磁盘块(可不可以理解为一个 B+Tree叶子节点就是一个数据页?),访问一个节点就是一次磁盘 I/O 操作。
通过主键查询商品数据的过程
主键索引的 B+Tree 如上图所示,查询语句 select * from product where id= 5;
的执行过程要记住,见原文。
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。
B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。
通过二级索引查询商品数据的过程
主键索引的 B+Tree 和二级索引的 B+Tree 区别如下:
- 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
- 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。
我这里将前面的商品表中的 product_no (商品编码)字段设置为二级索引,那么二级索引的 B+Tree 如下图(图中叶子节点之间我画了单向链表,但是实际上是双向链表):
![二级索引 B+Tree](https://i-blog.csdnimg.cn/blog_migrate/dac53b996c65ae2c41a5e6051b36e14d.png)
其中非叶子的 key 值是 product_no(图中橙色部分),叶子节点存储的数据是主键值(图中绿色部分)。
如果我用 product_no 二级索引查询商品,如下查询语句(记住如何查询的,忘了就见原文):
select * from product where product_no = '0002';
二级索引两个概念:回表 和 覆盖索引,见原文。
为什么 MySQL InnoDB 选择 B+tree 作为索引的数据结构?
-
B+Tree vs B Tree:① B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。② 另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。
-
B+Tree VS 二叉树:即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右(因为允许的最大子节点通常 > 100),也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。而二叉树的每个父节点的儿子节点个数只能是 2 个,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。
对于有 N 个叶子节点的 B+Tree,其搜索复杂度为
O(logdN)
,其中 d 表示节点允许的最大子节点个数为 d 个。二叉树搜索复杂度为O(logN)
-
B+Tree vs Hash:Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因。
2 按物理存储分类
从物理存储的角度来看,索引分为聚簇索引(主键索引)、二级索引(辅助索引)。
这两个区别在上一节也提到了:
- 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
- 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。
3 按字段特性分类
从字段特性的角度来看,索引分为主键索引、唯一索引、普通索引、前缀索引。
-
主键索引:主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。在创建表时,创建主键索引的方式如下:
CREATE TABLE table_name ( .... PRIMARY KEY (index_column_1) USING BTREE );
-
唯一索引:唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。在创建表时,创建唯一索引的方式如下:
CREATE TABLE table_name ( .... UNIQUE KEY(index_column_1, index_column_2, ...) );
建表后,如果要创建唯一索引,可以使用这面这条命令:
CREATE UNIQUE INDEX index_name ON table_name(index_column_1, index_column_2, ...);
-
普通索引:普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。在创建表时,创建普通索引的方式如下:
CREATE TABLE table_name ( .... INDEX(index_column_1, index_column_2, ...) );
建表后,如果要创建普通索引,可以使用这面这条命令:
CREATE INDEX index_name ON table_name(index_column_1,index_column_2,...);
-
前缀索引:就是对文本的前几个字符建立索引
这里说明一下主键索引和唯一索引在创建表时自动创建:
按照如下方式创建表:
create table community.test( id int primary key, username varchar(50), uni varchar(50) unique )
创建好后,会自动给 id 创建聚簇索引,给 uni 字段创建唯一索引,并且索引名字分别是 PRIMARY 和 uni。另外的创建方式就是上面写的那些。实际上 mysql 会对上面创建表的代码进行优化,优化后真正生成表的代码如下:
CREATE TABLE `test` ( `id` int NOT NULL, `username` varchar(50) DEFAULT NULL, `uni` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uni` (`uni`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
4 按字段个数分类
从字段个数的角度来看,索引分为单列索引、联合索引(复合索引)。
- 建立在单列上的索引称为单列索引,比如主键索引;
- 建立在多列上的索引称为联合索引;
联合索引
通过将多个字段组合成一个索引,该索引就被称为联合索引。
比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name)
,创建联合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
![](https://i-blog.csdnimg.cn/blog_migrate/453471d06f6f511e603073eabd408e90.png)
叶子节点绿色部分是 主键值。
了解最左匹配规则,符合该规则的查询sql就可以匹配上联合索引,不符合的该规则的查询sql就不能匹配上联合索引。
问题1:
select * from t_table where a > 1 and b = 2
,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?
- a 字段使用了 > 进行范围查询,联合索引的最左匹配原则在遇到 a 字段的范围查询( >)后就停止匹配了,因此 b 字段并没有使用到联合索引。因为只有 a = 某个定值的时候,b 才是有序的。
问题2:
select * from t_table where a >= 1 and b = 2
,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?
- 这条查询语句 a 和 b 字段都用到了联合索引进行索引查询。定位的第一条数据是 a=1 和 b=2,而不是问题1中的只有 a > 1,当然前提是 a=1 存在。
问题3:
SELECT * FROM t_table WHERE a BETWEEN 2 AND 8 AND b = 2
,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?
- 在 MySQL 中,BETWEEN 包含了 value1 和 value2 边界值,类似于 >= and =<。所以这道题与第二个问题类似。
问题4:
SELECT * FROM t_user WHERE name like 'j%' and age = 22
,联合索引(name, age)哪一个字段用到了联合索引的 B+Tree?
因为可能存在 name=j,这种情况,name=j的范围里,age就是有序的,所以name和age都用到了联合索引,定位的第一条数据是 name=j && age =22,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-426ODG1f-1682266245850)(D:\tools\Typora\插入的图片\typora-user-images\image-20230325225413306.png)]
索引下推
对于联合索引(a, b),在执行 select * from table where a > 1 and b = 2
语句的时候,只有 a 字段能用到索引,那在联合索引的 B+Tree 找到第一个满足条件的主键值(ID 为 2)后,还需要判断其他条件是否满足(看 b 是否等于 2),那是在联合索引里判断?还是回主键索引去判断呢?
- MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断(本案例中就是对 b=2进行判断),直接过滤掉不满足条件的记录,减少回表次数。MYSQL 5.6之前是会对联合索引查出来的主键进行回表,然后再主键索引中查到数据,再对 b=2 进行判断。
当你的查询语句的执行计划里,出现了 Extra 为 Using index condition
,那么说明使用了索引下推的优化。
索引区分度
建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。区分度就是某个字段 column 不同值的个数「除以」表的总行数,计算公式如下:
![区分度计算公式](https://i-blog.csdnimg.cn/blog_migrate/34967c1d775620a8fa4b421c64311030.png)
联合索引进行排序
这里出一个题目,针对针对下面这条 SQL,你怎么通过索引来提高查询效率呢?
select * from order where status = 1 order by create_time asc
有的同学会认为,单独给 status 建立一个索引就可以了。
但是更好的方式给 status 和 create_time 列建立一个联合索引,因为这样可以避免 MySQL 数据库发生文件排序。因为在查询时,如果只用到 status 的索引,但是这条语句还要对 create_time 排序,这时就要用文件排序 filesort,也就是在 SQL 执行计划中,Extra 列会出现 Using filesort。
所以,要利用索引的有序性,在 status 和 create_time 列建立联合索引,这样根据 status 筛选后的数据就是按照 create_time 排好序的,避免再文件排序,提高了查询效率
2.3 什么时候需要 / 不需要创建索引?
索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:
- 需要占用物理空间,数量越大,占用空间越大;
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;
- 会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。
所以,索引不是万能钥匙,它也是根据场景来使用的。
关键词:空间成本、时间成本、维护成本
1 什么时候适用索引?
- 字段有唯一性限制的。比如商品编码;
- 经常用于
WHERE
查询条件的字段。这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。 - 经常用于
GROUP BY
和ORDER BY
的字段。这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。
2 什么时候不需要创建索引?
WHERE
条件,GROUP BY
,ORDER BY
里用不到的字段。索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。- 字段中存在大量重复数据,不需要创建索引。比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。
- 表数据太少的时候,不需要创建索引;
- 经常更新的字段不用创建索引。比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。
2.4 有什么优化索引的方法?(重点需记忆)
这里说一下几种常见优化索引的方法:
- 前缀索引优化;
- 覆盖索引优化;
- 主键索引最好是自增的;
- 防止索引失效;
1 前缀索引优化
前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引。使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。
不过,前缀索引有一定的局限性,例如:
- order by 就无法使用前缀索引;
- 无法把前缀索引用作覆盖索引;
2 覆盖索引优化
覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。
假设我们只需要查询商品的名称、价格,有什么方式可以避免回表呢?
我们可以建立一个联合索引,即「商品ID、名称、价格」作为一个联合索引。如果索引中存在这些数据,查询将不会再次检索主键索引,从而避免回表。
所以,使用覆盖索引的好处就是,不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。
3 主键索引最好是自增的
InnoDB 创建主键索引默认为聚簇索引,数据被存放在了 B+Tree 的叶子节点上。也就是说,同一个叶子节点内的各个数据是按主键顺序存放的,因此,每当有一条新的数据插入时,数据库会根据主键将其插入到对应的叶子节点中。
如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。
如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率。
另外,主键字段的长度不要太大,因为主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小。
4 索引最好设置为 NOT NULL
为了更好的利用索引,索引列要设置为 NOT NULL 约束。有两个原因:
- 第一原因:索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可为 NULL 的列会使索引、索引统计和值比较都更复杂,比如进行索引统计时,count 会省略值为NULL 的行。
- 第二个原因:NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么 行格式 中至少会用 1 字节空间存储 NULL 值列表,如下图的紫色部分:
5 防止索引失效
用上了索引并不意味着查询的时候会使用到索引,所以我们心里要清楚有哪些情况会导致索引失效,从而避免写出索引失效的查询语句,否则这样的查询效率是很低的。
这里简单说一下,发生索引失效的情况:
- 当我们使用左或者左右模糊匹配的时候,也就是
like %xx
或者like %xx%
这两种方式都会造成索引失效; - 当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;
- 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
- 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
我上面说的是常见的索引失效场景,实际过程中,可能会出现其他的索引失效场景,这时我们就需要查看执行计划,通过执行计划显示的数据判断查询语句是否使用了索引。
如下图,就是一个没有使用索引,并且是一个全表扫描的查询语句。
对于执行计划,参数有:
- possible_keys 字段表示可能用到的索引;
- key 字段表示实际用的索引,如果这一项为 NULL,说明没有使用索引;
- key_len 表示索引的长度;
- rows 表示扫描的数据行数。
- type 表示数据扫描类型,我们需要重点看这个。
type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:
- All(全表扫描);
- index(全索引扫描);
- range(索引范围扫描);
- ref(非唯一索引扫描);
- eq_ref(唯一索引扫描);
- const(结果只有一条的主键或唯一索引扫描)。
看原文,弄清楚上面 几个类型的场景
除了关注 type,我们也要关注 extra 显示的结果。这里说几个重要的参考指标:
-
Using filesort :当查询语句中包含 group by 操作,而且无法利用索引完成排序操作的时候, 这时不得不选择相应的排序算法进行,甚至可能会通过文件排序,效率是很低的,所以要避免这种问题的出现。
-
Using temporary:使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。
-
Using index:表示使用了覆盖索引,不需要回表操作。
Using where:表示需要通过回表去查询数据,或者不使用索引但使用了where也会出现。
Using index condition:表示查询优化器选择使用了索引下推这个特性。
2.5 补充知识
1 哪种 count 性能最好?
![图片](https://cdn.xiaolincoding.com//mysql/other/af711033aa3423330d3a4bc6baeb9532.png)
2 count() 是什么?
count() 是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意表达式,该函数作用是统计符合查询条件的记录中,函数指定的参数不为 NULL 的记录有多少个。
count(1)、 count(*)、 count(主键字段)在执行的时候(要知道底层执行逻辑,见原文),如果表里存在二级索引,优化器就会选择二级索引进行扫描。
所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。
再来,就是不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计(使用二级索引会采用index(全索引扫描))。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。
3 如何优化 count(*)?
如果对一张大表经常用 count(*) 来做统计,其实是很不好的,非常费时间。比如统计一个有 1200 万+ 的数据表,耗时5秒:
![图片](https://cdn.xiaolincoding.com//mysql/other/74a4359b58dc6ed41a241e425f43764d.png)
优化方法有如下两种:
-
近似值:使用 show table status 或者 explain 命令来表进行估算。执行 explain 命令效率是很高的,因为它并不会真正的去查询,下图中的 rows 字段值就是 explain 命令对表 t_order 记录的估算值。
-
额外表保存计数值:如果是想精确的获取表的记录总数,我们可以将这个计数值保存到单独的一张计数表中。当我们在数据表插入一条记录的同时,将计数表中的计数字段 + 1。也就是说,在新增和删除操作时,我们需要额外维护这个计数表。