在执行update语句时,InnoDB对更新主键和不更新主键这两种情况有截然不同的处理方式。
【不更新主键】
- 就地更新
- 在更新记录时,对于被更新的每个列来说,如果更新后的列与更新前的列占用的存储空间一样大,那么可以进行就地更新,也就是直接在原记录的基础上修改对应列的值。
- 先删除旧记录,再插入新记录
- 如果有任何一个被更新的列在更新前后占用的存储空间大小不一致,那么就需要先把这条旧记录从聚簇索引页面中删除,然后在根据更新后列的值创建一条新的记录并插入到页面中。
- 这里的删除,是真正的删除,也就是把这条记录从正常记录链表中移除并加入到垃圾链表中。
- 是由用户线程同步执行真正的删除操作,而不是DELETE语句中进行purge操作时使用的专门线程。
- 如果新创建的记录占用的存储空间不超过旧记录占用的空间,那么可以直接重用加入到垃圾链表中的旧记录所占用的存储空间,否则,需要在页面中新申请一块空间供新记录使用。
- 如果本页面已经没有可用的空间,就需要进行页面分裂操作,然后再插入新的记录。
- 更新操作对应TRX_UNDO_UPD_EXIST_REC类型的undo日志结构,如下图所示:
【注】其中大部分属性与DELETE操作的redo日志相同。其中不同的如下说明:
- n_updated
表示本条UPDATE语句执行后将有几个列被更新
- 被更新的列更新前信息 <pos, old_len, old_value>列表
被更新列在记录中的位置、更新前该列占用的存储空间大小、更新前该列的真实值。
- 演示更新操作生成undo日志。
我们先插入两条记录:
代码语言:javascript复制BEGIN; # 显示开启一个事务,假设该事务的事务id为100
# 插入两条记录
INSERT INTO tb_user(id, name, city) VALUES(1, 'muse','北京市'), (2, 'bob','上海市');
# 删除一条记录DELETE FROM tb_user WHERE id = 1;# 更新一条记录UPDATE tb_user SET name='sam', city='成都市' where id = 2;
【解释】
这个UPDATE语句更新的列的大小都没有改动,所以可以采用就地更新的方式来执行。
在真正改动页面记录前,会先记录一条类型为TRX_UNDO_UPD_EXIST_REC的undo日志。
其中,<0, 4, 2><3, 3, 'bob'>占用空间等于:(1 1 4) (1 1 3) =11,最后,再加上len of index_col_info属性本身占2个字节,所以总共13字节。即:len of index_col_info=13。
【更新主键】
- 步骤一:将旧记录进行delete mark操作 此时仅执行delete mark操作。而在事务提交后,才由专门的线程执行purge操作,从而把它加入到垃圾链表中。
【注】之所以只对旧记录执行delete mark操作,是因为别的事务也可能同时访问这条记录,如果把它真正删除并加入到垃圾链表后,别的事务就访问不到了。这个功能就是MVCC。
- 步骤二:根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中 针对UPDATE语句更新记录主键值的这种情况,在对该记录进行delete mark操作时,会记录一条类型为TRX_UNDO_DEL_MARK_REC的undo日志;之后插入新记录时,会记录一条类型为TRX_UNDO_INSERT_REC的undo日志。也就是说,每对一条记录的主键值进行改动,都会记录2条undo日志。