一个数据页满了,按照B+Tree算法,新增加一个数据页,叫做页分裂,会导致性能下降。空间利用率降低大概50%。当相邻的两个数据页利用率很低的时候会做数据页合并,合并的过程是分裂过程的逆过程。
数据页的整理可以通过以下两种方式实现(都是内部操作):
每一个索引在 InnoDB 里面对应一棵 B+ 树。
重建索引
alter table T drop index k;
alter table T add index(k);
以上SQL可以这样代替
alter table T engine=InnoDB
重建非主键索引的做法是合理的,可以达到省空间的目的,但是重建主键的过程不合理,无论是创建主键还是删除主键,都会将整个表重建。
假设有这么一张表,其中查询语句是
select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';
这个不同带来的性能差距会有多少呢?答案是,微乎其微。
InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。
因为引擎是按页读写的,所以说,当找到 k=5 的记录的时候,它所在的数据页就都在内存里了。那么,对于普通索引来说,要多做的那一次“查找和判断下一条记录”的操作,就只需要一次指针寻找和一次计算。
当然,如果 k=5 这个记录刚好是这个数据页的最后一个记录,那么要取下一个记录,必须读取下一个数据页,这个操作会稍微复杂一些。
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。change buffer 在内存中有拷贝,也会被写入到磁盘上。
将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。
显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。
那么,什么条件下可以使用 change buffer 呢?
对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插入 (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。
因此,唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。
change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。
如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。
第一种情况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程如下
第二种情况是,这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程如下:
change buffer 的使用场景
普通索引的所有场景,使用 change buffer 都可以起到加速作用吗?
因为 merge 的时候是真正进行数据更新的时刻,而 change buffer 的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。
对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。
通常情况下,MySQL的Change Buffer是自动启用的,不需要手动设置。当InnoDB存储引擎检测到适当的条件时,它会自动使用Change Buffer。
但是,你可以通过修改一些配置参数来控制Change Buffer的使用。例如,可以使用innodb_change_buffer_max_size参数来设置Change Buffer所使用的最大内存大小。另外,你也可以使用innodb_change_buffering参数来控制Change Buffer的启用方式,可以将其设置为“none”以禁用Change Buffer,或将其设置为“inserts”以只启用插入操作的Change Buffer。
redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。
注意
B+树中的B不是代表二叉(binary),而是代表平衡,因为B+树是从最早的平衡二叉树演化而来的,但B+树不是一个二叉树。另外一个被忽视的问题是,B+树索引并不找到一个给定键值的具体行,B+树索引能找到的只是被查找数据行所在的页,然后数据库通过把页读入到内存,再在内存中进行查找
在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 树中的。
每一个索引在 InnoDB 里面对应一棵 B+ 树。
在 MySQL 5.6 之前,只能从 ID3 开始一个个回表。到主键索引上找出数据行,再对比字段值。
而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
普通索引和唯一索引应该怎么选择
这两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以还是尽量选择普通索引(但在更新性能上,由于普通索引可以使用change buffer)。
MySQL 是怎样得到索引的基数的呢?
采样统计的时候,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。
而数据表是会持续更新的,索引统计信息也不会固定不变。所以,当变更的数据行数超过 1/M 的时候,会自动触发重新做一次索引统计。在 MySQL 中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent 的值来选择:设置为 on 的时候,表示统计信息会持久化存储。这时,默认的 N 是 20,M 是 10。设置为 off 的时候,表示统计信息只存储在内存中。这时,默认的 N 是 8,M 是 16。
一个表有联合索引a,b,c 。b = ? and c = ? and a = ?会使用索引吗
本地 MYSQL版本 5.7
mysql创建一张表,表名:‘test_models’
执行语句大体这样吧:
CREATE TABLE `test_models`
(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`a` INT(11) DEFAULT NULL,
`b` INT(11) DEFAULT NULL,
`c` INT(11) DEFAULT NULL,
`d` INT(11) DEFAULT NULL,
`e` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_abc` (`a`, `b`, `c`)
) ENGINE = INNODB
DEFAULT CHARSET = utf8mb4 COMMENT = 'test';
用代码往表中写入100万条数据
使用 ‘EXPLAIN’ sql语句查看执行详情
EXPLAIN SELECT * FROM test_models WHERE a = 100 AND b = 1000 AND c = 10000;
a = 1 AND b = 2 AND c = 3 ; 使用索引
c = 1 AND b = 2 AND a = 3 ; 使用索引
a = 1 AND b = 2 ; 使用索引
a = 1 AND c = 3 ; 使用索引
c = 1 AND a = 2 ; 使用索引
c = 3 ; 未使用索引
b = 2 ; 未使用索引
b = 2 AND c = 3 ; 未使用索引
c = 1 AND b = 2 ; 未使用索引
a = 1 AND b = 2 OR c = 3 未使用索引
a = 1 OR b = 2 AND c = 3 未使用索引
a = 1 OR b = 2 OR c = 3 未使用索引
a > 1 AND b = 2 AND c = 3 未使用索引
a < 1 AND b = 2 AND c = 3 未使用索引 最多 a、b 走索引,c不会走索引。
a > 1 ; 未使用索引
a <> 1 AND b = 2 AND c = 3 未使用索引
a = 1 AND b < 2 AND c = 3 使用索引
a = 1 AND c = 2 AND b < 3 使用索引
a = 1 AND b < 2 使用索引
a = 1 AND b <> 2 AND c = 3 使用索引
// 可以说 OR一出现就不使用
a = 1 AND b < 2 OR c = 2 未使用索引
a = 某,后面order 无所谓 都 使用索引 (和最上面的最左匹配一样)
a = 1 AND b = 2 AND c = 3 ORDER BY a;// 或者 ORDER BY b , ORDER BY c ,ORDER BY d, 使用索引
a = 1 ORDER BY a; // 或者 ORDER BY b,ORDER BY c,ORDER BY d 使用abc索引
b = 某,不使用
b = 1 ORDER BY a; //ORDER BY b 都 未使用索引
a and c,只会命中索引的a,不会命中a,b,c索引,abc索引相当于创建了三个索引,a,ab,abc
如果我们遇到了范围条件查询,比如(<)(<=)(>)(>=)和 between 等,那么范围列后的列就无法使用到索引了。
当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。
所以执行一个 WHERE A=a1 AND B=b1 AND C=c1 的查询就类似于:
for a in A {
if a == a1 {
for b in B {
if b == b1 {
for c in C {
if c == c1 {
// 这就是你要的数据,拿到主键之后去磁盘里面加载出来
}
}
}
}
}
}
如果查询条件是 WHERE A = a1 AND B = b1,那么你可以推断出来,数据库只会应用外层的两重循环,不会对 C 进行过滤。
for a in A {
if a == a1 {
for b in B {
if b == b1 {
// 这就是你要的结果,去磁盘里面读取
}
}
}
}
如果查询条件是 WHERE A = a1 OR B = b1,那么这个查询并不会使用这个索引。
for a in A {
if a == a1 {
as = append(as, a)
}
}
for b in B {
if b == b1 {
bs = append(bs, b)
}
}
// as 和 bs 的并集就是你要的结果
按照组成索引的列的顺序,从左往右:AND 用 OR 不用,正用反不用,范围则中断。
索引并不是没有代价的,它会消耗很多的系统资源。
单列索引和复合索引的长度也需要控制,在 MySQL InnoDB 中,系统默认单个索引长度最大为 767 bytes,如果单列索引长度超过了这个限制,就会取前缀索引,也就是取前 255 字符。字符列会占用较大的空间,在数据表设计的时候,尽量采用数值类型替代字符类型,尽量避免用字符类型做主键,同时针对字符字段最好只建前缀索引。
假设当前数据表的数据量为N,每个磁盘块的数据项的数量是m,则树高h=㏒(m+1)N
指索引列唯一值的数目与表中记录数的比例
SHOW INDEX结果中的列Cardinality来观察。Cardinality是一个预估值,而不是一个准确值.Cardinality/n_rows_in_table应尽可能地接近1。如果非常小,那么用户需要考虑是否还有必要创建这个索引。
show index from xxx
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。