MySQL InnoDB表 ,Oracle 的索引组织表 (IOT),SQL Server 的聚集索引,本质上是同一个东西,就是按照主键索引的 B-Tree 结构来组织的,索引非叶子节点只存索引键值 + 指向子节点的指针,索引叶子节点保存了完整的行记录。表中的数据行是按主键排序顺序存储的,并不是指数据在磁盘上像排队一样严格连续地存放,而是指数据在逻辑上按主键顺序存储。
InnoDB表Table以ID列为主键,ID有100到800的8行数据,400是索引非叶子节点只存索引键值100到400的这4行,800是索引非叶子节点只存索引键值500到800的这4行,100是行1的列,200是行2的列,行1和行2在逻辑上是顺序存储的(就算随机插入时遇到块的空间不够,也有Mysql InnoDB页分裂机制,Oracle行迁移机制这样的类似机制保证这个顺序),但是行1的物理位置和行2的物理位置可能相邻(顺序插入的情况下,2行数据在同一个数据块或相邻数据块),也可能相差十万八千里(随机插入的情况下,2行数据分散到了相隔很远的数据块)
400----------800(非叶子节点)
100 200 300 400 500 600 700 800(叶子节点)
行1 行2 行3 行4 行5 行6 行7 行8
聚集索引根据数据行的键值在表或视图中排序和存储这些数据行。
只有当表包含聚集索引时,表中的数据行才按排序顺序存储。 如果表没有聚集索引,则其数据行存储在一个称为堆的无序结构中。
Mysql官方文档https://dev.mysql.com/doc/refman/9.6/en/innodb-physical-structure.html
当将新记录插入 InnoDB 聚簇索引 时,InnoDB 尝试在页中保留 1/16 的空间以供将来插入和更新索引记录。如果索引记录按顺序插入(升序或降序),则生成的索引页大约是 15/16 满的。如果记录是随机插入的,则页面是 1/2 到 15/16 满的。
Oracle ROWID
https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/ROWID-Pseudocolumn.html
For each row in the database, the ROWID pseudocolumn returns the address of the row
ROWID 主要记录以下四部分信息:数据对象编号、数据文件编号、数据块编号、行编号。数据对象编号、数据文件编号、数据块编号、行编号都是数据库内部的逻辑单元,而不是磁盘物理地址(柱面/磁头/扇区),但是我们可以通过ROWID这个值快速定位到它在磁盘上的物理地址。
官方文档从来没说这个ROWID是行在磁盘上的物理地址比如柱面/磁头/扇区,当MySQL InnoDB表 ,Oracle 的索引组织表 (IOT),SQL Server 的聚集索引没有主键而是使用ROWID来当主键排序存储时,这个ROWID值本身是顺序的(就算随机插入这个时候也有Mysql InnoDB页分裂机制,Oracle行迁移机制这样的类似机制来保证顺序ROWID值本身顺序),ROWID并不代表行在磁盘上像排队一样严格连续地存放,只不过可以通过这个ROWID可以快速定位到哪个文件哪个块,Oracle 知道需要读取哪个文件的哪个块后,会通过系统调用请求操作系统读取该数据块。操作系统则负责将文件中的逻辑偏移量(块编号 × 块大小)转换为磁盘上的物理扇区地址,最终完成读取。
Postgresql ctid
https://www.postgresql.org/docs/current/ddl-system-columns.html
The physical location of the row version within its table. Note that although the ctid can be used to locate the row version very quickly, a row's ctid will change if it is updated or moved by VACUUM FULL. Therefore ctid should not be used as a row identifier. A primary key should be used to identify logical rows.
行版本在其表中的物理位置(不代表磁盘上的物理地址比如柱面/磁头/扇区)。注意,尽管 ctid 可用于极快地定位行版本,但如果行被更新或被 VACUUM FULL 移动,行的 ctid 会发生变化。因此,ctid 不应作为行标识符使用。应使用主键来标识逻辑行。
那么一张表有主键ID有数据1亿零1条记录,但是数据是从1,3,4开始,没有2,如果这个时候插入2,因为2得排在3,4之前,是不是3,4后面的1亿条数据都得重新挪位置?如果是这样的话,插入2就会引发2 后面的所有数据(约1亿条)都往后挪一位,那这样的操作在任何生产系统上都是无法接受的。
InnoDB 引擎当然也想到了这一点,它并没有采用这种低效的"物理重排"方式。实际上,InnoDB 通过页(Page) 结构和页分裂(Page Split) 机制,以一种相对轻量级的方式解决了这个问题。InnoDB 通过页(Page) 结构中有提到如果索引记录按顺序插入(升序或降序),则生成的索引页大约是 15/16 满的。
Oracle
行链接:INSERT一行时,这行太长,导致数据块没有可用空间装下这一行,这时会将一行数据拆分成多片,分散存储在多个数据块中。读取一行数据,本来读一个数据块就够了,现在要读 2 个甚至更多块,I/O 次数成倍增加。
行迁移:UPDATE一行时,行原本能装下,但更新后变长了,原块没有足够剩余空间,将整行数据整体搬家到一个新块,原块只留下一个指针指向新块。读取一行数据,本来读一次 I/O,现在变成:先读老块(找到指针) + 再读新块(找到数据) = 至少 2 次 I/O。对于频繁通过索引访问的表,性能下降会非常明显。
Oracle 数据块存储参数 PCTFREE默认值 10%:当这个块的剩余可用空间只有10%时,这个块不能再插入新数据,只能用于已有数据的更新操作
Oracle 数据块存储参数 PCTUSED默认值 40%:当这个块的使用率降到 40% 时表示这个块又可以继续插入新数据