MySQL 索引

一、索引是什么?

索引是帮助 MySQL 高效获取数据的数据结构。

二、索引能干什么?

索引非常关键,尤其是当表中的数据量越来越大时,索引对于性能的影响愈发重要。 索引能够轻易将查询性能提高好几个数量级,总的来说就是可以明显的提高查询效率。

三、索引的分类?

1、从存储结构上来划分:BTree 索引(B-Tree 或 B+Tree 索引),Hash 索引,full-index 全文索引,R-Tree 索引。这里所描述的是索引存储时保存的形式,

2、从应用层次来分:普通索引,唯一索引,联合索引

3、根据中数据的物理顺序与键值的逻辑(索引)顺序关系:聚集索引,非聚集索引。

平时讲的索引类型一般是指在应用层次的划分。

就像手机分类:安卓手机,IOS 手机 与 华为手机,苹果手机,OPPO 手机一样。

普通索引:即一个索引只包含单个列,一个表可以有多个单列索引,当使用不等于判断时,普通索引不会生效,只有主键和唯一索引会生效

唯一索引:索引列的值必须唯一,但允许有空值

联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并

聚簇索引(聚集索引):并不是一种单独的索引类型,而是一种数据存储方式。具体细节取决于不同的实现,InnoDB 的聚簇索引其实就是在同一个结构中保存了 B-Tree 索引(技术上来说是 B+Tree)和数据行。

非聚簇索引:****不是聚簇索引,就是非聚簇索引

四、索引的底层实现

mysql 默认存储引擎 innodb 只显式支持 B-Tree( 从技术上来说是 B+Tree)索引,对于频繁访问的表,innodb 会透明建立自适应 hash 索引,即在 B 树索引基础上建立 hash 索引,可以显著提高查找效率,对于客户端是透明的,不可控制的,隐式的。

不谈存储引擎,只讨论实现(抽象)

Hash 索引

基于哈希表实现,只有精确匹配索引所有列的查询才有效,对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),并且 Hash 索引将所有的哈希码存储在索引中,同时在索引表中保存指向每个数据行的指针。

缺点

  • Hash 表不适合做范围查询,它更适合做等值查询。
    Hash 索引仅仅能满足 = , IN 和 <=>(表示 NULL 安全的等价) 查询,不能使用范围查询。
    由于 Hash 索引比较的是进行 Hash 运算之后的 Hash 值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的 Hash 算法处理之后的 Hash 值的大小关系,并不能保证和 Hash 运算前完全一样。

  • Hash 索引依然需要回表扫描。
    Hash 索引是将索引键通过 Hash 运算之后,将 Hash 运算结果的 Hash 值和所对应的行指针信息存放于一个 Hash 表中,由于不同索引键可能存在相同 Hash 值,所以即使取满足某个 Hash 键值的数据的记录条数,也无法从 Hash 索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。

B-Tree 索引(MySQL 使用 B+Tree)

B-Tree 能加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,数据分布在各个节点之中。

B+Tree 索引

是 B-Tree 的改进版本,同时也是数据库索引索引所采用的存储结构。数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。相比 B-Tree 来说,进行范围查找时只需要查找两个节点,进行遍历即可。而 B-Tree 需要获取所有节点,相比之下 B+Tree 效率更高。

结合存储引擎来讨论(一般默认使用 B+Tree)

案例:假设有一张学生表,id 为主键

id name birthday
1 Tom 1996-01-01
2 Jann 1996-01-04
3 Ray 1996-01-08
4 Michael 1996-01-10
5 Jack 1996-01-13
6 Steven 1996-01-23
7 Lily 1996-01-25

在 MyISAM 引擎中的实现(二级索引也是这样实现的)

在 InnoDB 中的实现

聚簇索引

基于主键的索引结构,是一颗B+树,我们把这种索引叫做聚簇索引

特点:

  • 聚簇索引中的叶子节点存放了用户记录的全部数据(每一列的数据,上面图片的name字段建立一个索引这种就是非聚簇索引,以id为主键包含值得这种就是聚簇索引),它就是innoDB中数据存放的格式,即数据即聚簇索引,聚簇索引即数据,这也是聚簇索引名字的由来吧,数据和索引聚集在一起。
  • InnoDB要求表必须有主键。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐 含字段作为主键,这个字段长度为6个字节,类型为长整型,这样始终就会有一个聚簇索引。

联合索引

将多个字段组合成一个索引,那么这种二级索引就被称为联合索引 (结合下面的联合索引共同理解)

CREATE INDEX index_product_no_name ON product(product_no, name);

联合索引 ``(product_no, name)` 的 B+Tree 示意图如下:


可以看到,联合索引的非叶子节点用两个字段的值作为 B+Tree 的索引值。

联合索引的 B+Tree 是先按 product_no 进行排序,然后再 product_no 相同的情况再按 name 字段排序。记住这句话,很重要!

最左匹配原则

使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。

在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。

比如,如果创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以利用联合索引:

  • where a=1;
  • where a=1 and b=2 and c=3;
  • where a=1 and b=2;

需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:

  • where b=2;
  • where c=3;
  • where b=2 and c=3;
  • where a=2 and c=3; a会走,c不会走

上面这些查询条件之所以会失效,是因为(a, b, c) 联合索引,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。

二级索引

二级索引和聚簇索引概念大致一致,只是提出来更详细的讲解,

聚簇索引只能用于主键字段的快速查询,如果想实现「非主键字段」的快速查询,我们就要针对「非主键字段」创建索引,这种索引称作为「二级索引」。二级索引同样基于 B+Tree 实现的,不过二级索引的叶子节点存放的是主键值,不是实际数据。

主键索引是InnoDB存储引擎默认给我们创建的一套索引结构,我们表里的数据也是直接放在主键索引里(所以innodb引擎时最好指定一个主键索引),作为叶子节点的数据页。

但我们在开发的过程中,往往会根据业务需要在不同的字段上建立索引,这些索引就是二级索引,今天我们就给大家讲讲二级所有的原理。

比如,你给name字段加了一个索引,你插入数据的时候,就会重新创建一棵B+树,B+树的叶子节点,也是数据页,但是这个数据页里仅仅放了主键字段和name字段。

叶子节点的数据页的name值,跟主键索引一样的,都是按照大小排序的。同一个数据页里的name字段值都是大于上一个数据页里的name字段值。

name字段的B+树也会构建多层索引页,这个索引页里放的是下一层的页号和最小name字段值。就像这样:

假设你要根据name字段来搜索数据,比如:select * from user where name=‘xxx',过程与主键索引一样的。从name索引的根节点开始找,一层一层的向下找,一直找到叶子节点,定位到name字段值对应的主键值。

但此时叶子节点的数据页没有完整所有字段,就需要根据主键到主键索引里去查找,从主键索引的根节点一路找到叶子节点,就可以找到这行数据的所有字段了,这个过程就叫回表 (在开发中就应该避免回表频率)。

二级索引,可以对多个字段建立联合索引,比如,name + age + sex

此时联合索引与单个字段的索引原理是一样的,只不过叶子节点的数据页里放的是id + name + age + sex,然后默认按照name排序,name一样就按age排序,age一样就按sex排序。

每个name + age +sex的索引页里,放的就是下层节点的页号和最小的name + age + sex值。当你用name + age + sex搜索的时候,就会走name + age + sex联合索引这棵树,再回表查询。

覆盖索引

覆盖索引的概念也是基于二级索引 B+Tree

在上面的二级索引中,给name字段添加索引,其他几个字段不添加索引,进行查询

select * from product where name = '0002';

会先在二级索引的 B+Tree 中快速查找到 name 为 0002 的二级索引记录,然后获取主键值,然后利用主键值在主键索引的 B+Tree 中快速查询到对应的叶子节点,然后获取完整的记录。这个过程叫「回表」,也就是说要查两个 B+Tree 才能查到数据。

不过,当查询的数据是能在二级索引的 B+Tree 的叶子节点里查询到,这时就不用再查主键索引查,比如下面这条查询语句

select id from name where name = '0002';

这种在二级索引的 B+Tree 就能查询到结果的过程就叫作「覆盖索引」,也就是只需要查一个 B+Tree 就能找到数据

所以减少 回表也是优化的方式

四.二 执行计划

MySQL 的 EXPLAIN 是一个非常有用的工具,它可以分析和解释查询语句的执行计划,帮助开发者优化查询性能。通过执行 EXPLAIN 命令,可以获取查询执行计划的详细信息,包括以下字段:

  1. id: 查询的唯一标识符,用于标识每个查询的不同步骤或子查询。

  2. select_type: 查询类型,表示查询的类型和方式,常见的取值包括:

    • SIMPLE: 简单查询,不包含子查询或联接。
    • PRIMARY: 外层查询。
    • SUBQUERY: 子查询。
    • DERIVED: 派生表的查询。
    • UNION: UNION 查询。
    • UNION RESULT: UNION 查询的结果。
    • DEPENDENT UNION: 依赖于外部查询的 UNION 查询。
    • UNION RESULT: UNION 查询的结果。
    • DEPENDENT UNION: 依赖于外部查询的 UNION 查询。
    • UNION RESULT: UNION 查询的结果。
    • DEPENDENT UNION: 依赖于外部查询的 UNION 查询。
  3. table: 表名,表示查询涉及的表名或表的别名。

  4. partitions: 分区信息,如果查询涉及到分区表,则显示分区信息。

  5. type: 访问类型,表示查询使用的访问方法和算法,常见的取值包括:

    • ALL: 全表扫描,需要扫描整个表。
    • index: 只访问索引,无需扫描表数据。
    • range: 使用索引范围进行查询。
    • ref: 使用非唯一索引或唯一索引前缀进行查询。
    • eq_ref: 使用唯一索引进行等值查询。
    • const: 常量查询,使用常量值进行查询。
    • system: 系统表查询。
    • NULL: 无效或未知的查询类型。
  6. possible_keys: 可能使用的索引,表示查询可能使用的索引列表。

  7. key: 实际使用的索引,表示查询实际使用的索引。

  8. key_len: 使用索引的长度,表示索引中使用的字节数。

  9. ref: 连接条件,表示连接使用的列或常量。

  10. rows: 估计的行数,表示查询扫描的行数估计值。

  11. filtered: 过滤的行百分比,表示查询结果中实际返回的行数百分比。

  12. Extra: 额外信息,表示查询的附加信息,可能包括排序、临时表、使用的文件等。

通过分析 EXPLAIN 的输出结果,可以了解查询的执行计划、访问方法和可能存在的性能问题。可以根据输出结果中的字段信息,优化查询语句、索引设计和数据库配置,以提高查询性能和效率。

五、索引在磁盘中保存的格式

myisam 存储引擎(非聚集索引):索引文件和数据文件分离。

data 目录下以.frm 、.MYD、.MYI 结尾的三个文件。
.frm:表的表结构。
.MYD:表的数据记录(表的数据行)。
.MYI:表的索引。
查找过程:根据 myi 文件中索引找到数据在 myd 文件中的位置(B+tree 的叶节点存储了数据的位置)。

innodb 存储引擎(聚集)(支持事务):

data 目录下以.frm 和.ibd(index、data)结尾的两个文件。
ibd 文件存储了索引和数据。该存储引擎要求存储数据文件时就是以 B+Tree 存储的。
表数据文件本身就是按 B+Tree 组织的一个索引结构文件。
与 myi 比少了一次磁盘 I/O。

问题: 为什么 innoDB 必须有主键?为什么推荐使用整型的自增主键。

答: 必须有一个 B+Tree 来组织数据文件。如果不指定主键,mysql 也会自动生成一个主键(使用唯一列或自动加一列)。
整型所占用的存储空间较小,而且整型数据容易做比较(检索)。
使用自增主键更便于插入,避免或减少了 B+Tree 的调整。

六、为什么索引结构默认使用 B+Tree,而不是 Hash,二叉树,红黑树?

B+tree:因为 B 树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致 IO 操作变多,查询性能变低;

Hash:虽然可以快速定位,但是没有顺序,IO 复杂度高。

二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且 IO 代价高。

红黑树:树的高度随着数据量增加而增加,IO 代价高。

七、为什么官方建议使用自增长主键作为索引?

结合 B+Tree 的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率。

插入连续的数据:

插入非连续的数据

八、简单总结下

1、MySQL 使用 B+Tree 作为索引数据结构。 2、B+Tree 在新增数据时,会根据索引指定列的值对旧的 B+Tree 做调整。 4、从物理存储结构上说,B-Tree 和 B+Tree 都以页(4K)来划分节点的大小,但是由于 B+Tree 中中间节点不存储数据,因此 B+Tree 能够在同样大小的节点中,存储更多的 key,提高查找效率。 5、影响 MySQL 查找性能的主要还是磁盘 IO 次数,大部分是磁头移动到指定磁道的时间花费。 6、MyISAM 存储引擎下索引和数据存储是分离的,InnoDB 索引和数据存储在一起。 7、InnoDB 存储引擎下索引的实现,(辅助索引)全部是依赖于主索引建立的(辅助索引中叶子结点存储的并不是数据的地址,还是主索引的值,因此,所有依赖于辅助索引的都是先根据辅助索引查到主索引,再根据主索引查数据的地址)。 8、由于 InnoDB 索引的特性,因此如果主索引不是自增的(id 作主键),那么每次插入新的数据,都很可能对 B+Tree 的主索引进行重整,影响性能。因此,尽量以自增 id 作为 InnoDB 的主索引。

九、其他概念

覆盖索引

如果索引包含所有满足查询需要的数据的索引成为覆盖索引(Covering Index),也就是平时所说的不需要回表操作。

简单的说,覆盖索引覆盖所有需要查询的字段(即,大于或等于所查询的字段)。MySQL 可以通过索引获取查询数据,因而不需要读取数据行。

覆盖索引的好处:

  1. 索引大小远小于数据行大小。因而,如果只读取索引,则能极大减少对数据访问量。
  2. 索引按顺序储存。对于 IO 密集型的范围查询会比随机从磁盘读取每一行数据的 IO 要少。
  3. 避免对主键索引的二次查询。二级索引的叶子节点包含了主键的值,如果二级索引包含所要查询的值,则能避免二次查询主键索引(聚簇索引,聚簇索引既存储了索引,也储存了值)。

十、在InnoDB执行了delete,为啥数据删了个寂寞?

一、删除并不是真正的删除

熟悉MySQL InnoDB存储引擎的同学都应该知道,当我们执行delete的时候,数据并没有被真正的删除,只是对应数据的删除标识deleteMark被打开了,这样每次执行查询的时候,如果发现数据存在但是deleteMark是开启的话,那么依然返回空,因为这个细节,所以经常会出现“我明明删除了数据,为什么空间没释放”的现象。

15M  7  6 18:46 user_info.ibd #删除前
15M 10  4 16:47 user_info.ibd #删除后

二、为什么不直接删除,而是打个标记

我们知道InnoDB存储引擎是支持MVCC的,即多版本控制,得益于MVCCMySQL在事务里查询数据的时候不需要加锁,可以提供很好的并发性,同时提供可重复读这个很重要的特性。那么它是怎么到的呢?答案是undo log,可以简单的理解为,每次更新数据的时候将更新前的数据先写入undo log中,这样当需要回滚的时候,只需要顺着undo log找到历史数据即可。undo log与原始数据之间是用指针链接起来的,即每条数据都有个回滚指针指向undo log


如果InnoDB在删除数据的时候,真的是把数据从磁盘上擦除,那么这时候:

  • 别的事务通过undo log是无法找到原始数据。

  • 可重复读这个特性会被破坏。

三、只是打个标记的话,岂不是很浪费空间

MySQL里面有个purge线程,它的工作中有一项任务就是专门检查这些有deleteMark的数据,当有deleteMark的数据如果没有被其他事务引用时,那么会被标记成可复用,因为叶子节点数据是有序的原因,这样当下次有同样位置的数据插入时,可以直接复用这块磁盘空间。当整个页都可以复用的时候,也不会把它还回去,会把可复用的页留下来,当下次需要新页时可以直接使用,从而减少频繁的页申请。

四、基于页的存储方式

我们知道MySQL数据是存储在磁盘上的,磁盘的速度想必大家都知道,特别是当发生随机IO的时候。这里简单解释下什么叫IO,以机械磁盘为例,我们最终的数据都是落在磁盘的一个一个扇区上的,当一个扇区写满了,就得换下一个扇区,这时就要通过盘片的转动找到目标扇区,这是物理运动。如果要写入的下一个扇区和当前的扇区是紧挨着的,这叫顺序IO,如果要写入的扇区和当前的扇区中间隔了几个扇区,这叫随机IO,很明显随机IO需要更长的转动时间。所以查询一个数据的时候,减少IO是非常关键的,特别是随机IO。

为了减少磁盘IO,MySQL采用B+树的索引结构来组织数据,B+树的特点是矮胖,一般树的高度就代表了IO的次数,越矮的话,树的高度越低,那么对应的IO次数就越少,还有一点需要知道的是数据最终都在叶子节点上,所以在B+树上搜索的时候,一定是要检索到最后一层叶子节点上,这是一种稳定性的表现。

  • 1、行与页

    这里需要知道的是,我们最终通过B+树检索到的不是我们的目标行数据,而是目标行数据所在的页,这个页上有很多数据,都是索引序号相邻的,当找到目标页后,会把目标页加载到内存中,然后通过二分法找到目标数据。

    也许你会问,那搜索的开销不仅仅是磁盘IO,还有在二分法查找的开销。这里不可否认,但是我们一般忽略这部分开销,因为cpu在内存里检索的速度很快,并且一页也就16k,数据并不多。

  • 2、IO次数不一定等于树的高度

    前面我们说到树的高度等于IO的次数,这其实不是很准确,我们知道树的根节点一定是在内存里的,那么对于一颗高度为3的数据,只用2次IO即可,这其实可以理解,毕竟根节点只占用一页的空间,一页才16K,放在内存里绰绰有余。但有时候树的第二层也可以放在内存里。

假设现在主键是bigint,bigint我们知道占用8个字节,对于一个索引来说除了类型本身占用空间之外,还有一个指针,这个指针占用6个字节,那么对于根节点来说它大概能存 16K/(8+6)B = 1170 个数据,每个数据都可以指向一页(也就是它的下一层),这样整个树的第二层大概占用 1170*16K = 18M 的空间,这也不是一个很大的数字,对于机器的内存来说,几乎也是沧海一粟,所以第二层往往也在内存里,所以最终在B+树上检索数据所消耗的IO应该比理论的要低。

通过上面我们知道检索一条数据的快慢,主要受树的高度影响的,这和你的数据表的大小并没有太大的关系,现实中有人可能在数据表达到百万级别的时就考虑分表,个人认为这有点低估B+树的能力了。还是以bigint类型的主键索引为例,假设一行数据占用1K(理论上已经足够大了),那么一页可以存下 16K/1K=16 条数据,对于一颗高度为3的B+树来说,它可以存下 1170117016=21902400 的数据,将近2千万,如果你的数据行占用的空间更小,就可以存下更多的数据,所以只是简单的根据数据行数来判断是否需要分表不是那么的合理。

五、可复用的空间一直没有被利用咋办

前面我们说到删除的数据不会被真的删除,只是打上个deleteMark的标识,然后会被复用,但是如果一直没被复用,那么空间不就是白白的浪费了,更糟糕的是,如果删除的很多数据空间都没有被复用,就会造成页空间存在大量的碎片,为了解决这种情况,MySQL内部有个叫页合并的功能,这是什么意思呢?简单理解就是页A现在有很多可以被复用的空间,它的邻居页B也有很多可以复用的空间,此时页A就可以和页B合并,如果合并后能省出来一页,那么多出来的一页就可以被下次使用,从而达到页最大利用的效果。

合并的关键需要当前页的前一页或者后一页也有大量的碎片空间,这里为何要「大量」很关键,合并的动作可以简单理解就是把别的页的数据移动过来,如果两个页pageApageB都只有少量的可复用空间,那么合并后,即使pageA可以填满,但是另一个页Page也还是有碎片空间的,并且碎片更大,这时候数据移动的开销可能要大于存储的开销,得不偿失。

而且还会有个严重的问题,pageB可能会和pageC合并,那么pageC的碎片更大…,这样的话似乎是个无底洞,导致很多页都在移动数据。因此一个合理的合并条件很关键,InnoDB中何时合并受MERGE_THRESHOLD这个参数影响,它的默认值是50%,50%的意图很明显,两个50%就可以省出一个页。

我们看个例子,pageA已经有50%的数据被删除了,它的邻居pageB只使用了不到50%的数据,这时候会将pageB的数据移动到pageA上,那么整个pageB就是空页了,可以提供给别的数据使用。这里需要知道的是除了删除会触发页合并外,更新可能也会触发页合并。

六、有合并也有分裂

合并页是提升页的利用率的方式,但是有时候我们又不得不分裂页,我们知道叶子节点的页之间是用双向链表串接起来的,并且页与页之间的数据是有序的。

以上图为例,当我们要插入5这条数据,按道理应该尝试放在pageA里面,但是pageA目前没有足够的空间来存放一条数据,于是尝试找到pageA的相邻页pageB,但是此时很不幸的是pageB也没有足够的空间来存放一条数据,由于要求数据的连续性,数据5必须在数据4和数据6之间,那么只能新建一个页,新建一个页后,会尝试从pageA中移动一部分数据到新的页上,并且会重新组织页与页之间的关系,即在pageApageB之间会隔一道新页pageC

页分裂会造成页的利用率降低,造成页分裂的原因有很多,比如:

  • 比如离散的插入,导致数据不连续。

  • 把记录更新成一个更大记录,导致空间不够用。

还有一点需要知道的是:不管是页的合并还是页的分裂,都是相对耗时的操作,除了移动数据的开销外,InnoDB也会在索引树上加锁。

七、手动重建表

页的合并和分裂主要是在插入、删除或更新的时候,并且正好满足某些条件才发生的,那如果这些条件一直不满足,碎片就无法得到清理,这时候往往会出现”我的表明明没多少数据,为什么还占用这么大空间“这个现象,针对这个现象有人说重建索引,这个是对的,重建索引可以让数据更加紧凑,页的利用率达到更高。但是如何重建索引?第一时间你可能会想到先drop index然后add index,这个似乎不是那么准确。

如果要重建的索引是普通索引,使用这种方式还好,需要注意的是假如你的业务TPS很大,建议在业务低峰期执行,因为虽然MySQL支持online ddl,但是重建索引的过程还是很耗cpuio资源的。

如果你要重建的是主键索引,那么问题来了,首先如果你的主键索引设置的是自增长,是不支持drop的。其次如果你的主键没设置成自增长,直接drop也不是我们想象的那样,我们知道普通索引除了记录本身的索引字段外,还会记录主键的值,如果drop是直接删除索引,那么通过普通引将找不到对应的行记录,所以InnoDB是要求必须有主键索引的,这时InnoDB会尝试去表中找个唯一索引来当主键,如果没有唯一索引,那就自动创建一个默认的主键索引rowid,当新的主键索引建立好之后,还要去修改相关的普通索引让其存储新的主键,但是如果按照这种方法来修改的话,开销会很大,特别是普通索引很多的情况下,于是InnoDB干脆选择重建表。对于紧接着执行的add index操作,同样也会发生主键索引的变更,所以也会选择重建表,最终可以发现在主键索引上的drop和add其实干了一样的事情。

综上所述,一般在你的表出现很多页碎片的时候,建议使用:

alter table xx engine=InnoDB

这个命令可以重建我们这个表,但是前提是我们的表是独占表空间的。基于MySQLonline ddl,这个过程它是不影响正常的读写的,它的过程如下:

  • 扫描原表主键索引的所有记录

  • 生成新的b+树记录到临时文件

  • 生成临时文件的过程中,新的变更记录到一个中转日志row log中

  • 在临时文件生成后,将期间row log的变更应用到新的临时文件中

  • 然后替换临时文件为当前文件

这里需要注意的是重建表的过程涉及到数据的copy,得保证磁盘有足够的空间,至少是现在磁盘空间的1倍,如果磁盘空间不足,那么是不会重建成功的。

八、重建表不一定会收缩空间

在重建表的过程中,有一点需要知道:InnoDB不会让重建后的页充满数据,会预留个「1/16」的空间,这个意图很明显,如果不预留,选择占满整个页,这时候去更新一条需要更大空间的老数据,就会需要新的页,写入新的页后,往往又会造成碎片,所以提前预留一点空间是有用的。

但是因为这个预留操作,某些情况下会导致重建后的表空间反而会变大。

  • 如果你的表本身就很紧凑,因为预留1/16会变大。

  • 在第一次重建表后,因为新的插入导致用掉了预留空间的一部分(这里需要注意的是预留空间没用完,还剩一部分),但是没有用到新的页,所以整体的空间没有变化,这时候如果再次重建表,就会因为要预留1/16,导致申请的新的页,那么空间就会变大。