当前位置: 首页 > 科技观察

眼见为实,看看MySQL中的隐藏列!

时间:2023-03-17 14:46:00 科技观察

在介绍mysql的多版本并发控制MVCC的过程中,我们提到mysql中有一些隐藏的列,比如行ID,事务ID,回滚指针等,不知道大家有没有同样好奇如我一般。我们如何才能真正看到这些隐藏列的值呢?在本文中,我们将重点关注许多隐藏列中的行IDDB_ROW_ID。实际上,将行ID称为隐藏列是不准确的,因为它不是真实的。列DB_ROW_ID实际上是非空唯一列的别名。在揭开它的神秘面纱之前,我们先看看官方文档的描述:如果一个表有一个PRIMARYKEY或UNIQUENOTNULL索引,并且由一个整数类型的单列组成,那么可以使用_rowid来引用被索引的ColumninSELECTstatements简单翻译一下,如果表中有主键或者非空唯一索引,而且只是整型列组成,那么可以用SELECT语句直接查询_rowid,而其值这个_rowid将引用索引列的值。重点关注文档中提到的关键字,如主键、唯一索引、非空、单列、数字类型。接下来,我们就从这几个角度出发,探寻神秘的隐藏字段_rowid。1.如果有主键,首先检查主键是否设置,并且是数值类型,使用如下语句建表:CREATETABLE`table1`(`id`bigint(20)NOTNULLPRIMARYKEY,`name`varchar(32)DEFAULTNULL)ENGINE=InnoDB;插入三条测试数据后,执行如下查询语句,在select查询语句中直接查询_rowid:select*,_rowidfromtable1查看执行结果,可以正常查询到_rowid:可以看到设置了主键,而主键字段是数值类型的情况下_rowid直接引用主键字段的值。对于这种可以通过select语句查询的情况,可以称之为显式rowid。回顾前面提到的文档中的几个关键字,然后再考虑其他情况。由于主键必须是非空字段,所以我们来看一下主键是非数值字段的情况。创建表如下:CREATETABLE`table2`(`id`varchar(20)NOTNULLPRIMARYKEY,`name`varchar(32)DEFAULTNULL)ENGINE=InnoDB;对table2执行上面同样的查询,结果是无法查询到_rowid的错误,这证明如果主键字段是非数值类型,则无法直接查询到_rowid。2.没有主键,有唯一索引。测试完上面两种类型的主键,我们再来看看表中没有主键但有唯一索引时的情况。先测试数值型字段添加非空唯一索引的情况,建表如下:CREATETABLE`table3`(`id`bigint(20)NOTNULLUNIQUEKEY,`name`varchar(32))ENGINE=创新数据库;查询可以正常执行,_rowid是指唯一索引所在列的值:唯一索引与主键的区别在于唯一索引所在的字段可以为NULL。在上面的table3中,在唯一索引所在的列上添加了一个NOTNULL约束。如果我们删除这个非空约束,我们仍然可以显式查询_rowid吗?接下来,创建另一个表。不同的是在唯一索引所在的列上,不添加非空约束:CREATETABLE`table4`(`id`bigint(20)UNIQUEKEY,`name`varchar(32))ENGINE=InnoDB;执行查询语句,在这种情况下,Unabletoexplicitlyquery_rowid:与主键类似,我们将测试在非数值字段上添加唯一索引的情况。接下来建表时,给字符类型的字段添加唯一索引,并添加非空约束:CREATETABLE`table5`(`id`bigint(20),`name`varchar(32)NOTNULLUNIQUEKEY)ENGINE=创新数据库;同样不能显示查询到_rowid:综合以上三种情况的测试结果可以得出,当没有主键但存在唯一索引时,只在数值类型的字段上增加唯一索引,并且添加了字段只有在指定了非空约束的情况下,才能显式查询到_rowid,_rowid是指这个唯一索引字段的值。3、存在联合主键或联合唯一索引在上面的测试中,我们都是对单个列应用主键或唯一索引,那么如果使用联合主键或联合唯一索引会怎样呢?我们先看看官方文档中的描述:如果有一个由单个整数列组成的PRIMARYKEY,_rowid指的是PRIMARYKEY列。如果有PRIMARYKEY但它不包含单个整数列,则不能使用_rowid。简单即如果主键存在且只有一列数值类型,那么_rowid的值就是指主键。如果主键由多个列组成,_rowid将不可用。根据这个描述,我们来测试一下联合主键的情况。下面两列数值型字段作为联合主键建表:CREATETABLE`table6`(`id`bigint(20)NOTNULL,`no`bigint(20)NOTNULL,`name`varchar(32),PRIMARYKEY(`id`,`no`))ENGINE=InnoDB;查询到无法显示执行结果的_rowid:同样,这个理论也可以应用到唯一索引上,如果非空唯一索引不是由单个列组成的,那么不能直接查询得到_rowid。这个测试过程就省略了,有兴趣的朋友可以自己试试。4、存在多个唯一索引在mysql中,每张表只能有一个主键,但可以有多个唯一索引。那么如果同时有多个符合规则的唯一索引,会引用哪一个作为_rowid的值呢?旧规则,或官方文档的答案:否则,如果该索引由单个整数列组成,_rowid指的是第一个UNIQUENOTNULL索引中的列。如果第一个UNIQUENOTNULL索引不包含单个整数列,则不能使用_rowid。简单翻译一下,如果表中第一个非空唯一索引只包含一个整型字段,那么_rowid会引用这个字段的值。否则,如果第一个非空唯一索引不满足这个条件,那么_rowid将不可用。在下表中创建两个均符合规则的唯一索引:CREATETABLE`table8_2`(`id`bigint(20)NOTNULL,`no`bigint(20)NOTNULL,`name`varchar(32),UNIQUEKEY(没有),UNIQUEKEY(id))ENGINE=InnoDB;查看查询语句的执行结果:可以看到_rowid的值与no列的值相同,证明_rowid会严格选择最先创建的唯一索引作为其引用。那么,如果表中创建的第一个唯一索引不符合_rowid的引用规则,而第二个唯一索引满足规则,那么在这种情况下,是否可以显式查询_rowid呢?针对这种情况,我们建表如下,表中第一个索引是联合唯一索引,第二个索引是单列唯一索引。我们再测试一下:CREATETABLE`table9`(`id`bigint(20)NOTNULL,`no`bigint(20)NOTNULL,`name`varchar(32),UNIQUEKEY`index1`(`id`,`no`),UNIQUEKEY`index2`(`id`))ENGINE=InnoDB;查询可以看到虽然有单列非Empty唯一索引,但是因为按顺序选择的第一个不符合要求,所以还是不能直接查询_rowid:如果创建语句的顺序把上面的uniqueindex倒过来,那么_rowid就可以正常显式查询了。5.同时存在主键和唯一索引从上面的例子我们可以看出,唯一索引的定义顺序会决定_rowid应用哪个索引,那么当主键和唯一索引同时存在时同时,定义顺序会影响它的引用吗?按照以下语句创建两张表,只是创建主键和唯一索引的顺序不同:CREATETABLE`table11`(`id`bigint(20)NOTNULL,`no`bigint(20)NOTNULL,PRIMARYKEY(id),UNIQUEKEY(no))ENGINE=InnoDB;CREATETABLE`table12`(`id`bigint(20)NOTNULL,`no`bigint(20)NOTNULL,UNIQUEKEY(id),PRIMARYKEY(no))ENGINE=InnoDB;检查运行结果:是得出结论,当同时存在符合条件的主键和唯一索引时,无论创建顺序如何,_rowid都会优先引用主键字段的值。6、没有满足条件的主键和唯一索引。上面我们把可以直接通过select语句查询到的称为显式_rowid。在其他情况下,虽然不能显式查询到_rowid,但它仍然一直存在。在这种情况下我们可以称它为隐式_rowid。事实上,如果没有默认主键,innoDB会生成一个6字节的无符号数作为自增_rowid,所以最大值为2^48-1,达到最大值后从0开始计数。接下来,我们创建一个没有主键和唯一索引的表。基于此表,我们探索隐式_rowid。创建表`table10`(`id`bigint(20),`name`varchar(32))ENGINE=InnoDB;首先,我们需要找到mysql的进程pid:ps-ef|grepmysqld可以看到,mysql的进程pid是2068:在开始之前,我们还需要做一些准备工作。InnoDB其实维护了一个全局变量dictsys.row_id。没有主键的表将共享此row_id。在插入数据的时候,这个全局的row_id会作为自己的主键,然后给这个全局变量加1。接下来我们需要用到gdb调试相关技术,gdb是Linux下的调试工具,可以用来调试可执行文件。在服务器上,先通过yuminstallgdb安装。安装完成后,通过以下gdb命令将row_id改为1:gdb-p2068-ex'pdict_sys->row_id=1'-batch命令执行结果:在一个空表中插入3行数据:INSERTITOtable10VALUES(100000001,'Hydra');INSERTITOtable10VALUES(100000002,'Trunks');INSERTITOtable10VALUES(100000003,'Susan');查看表中的数据,对应的_rowid理论上为1~3:然后使用gdb命令将row_id修改为最大值2^48,已经超过了dictsys.row_id的最大值:gdb-p2068-ex'pdict_sys->row_id=281474976710656'-批量命令执行结果:向表中插入三条数据:INSERTITOtable10VALUES(100000004,'King');INSERTITOtable10VALUES(100000005,'Queen');INSERTITOtable10VALUES(100000006,'Jack');查看表中的所有数据,可以看到第一次插入的三个数据中有两个数据被覆盖:为什么会有数据覆盖?我们分析这个结果。首先,在第一次插入数据前,_rowid为1,插入的3条数据对应的_rowid分别为1、2、3。如下图所示:手动设置_rowid超过最大值后,插入的数据下次插入数据时_rowid会再次从0开始,所以第二次插入的三个数据的_rowid应该是0、1、2。此时要插入的数据如下:当_rowid相同时出现时,新插入的数据会根据_rowid覆盖原来的数据。流程如图:所以当表中的主键或唯一索引不满足上述要求时,innoDB使用的隐式_rowid存在一定的风险。虽然2^48的值很大,但还是有可能用完的。当_rowid耗尽时,之前的记录将被覆盖。从这个角度也可以提醒大家,建表的时候一定要创建主键,否则可能会出现数据覆盖。本文转载自微信公众号《码农人参》