InnoDB索引结构:B+树
InnoDB使用B+树(Balanced Tree)数据结构来实现索引。B+树是一种树形的数据结构,非叶子节点只存储索引值,叶子节点才存储数据的值,叶子节点从小到大有序,形成链表。
这种结构有两个优点:
索引结构选型
索引结构 | 磁盘读取性能 | 范围查询 | 插入删除性能 | 优点 | 缺点 |
---|---|---|---|---|---|
哈希表 | 弱 | 弱 | 强 | 查找某个key的时间复杂度为O(1) | 1. 解决哈希碰撞的问题 2. 不支持高效的范围的查找 |
二叉树 | 弱 | 弱 | 中 | 查找时间复杂度为 log(n),支持一定程序的范围查找 | 极端情况下会退化为线性链表,二分查找也会退化为遍历查找,时间复杂退化为 O(N),检索性能急剧下降。 |
红黑树 | 弱 | 弱 | 中 | 相比二叉查找树 不会存在退化为线性链表的情况 | 但是树并不是严格平衡,树也会出现左倾或右倾的情况 且树频繁调整十分消耗性能 |
AVL树 | 弱 | 中 | 中 | 不错的查找性能(O(logn)),不存在极端的低效查找的情况。可以实现范围查找、数据排序。 | 我们每一个树节点只存储了一个数据,我们一次磁盘 IO 只能取出来一个节点上的数据加载到内存里。磁盘 IO 有个有个特点,就是从磁盘读取 1B 数据和 1KB 数据所消耗的时间是基本一样的。 |
B+ 树 | 强 | 强 | 中 | 优秀检索速度,时间复杂度:B 树的查找性能等于 O(h*logn),其中 h 为树高,n 为每个节点关键词的个数; 尽可能少的磁盘 IO,加快了检索速度; 可以支持范围查找。 |
索引类型
概念:聚簇索引(Clustered Index)与 非聚簇索引(Secondary Index)
核心区别:聚簇索引决定数据的物理存储顺序
聚簇索引规定数据在表中的物理存储顺序,因此一个表只能包含一个聚簇索引。聚簇索引的叶子节点存储的是实际的数据。因此,聚簇索引的表现形式是数据存储与索引放到了一块。
InnoDB表必须具有聚簇索引。如果没有显式定义主键,InnoDB会自动选择一个唯一非空索引作为聚簇索引,或者生成一个隐式的ROWID作为聚簇索引。
非聚簇索引(Secondary Index): 表中的顺序与实际物理顺序是不一样的,叶子节点存储的也不是实际的数据,而是主键。因此,非聚簇索引的表现形式为数据与索引是分开存储。
联合索引
MySQL中的联合索引(Compound Index),也称为复合索引或多列索引,是一种将多个列组合在一起创建的索引类型.
假设有一个用户表"users",包含了以下几个列:id、name、age、gender、city。现在我们经常需要按照年龄和城市来查询用户信息,那么可以在这两个列上创建一个联合索引
CREATE INDEX age_city_idx ON users (age, city);
联合索引遵循最左匹配,遇到非等值判断时匹配停止。(注意,in是属于等值判断)
主键索引
主键是一种特殊的唯一索引,用于标识表中的每个记录。主键列中的值必须是唯一的,并且不能为NULL。在MySQL中,可以使用AUTO_INCREMENT关键字来为主键列生成唯一的、自增的值。
CREATE TABLE users (
id INT unsigned NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
age INT,
PRIMARY KEY (`id`),
);
我们在"id"列上创建了一个主键,并使用AUTO_INCREMENT关键字使MySQL自动生成唯一的、自增的值。
DB_ROW_ID
当表没有定义主键时,InnoDB 会使用 DB_ROW_ID
作为聚簇索引(主索引)的键值。
InnoDB 维护了一个全局的 dict_sys.row_id 值,所有无主键的 InnoDB 表,每插入一行数据,都将当前的 dict_sys.row_id 值作为要插入数据的 row_id,然后把 dict_sys.row_id 的值加 1。
在 InnoDB 逻辑里,申请到 row_id=N 后,就将这行数据写入表中;如果表中已经存在 row_id=N 的行,新写入的行就会覆盖原有的行。
唯一索引
唯一索引是用来保证表中某些列的唯一性,也就是说,在唯一索引列中的任何值都不能重复出现。如果重复插入相同的值,则会触发唯一性约束,导致插入失败。
与主键索引相比,主键是一种特殊的唯一索引,用于标识表中的每个记录,而唯一索引则是用来保证表中某些列的唯一性。
CREATE TABLE users (
id INT unsigned NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
email VARCHAR(255),
age INT,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_email` (`email`)
);
我们在"email"列上创建了一个唯一索引,这意味着在"email"列中的任何值都不能重复出现。如果尝试插入重复的"email"值,MySQL会触发唯一性约束,导致插入失败。
- 唯一索引(Unique Index)不允许具有相同的非 NULL 值,但是允许有多个 NULL 值。
- 值为NULL的索引会被放在B+树的最左边
前缀索引
MySQL中的前缀索引是一种特殊的索引类型,它只包含列值的前缀部分,而不是整个列值。通过使用前缀索引,可以减少索引的大小,从而提高查询性能和索引效率。
mysql> alter table SUser add index index2(email(6));
在建立索引时关注的是区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。因此,我们可以通过统计索引上有多少个不同的值来判断要使用多长的前缀。
# 首先,你可以使用下面这个语句,算出这个列上有多少个不同的值:
mysql> select count(distinct email) as L from SUser;
# 依次选取不同长度的前缀来看这个值,比如我们要看一下 4~7 个字节的前缀索引
mysql> select
count(distinct left(email,4))as L4,
count(distinct left(email,5))as L5,
count(distinct left(email,6))as L6,
count(distinct left(email,7))as L7,
from SUser;
# 设定自己可以接受的损失比例然后调整
除上述索引外,mysql还有全文索引和空间索引,但是由于性能问题,在实践中使用较少.
在实践中,如果需要进行文本搜索和地理位置查询,通常会选择使用专门的搜索引擎,如Elasticsearch、Solr等,
索引机制
- 主键索引和普通索引: 普通索引需要多一次回表的操作
change buffer机制
MySQL中的Change Buffer机制是一种优化技术,用于提高写入操作的性能。它在内存中维护了一个缓存,用于暂存对非聚集索引的修改,而不是立即将这些修改写入磁盘。当查询要使用到这些修改时,Change Buffer会将缓存中的修改应用到磁盘上的索引中,从而避免了频繁的磁盘写入。
以下是Change Buffer机制的工作原理:
需要注意的是,Change Buffer机制只适用于非聚集索引,因为聚集索引是按照主键顺序存储的,不需要使用Change Buffer来优化写入性能。此外,Change Buffer机制也需要考虑到缓存的大小和写入磁盘的频率,以避免对查询性能造成负面影响。
索引、联合索引失效
不满足最左前缀原则
联合索引遵循最左前缀原则,即查询时必须从联合索引的最左侧列开始使用。如果查询条件没有涵盖索引的最左侧列,索引将无法使用。例如,如果有一个 (A, B, C)
的联合索引,查询条件仅包含 B
和 C
时,索引无法使用。
遇到非等值查询条件
如果联合索引中的某一列是非等值查询条件(例如 >
、=
、100 AND b>200;
不使用索引合并过程可能是这样的
idx_a
B+树中获取到第一条a>100
的记录,拿记录里的id值回表查询。b>200
是否成立,成立则返回给客户端,否则丢弃该记录。idx_a
B+树中获取到下一条a>100
的记录,重复前面的过程。使用索引合并过程可能是这样的
idx_a
索引将获取到的id集合记作id_setA
。idx_b
索引将获取到的id集合记作id_setB
。id_setA
和id_setB
取交集,记作id_set
。id_set
回表查询,将结果返回给客户端。优化器对索引的选择
In与索引开销计算
- IN里面的个数多少,会影响MySQL优化器对开销代价的估算方法
- 估算方法的差异,受 eq_range_index_dive_limit 这个参数控制,参数值默认是200
- 当IN的个数小于200时,MySQL使用 index dive 方式估算开销,这种方式统计速度慢,但是比较精确;
- 当IN的个数>=200时,使用index statistics,这种方式统计速度快,但是不一定精准
explain 详解
EXPLAIN
是 MySQL 中用于分析查询执行计划的关键字, 通过使用 EXPLAIN
,我们可以了解查询如何使用索引、连接表以及其它优化策略。
下面是一个简单的例子。假设我们有一个名为 employees
的表,包含以下字段:id
(主键)、first_name
、last_name
、age
和 department_id
。我们还为 department_id
和 last_name
创建了索引。
CREATE TABLE employees (
id INT PRIMARY KEY AUTO_INCREMENT,
first_name VARCHAR(50),
last_name VARCHAR(50),
age INT,
department_id INT,
INDEX (department_id),
INDEX (last_name)
);
现在我们想要查询年龄大于 30 的员工,且所在部门 ID 为 5 的所有员工。我们可以使用以下查询:
SELECT * FROM employees WHERE age > 30 AND department_id = 5;
为了分析这个查询的执行计划,我们可以在查询前加上 EXPLAIN
关键字:
EXPLAIN SELECT * FROM employees WHERE age > 30 AND department_id = 5;
执行这个查询后,我们可能得到类似如下的结果:
+----+-------------+-----------+------------+------+---------------+--------------+---------+-------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------+------------+------+---------------+--------------+---------+-------+------+----------+--------------------------+
| 1 | SIMPLE | employees | NULL | ref | department_id | department_id | 5 | const | 10 | 33.33 | Using where; Using index |
+----+-------------+-----------+------------+------+---------------+--------------+---------+-------+------+----------+--------------------------+
这个结果表示:
select_type
:查询类型为 SIMPLE
,表示这是一个简单的查询,没有子查询或者 UNION 操作。table
:正在查询的表是 employees
。type
:查询使用的连接类型是 ref
,表示使用了非唯一索引进行查找。possible_keys
:可能使用的索引为 department_id
。key
:实际使用的索引是 department_id
。key_len
:索引的长度为 5。ref
:索引的参考列是一个常量值(部门 ID)。rows
:优化器预计需要检查 10 行数据。filtered
:经过条件过滤后,大约 33.33% 的记录符合查询条件。Extra
:额外信息,这里显示了 Using where
和 Using index
。Using where
表示使用了 WHERE
子句过滤数据,Using index
表示使用了覆盖索引,不需要访问表的数据行。- 通过key_len 可以看出使用了索引的哪些部分
- Extra里 只有using index的话,不用回表
select_type
select_type | Description |
---|---|
SIMPLE | 简单SELECT,不使用UNION或子查询等 |
PRIMARY | 查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY |
UNION | UNION中的第二个或后面的SELECT语句 |
DEPENDENT UNION | UNION中的第二个或后面的SELECT语句,取决于外面的查询 |
UNION RESULT | UNION的结果 |
SUBQUERY | 子查询中的第一个SELECT |
DEPENDENT SUBQUERY | 子查询中的第一个SELECT,取决于外面的查询 |
DERIVED | 派生表的SELECT, FROM子句的子查询 |
UNCACHEABLE SUBQUERY | 一个子查询的结果不能被缓存,必须重新评估外链接的第一行 |
type
type | 描述 |
---|---|
ALL | Full Table Scan, MySQL将遍历全表以找到匹配的行 |
index | Full Index Scan,index与ALL区别为index类型只遍历索引树 |
range | 只检索给定范围的行,使用一个索引来选择行 |
ref | 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 |
eq_ref | 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件 |
const、system | 当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system是const类型的特例,当查询的表只有一行的情况下,使用system |
NULL | MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。 |
key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度
- 所有的索引字段,如果没有设置not null,则需要加一个字节。
- 定长字段,int占四个字节、date占三个字节、char(n)占n个字符。
- 对于变成字段varchar(n),则有n个字符+两个字节。
- 不同的字符集,一个字符占用的字节数不同。latin1编码的,一个字符占用一个字节,gbk编码的,一个字符占用两个字节,utf8编码的,一个字符占用三个字节。
Extra
- Using index: "覆盖索引扫描", 表示查询在索引树中就可查找所需数据, 不用扫描表数据文件, 往往说明性能不错
- Using where:
- 如果不读取表的所有数据,或不是仅仅通过索引就可以获取所有需要的数据,则会出现 Using where 信息。
- 列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤
- Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询
- Using filesort:MySQL中无法利用索引完成的排序操作称为“文件排序”
- Using join buffer:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。
- Impossible where:这个值强调了where语句会导致没有符合条件的行。
- Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行
参考
MySQL优化之Index Merge_程序员小潘的博客-CSDN博客_index_merge
15个必知的Mysql索引失效场景,别再踩坑了!
为什么生产环境中B+树的高度总是3-4层?
面试题:mysql 一棵 B+ 树能存多少条数据?