导读

本文略长,主要解决以下几个疑问

1、聚集索引里都存储了什么宝贝

2、什么时候索引层高会发生变化

3、预留的1/16空闲空间做什么用的

4、记录被删除后的空间能回收重复利用吗

1、背景信息

1.1 关于innodb_fill_factor

有个选项 innodb_fill_factor 用于定义InnoDB page的填充率,默认值是100,但其实最高只能填充约15KB的数据,因为InnoDB会预留1/16的空闲空间。在InnoDB文档中,有这么一段话

An innodb_fill_factor setting of 100 leaves 1/16 of the space in clustered index pages free for future index growth.

另外,文档中还有这样一段话

When new records are inserted into an InnoDB clustered index, InnoDB tries to leave 1/16 of the page free for future insertions and updates of the index records. If index records are inserted in a sequential order (ascending or descending), the resulting index pages are about 15/16 full. If records are inserted in a random order, the pages are from 1/2 to 15/16 full.

上面这两段话,综合起来理解,就是

  1. 即便 innodb_fill_factor=100,也会预留1/16的空闲空间,用于现存记录长度扩展用

  2. 在最佳的顺序写入数据模式下,page填充率有可能可以达到15/16

  3. 在随机写入新数据模式下,page填充率约为 1/2 ~ 15/16

  4. 预留1/16这个规则,只针对聚集索引的叶子节点有效。对于聚集索引的非叶子节点以及辅助索引(叶子及非叶子)节点都没有这个规则

  5. 不过 innodb_fill_factor 选项对叶子节点及非叶子节点都有效,但对存储text/blob溢出列的page无效

1.2 关于innodb_ruby项目

innodb_ruby 项目是由Jeremy Cole 和 Davi Arnaut 两位大神开发的项目,可用于解析InnoDB数据结构,用ruby开发而成。他们还维护了另一个众所周知的项目叫 InnoDB Diagrams,相信稍微资深一点的MySQL DBA都应该知道这个项目。

1.3 关于innblock工具

由八怪开发,用于扫描和分析InnoDB page,详见 innblock | InnoDB page观察利器

1.4 阅读本文背景信息

需要假设您对InnoDB的数据结构已经有了一定了解,包括B+树、聚集索引、辅助索引,以及innodb page的一些简单结构。

  • Clustered and Secondary Indexes

  • The Physical Structure of an InnoDB Index

  • InnoDB Row Formats

  • InnoDB Record Structure

  • InnoDB Page Structure


2、测试验证:一层高的InnoDB表聚集索引,最多能存多少条数据

从上面我们知道,一个page最大约能存储15/16容量,扣掉用于存储page header、trailer信息,以及index header、File Segment Header、Infimum&Supremum(两条虚拟记录)等必要的固定消耗之后,实际大约只有15212字节可用于存储用户数据。

这样一来,我们就可以简单测算出一个page大约能存储多少条记录了。

本次用到的测试表,只有一个INT列,同时作为主键(建议横版观看,可左右滑动。或者复制链接到PC端打开观看,效果更佳。下同


  1. # MySQL的版本是Percona Server 5.7.22-22,我自己下载源码编译的
  2. [root@yejr.me#] mysql -Smysql.sock innodb
  3. ...
  4. Server version: 5.7.22-22-log Source distribution
  5. ...
  6. [root@yejr.me]> \s
  7. ...
  8. Server version:     5.7.22-22-log Source distribution
  9. # 创建测试表
  10. [root@yejr.me]> CREATE TABLE `t1` (
  11.  `i` int(10) unsigned NOT NULL AUTO_INCREMENT,
  12.  PRIMARY KEY (`i`)
  13. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

另外,我们知道每条记录都要几个额外存储的数据

  • DB_TRX_ID,6字节

  • DB_ROLL_PTR,7字节

  • Record Header,至少5字节(用上面这个测试表,只需要5字节,不同数据类型需要的header长度也不同,详见 浅析InnoDB Record Header及page overflow

  • 因此,一条数据需要消耗 4(INT列) + 6 + 7 + 5 = 22字节

  • 此外,大约每4条记录就需要一个directory slot,每个slot需要2字节

  • 综上,假设可以存储N条记录,则 N*22 + N/4*2 = 15212,可求得N约等于676

接下来我们验证一下,往该表中持续插入 676 条数据


  1. [root@yejr.me]> insert into t1 select 0;
  2. ...
  3. # 逐次反复执行676次

然后,我们利用 innodb_ruby 工具查看其数据结构

2.1 查看聚集索引page结构

此时t1表的聚集索引树只有一层高,一个page即pageno=3


  1. [root@yejr]# innodb_space -s ibdata1 -T innodb/t1 space-indexes

再用innblock工具扫描佐证一下


  1. [root@yejr]# innblock innodb/t1.ibd scan 16
  2. ...
  3. level0 total block is (1)
  4. block_no:         3,level:   0|*|

2.2 查看其directory slot

可以看到170个slot,其中Infimum记录的owned=1,Supremum记录的owned=5


  1. [root@yejr]# innodb_space -s ibdata1 -T innodb/t1 \
  2. -p 3 page-directory-summary|grep -c -v slot
  3. 170

2.3 查看整个page的全览图

前面是一堆头信息


  1. [root@yejr]# innodb_space -s ibdata1 -T innodb/t1 -p 3 page-illustrate
  2.      Offset ╭────────────────────────────────────────────────────────────────╮
  3.           0 │█████████████████████████████████████▋██████████████████████████│
  4.          64 │█████████▋███████████████████▋████████████▋████████████▋████▋███│
  5. # 大概从这里开始是第一条记录
  6.         128 │█████████████▋████▋████████████████▋████▋████████████████▋████▋█│
  7.         192 │███████████████▋████▋████████████████▋████▋████████████████▋████│
  8. ...
  9. # 中间是用户数据
  10. ...
  11. # 这里是预留的1/16空闲空间
  12.       15872 │                                                                │
  13.       15936 │                                                                │
  14. # 这里是page directory slot,逆序存储
  15. # trailer占用8字节,此后每个slot占用2字节
  16. # 共170个slot
  17.       16000 │                                      █▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋│
  18. ...
  19.       16320 │█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋█▋███████▋│
  20.             ╰────────────────────────────────────────────────────────────────╯
  21. # 最后是统计汇总信息
  22. Legend (█ = 1 byte):
  23.  Region Type                         Bytes    Ratio
  24.  █ FIL Header                           38    0.23%
  25.  █ Index Header                         36    0.22%
  26.  █ File Segment Header                  20    0.12%
  27.  █ Infimum                              13    0.08%
  28.  █ Supremum                             13    0.08%
  29.  █ Record Header                      3380   20.63%
  30.  █ Record Data                       11492   70.14%
  31.  █ Page Directory                      340    2.08%
  32.  █ FIL Trailer                           8    0.05%
  33.  ░ Garbage                               0    0.00%
  34.    Free                               1044    6.37%

可以得到几点信息

  • Record Data共占用11492字节,共676条记录,每条记录17字节(4+6+7)

  • Page Directory共340字节,170个slot,每个slot占用2字节

  • 两条虚拟记录,均占用13字节(含5字节的record header)

  • Record Header共3380字节,共676条记录,每条记录需要5字节头信息(再次提醒,表里字段类型各异,Record Header也会随之不同,仅在本例中只需要5字节。详见 浅析InnoDB Record Header及page overflow

  • 提醒:本次测试是顺序写入,如果是随机写入或批量写入,可能就没办法把15/16的page空间填充的满满当当了

2.4 什么时候发生B+树分裂

如果我们再插入一条记录,就会发现,t1表原本只有一层高的B+树,会分裂成两层高度

[root@yejr.me]> insert into t1 select 0;

再次查看数据结构,注意到此时leaf节点的page数为2,也就是分裂成两层高度了


  1. [root@yejr]# innodb_space -s ibdata1 -T innodb/t1 space-indexes
  2. id    name       root  fseg        fseg_id     used   allocated   fill_factor
  3. 128   PRIMARY    3     internal    1           1      1           100.00%
  4. 128   PRIMARY    3     leaf        2           2      2           0.00%

用 innblock 工具扫描佐证


  1. [root@yejr]# innblock innodb/t1.ibd scan 16
  2. ...
  3. Datafile Total Size:98304
  4. ===INDEX_ID:121
  5. level1 total block is (1)
  6. block_no:         3,level:   1|*|
  7. level0 total block is (2)
  8. block_no:         4,level:   0|*|block_no:         5,level:   0|*|

确认此时发生分裂了,由一层高度分裂成两层,根节点(level=1)pageno=3,叶子节点(level=0)分别为pageno=[4, 5]。


3、理论推演,当innodb表聚集索引达到三层高时,大概可以存储几条记录

3.1 分析根节点page

上述测试表此时是一个两层高的聚集索引,分别是根节点(level=1,pageno=3),叶子节点(level=0,pageno=[4,5])。

此时根节点里只有两条记录,分别指向两个叶子节点pageno=[4, 5]


  1. [root@yejr]# innodb_space -s ibdata1 -T innodb/t1 -p 3 page-records
  2. Record 125: (i=2) → #4
  3. Record 138: (i=382) → #5

再查看根节点详细数据


  1. [root@yejr]# innodb_space -s ibdata1 -T innodb/t1 -p 3 page-dump
  2. #<Innodb::Page::Index:0x00000001a5eb40>:
  3. fil header:
  4. {:checksum=>4010521133,
  5. :offset=>3,
  6. :prev=>nil,
  7. :next=>nil,
  8. :lsn=>4316394,
  9. :type=>:INDEX,
  10. :flush_lsn=>0,
  11. :space_id=>104}
  12. fil trailer:
  13. {:checksum=>4010521133, :lsn_low32=>4316394}
  14. page header:
  15. {:n_dir_slots=>2,
  16. :heap_top=>146,
  17. :garbage_offset=>0,
  18. :garbage_size=>0,
  19. :last_insert_offset=>138,
  20. :direction=>:right,
  21. :n_direction=>1,
  22. :n_recs=>2,
  23. :max_trx_id=>0,
  24. :level=>1,
  25. :index_id=>121,
  26. :n_heap=>4,
  27. :format=>:compact}
  28. fseg header:
  29. {:leaf=>
  30.  <Innodb::Inode space=<Innodb::Space file="innodb/t1.ibd", page_size=16384, pages=6>, fseg=2>,
  31. :internal=>
  32.  <Innodb::Inode space=<Innodb::Space file="innodb/t1.ibd", page_size=16384, pages=6>, fseg=1>}
  33. sizes:
  34.  header           120
  35.  trailer            8
  36.  directory          4
  37.  free           16226
  38.  used             158
  39.  record            26
  40.  per record     13.00
  41. page directory:
  42. [99, 112]
  43. # 2条系统记录,即infimum、supremum这两条虚拟记录
  44. system records:
  45. {:offset=>99,
  46. :header=>
  47.  {:next=>125,
  48.   :type=>:infimum,
  49.   :heap_number=>0,
  50.   :n_owned=>1,
  51.   :min_rec=>false,
  52.   :deleted=>false,
  53.   :length=>5},
  54. :next=>125,
  55. :data=>"infimum\x00",
  56. :length=>8}
  57. {:offset=>112,
  58. :header=>
  59.  {:next=>112,
  60.   :type=>:supremum,
  61.   :heap_number=>1,
  62.   :n_owned=>3,
  63.   :min_rec=>false,
  64.   :deleted=>false,
  65.   :length=>5},
  66. :next=>112,
  67. :data=>"supremum",
  68. :length=>8}
  69. garbage records:
  70. # 物理记录
  71. records:
  72. {:format=>:compact,
  73. :offset=>125,
  74. :header=>
  75.  {:next=>138,
  76.   :type=>:node_pointer,
  77.   :heap_number=>2,
  78.   :n_owned=>0,
  79.  
  80.   # 是聚集索引的min_key
  81.   :min_rec=>true,
  82.   :deleted=>false,
  83.   :nulls=>[],
  84.   :lengths=>{},
  85.   :externs=>[],
  86.   :length=>5},
  87. :next=>138,
  88. :type=>:clustered,
  89. # i=2这条记录(该表第一条记录,我此前把i=1记录给删了)
  90. :key=>[{:name=>"i", :type=>"INT UNSIGNED", :value=>2}],
  91. :row=>[],
  92. :sys=>[],
  93. # 指针指向叶子节点pageno=4,该记录消耗8字节,含4字节的指针
  94. :child_page_number=>4,
  95. :length=>8}
  96. {:format=>:compact,
  97. :offset=>138,
  98. :header=>
  99.  {:next=>112,
  100.   :type=>:node_pointer,
  101.   :heap_number=>3,
  102.   :n_owned=>0,
  103.   :min_rec=>false,
  104.   :deleted=>false,
  105.   :nulls=>[],
  106.   :lengths=>{},
  107.   :externs=>[],
  108.   :length=>5},
  109. :next=>112,
  110. :type=>:clustered,
  111. # i=382这条记录
  112. :key=>[{:name=>"i", :type=>"INT UNSIGNED", :value=>382}],
  113. :row=>[],
  114. :sys=>[],
  115. # 指针指向叶子节点pageno=5,该记录消耗8字节,含4字节的指针
  116. :child_page_number=>5,
  117. :length=>8}

查看根节点整个page的全览图


  1. [root@yejr.me#] innodb_space -s ibdata1 -T innodb/t1 -p 3 page-illustrate
  2.      Offset ╭────────────────────────────────────────────────────────────────╮
  3.           0 │█████████████████████████████████████▋██████████████████████████│
  4.          64 │█████████▋███████████████████▋████████████▋████████████▋████▋███│
  5.         128 │████▋████▋███████▋                                              │
  6.         192 │                                                                │
  7.         256 │                                                                │
  8. ...
  9. ...
  10.       16192 │                                                                │
  11.       16256 │                                                                │
  12.       16320 │                                                      █▋█▋█████▋│
  13.             ╰────────────────────────────────────────────────────────────────╯
  14. Legend (█ = 1 byte):
  15.  Region Type                         Bytes    Ratio
  16.  █ FIL Header                           38    0.23%
  17.  █ Index Header                         36    0.22%
  18.  █ File Segment Header                  20    0.12%
  19.  █ Infimum                              13    0.08%
  20.  █ Supremum                             13    0.08%
  21.  █ Record Header                        10    0.06%
  22.  █ Record Data                          16    0.10%
  23.  █ Page Directory                        4    0.02%
  24.  █ FIL Trailer                           8    0.05%
  25.  ░ Garbage                               0    0.00%
  26.    Free                              16226   99.04%

可以得到几点结论

  • 根节点里共有两条记录,每条记录占用8字节

  • 由于整型只需要4字节,因此我们可推断出指向叶子节点的指针需要占用4字节

  • 每条记录同样需要5字节的record header(不同聚集索引列数据类型,需要的record header也不一样)

  • 减去必要的FIL Header、Index Header等头信息后,非叶子节点可用空间约 16241 字节

  • 综上,假设非叶子节点可以存储N条记录,则 N*13 + N/4*2 = 16241,可求得N约等于1203

  • 既然每个非叶子节点可存储1203条记录,每个叶子节点可存储676条记录,则一个三层高度的InnoDB表聚集索引可以存储 1203*1203*676= 978313284,也就是约9.7亿条记录

  • 所以说,如果表足够“窄”的话,一个三层高的表足够存储上亿条数据,其平均搜索效率并不差,常规的存取效率也不会太差

  • 当然了,如果因为索引使用不当,导致检索效率低下,或者频繁发生锁等待,那要另当别论

3.2 补充测试:在两层高度时,根节点最多可以存储几条记录

我们对上面的t1表持续写入数据,验证在两层高度时,根节点最多可以存储几条记录。

我们继续使用上面的测试表,经验证:在两层高度时,根节点可以存储 1203 条记录,整个表最多 812890 条记录


  1. # 查看总记录数
  2. [root@yejr.me]> select count(*) from t1;
  3. +----------+
  4. | count(*) |
  5. +----------+
  6. |   812890 |
  7. +----------+
  8. # 查看聚集索引层级
  9. [root@yejr.me#] innblock innodb/t1.ibd scan 16
  10. ...
  11. # 存储81万条数据,数据表空间文件大小为27MB
  12. # 换算下,如果是3层高度的表存满,表空间文件大小约3.25GB
  13. Datafile Total Size:28311552
  14. ===INDEX_ID:131
  15. level1 total block is (1)
  16. block_no:         3,level:   1|*|
  17. level0 total block is (1203)
  18. block_no:    4,level: 0|*|block_no:    5,level: 0|*|block_no:    6,level: 0|*|
  19. ...
  20. ...
  21. block_no: 1232,level: 0|*|block_no: 1233,level: 0|*|block_no: 1234,level: 0|*|
  22. # 查看根节点page数据结构图
  23. [root@yejr.me#] innodb_space -s ibdata1 -T innodb/t1 -p 3 page-illustrate
  24. ...
  25. Legend (█ = 1 byte):(固定长度的头信息部分我都给去掉了,下同)
  26.  Region Type                         Bytes    Ratio
  27. ...
  28.  █ Record Header                      6015   36.71%
  29.  █ Record Data                        9624   58.74%
  30.  █ Page Directory                      602    3.67%
  31.  █ FIL Trailer                           8    0.05%
  32.  ░ Garbage                               0    0.00%
  33.    Free                                 15    0.09%
  34.    #最后只剩15字节空闲,而不像叶子节点那样有1/16空闲空间

再再次提醒,这都是基于只有一个INT列并作为主键的测试结果。如果是其他主键类型,或者不是顺序追加写入的模式,则结论可能就不是这个了。


4、疑问1:innodb page预留的1/16空闲空间做什么用的

测试到上面时,我们可能会个疑问:什么情况下,能把预留的1/16那部分空闲空间给用上呢?

我们再回顾下前面的文档说明:

An innodb_fill_factor setting of 100 leaves 1/16 of the space in clustered index pages free for future index growth.

凭直觉,我认为是用于需要“增长(读cháng)/扩充”方式更新某条记录时所需,而不是用于写入新记录。例如,c1列定义为VARCHAR(10),第一次存储时只写了5个字节,后来做了一次更新,把它从5个字节增长到10个字节,称为“增长”更新。像下面这样


  1. # c1列原值是 'abcde'
  2. update t1 set c1='abcdeabcde' where i=1;

我们创建一个新的测试表t2,这次增加一个可变长字符串列c1


  1. CREATE TABLE `t2` (
  2.  `i` int(10) unsigned NOT NULL AUTO_INCREMENT,
  3.  `c1` varchar(10) NOT NULL DEFAULT '',
  4.  PRIMARY KEY (`i`)
  5. ) ENGINE=InnoDB;

计算一条记录大概需要多少字节

  • DB_TRX_ID,6字节

  • DB_ROLL_PTR,7字节

  • Record Header,6字节(基础是5字节,外加有个变长列还需要1个字节,共6字节)

  • 因此,一条数据需要消耗 4(INT列) + 6(VARCHAR(10),但目前只存了5个字符)+6+7+5=28字节

  • 此外,大约每4条记录就需要一个directory slot,每个slot需要2字节

  • 综上,假设可以存储N条记录,则 N*28 + N/4*2 = 15212,可求得N约等于534

插入534条记录后,查看page数据结构图


  1. [root@yejr.me#] innodb_space -s ibdata1 -T innodb/t2 -p 3 page-illustrate
  2. ...
  3. Legend (█ = 1 byte):
  4.  Region Type                         Bytes    Ratio
  5. ...
  6.  █ Record Header                      3204   19.56%
  7.  █ Record Data                       11748   71.70%
  8.  █ Page Directory                      268    1.64%
  9.  █ FIL Trailer                           8    0.05%
  10.  ░ Garbage                               0    0.00%
  11.    Free                               1036    6.32%    

用innblock工具佐证一下


  1. [root@yejr.me#] innblock innodb/t2.ibd scan 16
  2. ...
  3. Datafile Total Size:98304
  4. ===INDEX_ID:136
  5. level0 total block is (1)
  6. block_no:         3,level:   0|*|

确认当前只有一层高度,还没分裂成两层。

进行一次 “增长”更新 一条记录后,看能不能把预留的空间给利用起来而不是分裂出一个新page


  1. [root@yejr.me]>update t2 set c1='abcdeabcde' where i=1;
  2. Query OK, 1 row affected (0.00 sec)
  3. Rows matched: 1  Changed: 1  Warnings: 0
  4. # 确认还是只有一层高度,树没有分裂
  5. [root@yejr.me#] innblock innodb/t2.ibd scan 16
  6. ...
  7. Datafile Total Size:98304
  8. ===INDEX_ID:136
  9. level0 total block is (1)
  10. block_no:         3,level:   0|*|    
  11. # 再查看下page数据结构图
  12. [root@yejr.me#] innodb_space -s ibdata1 -T innodb/t2 -p 3 page-illustrate
  13. ...
  14. Legend (█ = 1 byte):
  15.  Region Type                         Bytes    Ratio
  16. ...
  17.  █ Record Header                      3204   19.56%
  18.  █ Record Data                       11753   71.73%
  19.  █ Page Directory                      266    1.62%
  20.  █ FIL Trailer                           8    0.05%
  21.  ░ Garbage                              28    0.17%
  22.    Free                               1005    6.13%

从上面这个结果可以看到几点

  • 看到Garbage是28字节,也就是i=1的那条旧数据(长度不够存储新记录,需要新写入并删除旧记录)

  • 看到Record Data增加了5字节,因为我们对i=1那条记录的c1列增加了5字节

  • 看到Free少了31字节,那是因为“增长”更新后的i=1记录总长度是31字节,它需要从Free里分配新空间来存储

因此我们确认:聚集索引没有分裂,而是优先把Free空间给利用起来了


5、疑问2:Garbage空间可以被重用吗

5.1 先回答问题,Garbage空间是可以被重用的

在我们做逐次“增长”更新了50条记录后,这时发现Garbage比较大,但Free已经几乎用完了


  1.  Region Type                         Bytes    Ratio
  2. ...
  3.  █ Record Header                      3204   19.56%
  4.  █ Record Data                       11998   73.23%
  5.  █ Page Directory                      268    1.64%
  6.  █ FIL Trailer                           8    0.05%
  7.  ░ Garbage                             756    4.61%
  8.    Free                                 30    0.18%

也就是在这时,如果按照常理,再做一次“增长”更新,就会造成当前的page存储不下,会进行分裂,但事实上真是如此吗?

在继续做一次“增长”更新后,我们发现,实际上此时会把Garbage的空间给重整了,然后继续利用起来,而不是立即进行分裂


  1. # 已有50条记录被“增长”更新了
  2. [root@yejr.me]>select count(*) from t2 where c1='abcdeabcde';
  3. +----------+
  4. | count(*) |
  5. +----------+
  6. |       50 |
  7. +----------+
  8. 1 row in set (0.00 sec)
  9. # 继续“增长”更新
  10. [root@yejr.me]>update t2 set c1='abcdeabcde' where i=52;
  11. Query OK, 1 row affected (0.00 sec)
  12. Rows matched: 1  Changed: 1  Warnings: 0
  13. # 确认更新成功
  14. [root@yejr.me]>select count(*) from t2 where c1='abcdeabcde';
  15. +----------+
  16. | count(*) |
  17. +----------+
  18. |       51 |
  19. +----------+
  20. # 查看数据结构
  21.  Region Type                         Bytes    Ratio
  22. ...
  23.  █ Record Header                      3204   19.56%
  24.  █ Record Data                       12003   73.26%
  25.  █ Page Directory                      268    1.64%
  26.  █ FIL Trailer                           8    0.05%
  27.  ░ Garbage                               0    0.00%
  28.    Free                                781    4.77%
  29.    # 此时发现Garbage为0,而Free值增大了,明显是把Garbage的空间给重整后再次利用了,很好

我们可以再次得到几条结论

  • 一条记录被“增长”更新后,旧记录会被放到Garbage队列中,除非此时插入新记录的长度小于等于旧记录的长度,否则该记录总是不会被重用起来(也可参考这篇文章 innblock | InnoDB page观察利器

  • 当空闲空间全部用完后,若此时Garbage队列不为0的话,则会对其进行重整后,变成可用空间再次被分配

  • 如果是“缩短”的更新方式,缩减的空间并不会进入Garbage队列,而是被标记为碎片空间,这种无法被重用(除非全表重建)

5.2 Garbage空间延伸测试,更新数据的数据后面有其他数据

再来看个更为神奇的案例(这次更新的记录,在它后面有其他记录“阻碍”它)


  1. # 插入两条记录
  2. insert into t2 select 0, 'abcde';
  3. insert into t2 select 0, 'abcde';
  4. # 观察数据结构(只保留几个有用信息)
  5.  █ Record Header                        12    0.07%
  6.  █ Record Data                          44    0.27%
  7.  ░ Garbage                               0    0.00%
  8.    Free                              16196   98.85%
  9.    
  10. # 对第一条记录先做一次“增长”更新
  11. update t2 set c1='abcdeabcde' where i=1;
  12. # 观察数据结构(只保留几个有用信息)
  13.  █ Record Data                          49    0.30%
  14.  ░ Garbage                              28    0.17%
  15.    Free                              16163   98.65%
  16.    
  17. # 再做一次“缩短”更新
  18. update t2 set c1='abcdeabc' where i=1;
  19. # 观察数据结构(只保留几个有用信息)
  20.  █ Record Data                          47    0.29%
  21.  ░ Garbage                              28    0.17%
  22.    Free                              16165   98.66%
  23.    
  24. # 又做一次“增长”更新
  25. update t2 set c1='abcdeabcde' where i=1;
  26. # 观察数据结构(只保留几个有用信息)
  27.  █ Record Data                          49    0.30%
  28.  ░ Garbage                              59    0.36%
  29.    Free                              16132   98.46%    

最后发现Garbage队列中有两条记录,也就是两次“增长”更新都导致旧记录被删除,无法被重用。即便第二次是“缩短”更新后产生了剩余碎片,然后再次被“增长”更新,也无法原地更新,需要新写入一条记录。

5.3 Garbage空间延伸测试,更新数据的数据后面没有其他数据

再做个下面的测试案例。这次表里只有一条记录(在它后面没有其他记录“阻碍”它),那么在后面的更新中,都可以原地更新,即便是“增长”更新,旧记录也不需要先被删除后新写一条记录。


  1. # 只插入一条记录
  2. insert into t2 select 0, 'abcde';
  3. # 观察数据结构(只保留几个有用信息)
  4.  █ Record Data                          22    0.13%  
  5.  ░ Garbage                               0    0.00%  
  6.    Free                              16224   99.02%  
  7.    
  8. # 先做一次“增长”更新
  9. update t2 set c1='abcdeabcde' where i=1;
  10. # 观察数据结构
  11.  █ Record Data                          27    0.16%
  12.  ░ Garbage                               0    0.00%
  13.    Free                              16219   98.99%
  14.    
  15. # 再做一次“缩短”更新(缩短了两个字节)
  16. update t2 set c1='abcdeabc' where i=1;
  17. # 观察数据结构
  18.  █ Record Data                          25    0.15%
  19.  ░ Garbage                               0    0.00%
  20.    Free                              16221   99.01%
  21.        
  22. # 又做一次“增长”更新
  23. update t2 set c1='abcdeabcde' where i=1;
  24. # 观察数据结构(和第一次被“增长”更新后一样了)
  25.  █ Record Data                          27    0.16%
  26.  ░ Garbage                               0    0.00%
  27.    Free                              16219   98.99%

6、要点总结

  1. InnoDB聚集索引由非叶子节点(non leaf page)和叶子节点(leaf page)组成

  2. 在叶子节点中需要存储整行数据(除了overflow的部分列),因此可存储的记录数一般更少些

  3. 在non leaf page中只需要存储聚集索引列(主键键值),因此可存储的记录数一般更多些

  4. 对变长列,尽量(比如从业务上想办法)不要反复变长(无论是增长还是缩短)更新

  5. innodb_ruby不错,不过解析5.6及以上版本可能有些地方会不准确,可以用innblock工具辅助配合

我不是源码级MySQL内核开发者,水平有限,文中难免有误之处,还请多指教。

Enjoy MySQL :)

延伸阅读

  • 15.6.2.2 The Physical Structure of an InnoDB Index,http://t.cn/AiR7o1rv

  • 15.6.2.3 Sorted Index Builds,http://t.cn/AiR7oQr8

  • Visualizing the impact of ordered vs. random index insertion in InnoDB,http://t.cn/AiR7ooSy

  • The physical structure of InnoDB index pages,http://t.cn/AiR7oIMa

  • B+Tree index structures in InnoDB,http://t.cn/RcfZ0n7

  • The physical structure of records in InnoDB,http://t.cn/AiTPFXT5

  • On learning InnoDB: A journey to the core,http://t.cn/Rlp9V1s

  • innodb_diagrams on github,http://t.cn/8swyTdg

  • innblock | InnoDB page观察利器

  • 浅析InnoDB Record Header及page overflow


最后,欢迎扫码订阅《乱弹MySQL》专栏,快人一步获取我最新的MySQL技术分享

</article>

[转帖]InnoDB表聚集索引层高什么时候发生变化的更多相关文章

  1. MySQL InnoDB表和索引之聚簇索引与第二索引

    MySQL InnoDB表和索引之聚簇索引与第二索引 By:授客QQ:1033553122 每个InnoDB表都有一个称之为聚簇索引(clustered index)的特殊索引,存储记录行数据.通常, ...

  2. 聚集索引、非聚集索引、聚集索引组织表、堆组织表、Mysql/PostgreSQL对比、联合主键/自增长、InnoDB/MyISAM(引擎方面另开一篇)

    参考了多篇文章,分别记录,如下. 下面是第一篇的总结 http://www.jb51.net/article/76007.htm: 在MySQL中,InnoDB引擎表是(聚集)索引组织表(cluste ...

  3. mysql innodb存储引擎的聚集索引

    InnoDB聚集索引 MySQL有没有支持聚集索引,取决于采用哪种存储引擎. MySQL InnoDB一定会建立聚集索引,所谓聚集,指实际数据行和相关的键值保存在一块,这也决定了一个表只能有一个聚集索 ...

  4. mysql聚集索引的优缺点

    聚簇索引并不是一种单独的索引类型,而是一种数据存储方式(不是数据结构,而是存储结构),具体细节依赖于其实现方式,但innodb的聚簇索引实际上是在同一个结构中保存了btree索引和数据行. 当表有索引 ...

  5. MySQL聚集索引和非聚集索引

    索引分为聚集索引和非聚集索引,mysql中不同的存储引擎对索引的底层实现可能会不同,这里只关注mysql的默认存储引擎InnoDB. 利用下面的命令可以查看默认的存储引擎 show variables ...

  6. B+树,B树,聚集索引,非聚集索引

    简介: B+树中只有叶子节点会带有指向记录的指针,而B树则所有节点都带有 B+树索引可以分为聚集索引和非聚集索引 mysql使用B+树,其中Myisam是非聚集索引,innoDB是聚集索引 聚簇索引索 ...

  7. Mysql聚集索引的使用

    聚集索引 聚簇索引并不是一种单独的索引类型,而是一种数据存储方式(不是数据结构,而是存储结构),具体细节依赖于其实现方式,聚簇索引实际上是在同一个结构中保存了btree索引和数据行. innodb将通 ...

  8. SQLSERVER聚集索引与非聚集索引的再次研究(上)

    SQLSERVER聚集索引与非聚集索引的再次研究(上) 上篇主要说聚集索引 下篇的地址:SQLSERVER聚集索引与非聚集索引的再次研究(下) 由于本人还是SQLSERVER菜鸟一枚,加上一些实验的逻 ...

  9. MySQL InnoDB表--BTree基本数据结构

    MySQL InnoDB表是索引组织表这一点应该是每一个学习MySQL的人都会首先学到的知识,这代表这表中的数据是按照主键顺序存储,也就是说BTree的叶子节点存储了所有该行的数据. 我最开始是搞Or ...

  10. SQL Server临界点游戏——为什么非聚集索引被忽略!

    当我们进行SQL Server问题处理的时候,有时候会发现一个很有意思的现象:SQL Server完全忽略现有定义好的非聚集索引,直接使用表扫描来获取数据.我们来看看下面的表和索引定义: CREATE ...

随机推荐

  1. Vue 2 和 Vue 3 中 toRefs的区别

    摘要:本文将介绍 Vue 2 和 Vue 3 中 toRefs 函数的不同用法和行为,并解释其在各个版本中的作用. 正文: Vue 是一款流行的 JavaScript 框架,用于构建用户界面.在 Vu ...

  2. .NET周报【10月最后一期 2022-11-01】

    精选要闻 .NET 7 NativeAOT比.NET单文件发布文件小80% https://twitter.com/JamesNK/status/1584919726861737984?s=20&am ...

  3. 技术实操丨SoundNet迁移学习之由声音分类到语音情感识别

    摘要:声音也是识别对象的一种重要数据源.其中根据声音来识别声音所处的环境也是语音识别的研究内容之一. 一.思路 1.SoundNet模型在视频数据中先预训练,视频任务可能是场景识别,可参考这篇文章So ...

  4. 云图说|AppCube零代码,开启无码新生活

    阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说).深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云.更多精彩内容请单击此处. 摘要: 应用魔方 App ...

  5. Python图像处理丨图像的灰度线性变换

    摘要:本文主要讲解灰度线性变换. 本文分享自华为云社区<[Python图像处理] 十五.图像的灰度线性变换>,作者:eastmount. 一.图像灰度线性变换原理 图像的灰度线性变换是通过 ...

  6. 跟我读论文丨ACL2021 NER BERT化隐马尔可夫模型用于多源弱监督命名实体识别

    摘要:本文是对ACL2021 NER BERT化隐马尔可夫模型用于多源弱监督命名实体识别这一论文工作进行初步解读. 本文分享自华为云社区<ACL2021 NER | BERT化隐马尔可夫模型用于 ...

  7. 讲透学烂二叉树(五):分支平衡—AVL树与红黑树伸展树自平衡

    简叙二叉树 二叉树的最大优点的就是查找效率高,在二叉排序树中查找一个结点的平均时间复杂度是O(log₂N): 在<讲透学烂二叉树(二):树与二叉/搜索/平衡等树的概念与特征>提到 二叉排序 ...

  8. SBOM落地的关键一步——漏洞可利用性交流(VEX)

    SolarWinds 网络安全事件的影响,加上 Log4j 漏洞对众多知名企业产生难以估量的后果,使软件供应链安全成为安全领域的热门话题,并且SBOM现在成为网络安全漏洞计划的一个重要组成部分. SB ...

  9. 基于BaseHTTPRequestHandler的HTTP服务器基础实现

    1. BaseHTTPRequestHandler介绍 BaseHTTPRequestHandler是Python中的一个基类,属于http.server模块,用于处理HTTP请求的基本功能.它提供了 ...

  10. GPT-4测评,大家先别急,图片输入还没来

    昨天GPT-4朋友圈刷屏,我更新了一篇小文章,极简罗列GPT-4的一些情报: 1 ChatGPT Plus用户才可试用GPT-4 2 试用阶段每四小时最多100条信息 3 知识库还是2021年 4 上 ...