暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【手写数据库核心揭秘系列】第108节 数据行的更新操作流程,修改的内容放不下怎么办?

开源无限 2025-07-03
43


💻 深耕数据库内核架构设计与开发十余年,曾主导多款高性能分布式数据库内核研发,攻克高并发、低延迟等核心技术难题。现倾力打造《从零手写数据库》系列教程,首次系统性公开数据库内核源码级实现细节!
🚀 从存储引擎、查询优化到分布式事务,手把手拆解核心模块;从语法解析树构建到执行计划生成,逐行代码还原设计精髓
🌟 无论你是数据库开发者、系统架构师,还是对底层技术充满好奇的极客,这里都有你想要的“硬核干货”!点击关注,与行业老兵共同探索数据库技术的星辰大海!
公众号:开源无限

一、概述 


数据行更新,在新数据行覆盖旧数据行时,存在空间多余或者不足的情况。

当旧数据行空间小于新的数据行空间,比如我们写错字后,擦除了一个字,想改为两个字时,这时旧数据的空间不足存放新的数据,此时仍然要保持数据的位置不变,又可以存储新的数据。

在这种情况下,将新数据行在数据表文件中找一个可用空间插入,而在新数据行的位置,只记录新数据行的位置信息。

对于这类需要二次寻址的数据行,新增两种数据行标志,将新数据行和旧数据行分别标记为数据和重定向数据行。

二、数据行类型 


数据行的标志新增四种:

  • 「无效」,代表当前数据行是无效数据;
  • 「正常」,当前数据行是普通数据行数据;
  • 「重定向」,当前数据行存储的数据是位置,需要二次寻址;
  • 「数据」,仅存储数据,当前数据行存储的是二次寻址的真正数据行。
    typedef enum {
        ITEM_INVALID    =   0x00,
        ITEM_NORMAL     =   0x01,
        ITEM_REDIRECT   =   0x02,
        ITEM_DATA       =   0x04
    }ITEM_FLAGS;

    为了在数据行上记录以上四种标记,在数据行信息头中增加8位的标志字段,将长度字段减少到了24位,这样整个结构的大小不变。

    以上四种标志,可以组合形成四种类型数据行:

    • 「无效数据行」,在查找时可以跳过,标志位为ITEM_INVALID;
    • 「有效数据行」,也就是普通的数据行,标志位为ITEM_NORMAL;
    • 「重定向数据行」,它也是带有效标志的,标志位为ITEM_NORMAL | ITEM_REDIRECT;
    • 「只数据的数据行」,同时带有有效标志,标志位为ITEM_NORMAL | ITEM_DATA。
      typedef struct ItemData
      {
          int offset;
          int len:24
          unsigned int flags:8;
      }ItemData;

      在以数据块为管理单位的存储系统中,每个数据行的最大长度不能超过单个数据块的长度,使得长度字段实际用不到4个字节,这里就可以从它分出一个字节来记录其它信息。

      为了方便使用定义了两组宏操作,一组是判断是否有某一标志的宏,它的结果是布尔类型;

        #define ITEM_ISNORMAL(item) (((item)->flags & ITEM_NORMAL) != 0)
        #define ITEM_ISREDIECT(item) (((item)->flags & ITEM_REDIRECT) != 0)
        #define ITEM_ISDATA(item) (((item)->flags & ITEM_DATA) != 0)

        另一组是添加标识的宏,其中只有无效标识是直接赋值,与其它互斥;其它重定向和仅数据标识都需要与有效标识共同使用,所以它们两个是添加操作。

          #define ITEM_SETINVALID(item) ((item)->flags &= ITEM_INVALID)
          #define ITEM_SETREDIECT(item) ((item)->flags |= ITEM_REDIRECT)
          #define ITEM_SETDATA(item) ((item)->flags |= ITEM_DATA)

          三、数据行更新 


          在这里会有两个版本的数据行,更新前的数据行——旧数据行,更新后的数据行——新数据行,以及旧数据行的在数据文件中的位置,它由数据块号和块内数据行头位 置组成。

          在数据行更新时,先根据旧数据行的位置,找到旧数据行,然后拼估空间是否足够存储新数据行,此时分为三种情况:

          • 「空间足够」
          • 「空间不足时」,当前数据块剩余空间足够存储新数据行;
          • 「空间不足时」,当前数据块剩余空间也不足时。

          在这三种情况下,采用不同的方法来处理。


          3.1 找到旧数据行  

          根据旧数据行的位置,找到旧数据行;

            int UpdateTuple(int pageNum, int oldItemOffset, RelationInfo *relInfo, TupleHeader *newTupData)
                page = ReadPageBuffer(relInfo, pageNum);
                while(page != NULL)
                {
                    oldItem = page->item + itemIndex;
                    if(!ITEM_ISNORMAL(oldItem))
                    {
                        return -1;
                    }


                    if(!ITEM_ISREDIECT(oldItem))
                    {
                        break;
                    }            


                    itemIndex = oldItem->len;
                    page = ReadPageBuffer(relInfo, oldItem->offset);
                }  

            每个数据行都会有三种类型,要排除无效的数据行,对于重定向类型的数据行递归二次寻址找到真正数据的位置。


            3.2 空间足够新数据行 

            当旧数据行的空间可以存储新数据行时,直接将新数据覆盖到旧数据位置上,并更新数据行头信息中的数据长度,数据块标记为脏数据以便写入磁盘。

              if(oldItem->len >= newTupData->size)
                  {
                      oldItem->len = newTupData->size;
                      memcpy((char *)page + oldItem->offset, (char *)newTupData + sizeof(TupleHeader), oldItem->len);
                      relInfo->relstorage.isDirt = 1;
                      return 0;
                  }

              此时数据行信息头和数据部分的位置都没有发生变化。


              3.3 当前块空间足够  

              当旧数据行的空间不足时,而当前数据块中有足够空间存储新数据行时,就在当前数据块中存储新数据行,并将旧数据行头更新为新数据的位置。

                if (HasFreeSpace(page, newTupData->size))
                    {
                        oldItem->len = newTupData->size;
                        page->dataStartOffset -= oldItem->len;
                        memcpy((char *)page + page->dataStartOffset, (char *)newTupData + sizeof(TupleHeader), oldItem->len);


                        oldItem->offset = page->dataStartOffset;
                        relInfo->relstorage.isDirt = 1;
                        return 0;
                    }

                此时数据行头信息的位置没有发生变化,而数据部分的位置发生了变化,需要同时更新行头信息中的偏移字段和数据长度字段。

                另外,当前数据块中新插入了数据,消耗了数据块剩余空间,数据块的数据偏移字段也需要更新。


                3.4 插入新数据行  

                旧数据行和当前数据块剩余空间都不足时,只能在数据表的所有数据块中查找足够的空间。

                  page = InsertNewTupForUpdate(relInfo, newTupData, page, &tempItem);
                      newPageNum = page->header.pageNum;


                      if(pageNum != newPageNum)
                      {        
                          page = ReadPageBuffer(relInfo, pageNum);
                          oldItem = page->item + PAGE_ITEM_INDEX(oldItemOffset);
                      }


                      oldItem->len = PAGE_ITEM_INDEX(tempItem.offset);
                      oldItem->offset = newPageNum;
                      ITEM_SETREDIECT(oldItem);   
                      relInfo->relstorage.isDirt = 1;

                  在当前数据表文件中找到空闲空间块并插入新的数据行,将旧数据行头信息更新为新数据行的位置,此时就会产生二次寻址的数据行,需要将旧数据行头和新数据行的行头信息设置标志,区别普通数据行。

                  对于二次寻址的数据行头信息,如何记录实际数据行的地址呢?数据行的地址由数据块编号和数据行头的偏移两部分组成。

                  在数据行头信息中有数据偏移和数据长度两个字段,此时复用这两个字段,数据偏移字段存储数据块编号,数据长度字段存储数据行头的偏移。

                  当数据行为重定向类型时,数据行头内存储的信息需要解析为数据行的地址。


                  3.5 重定向数据行  

                  只存储数据的数据行。

                    PageDataHeader* InsertNewTupForUpdate(RelationInfo *relInfo, TupleHeader *tup, PageDataHeader *page, ItemData *oldItem)
                        if ((page == NULL) || (!HasFreeSpace(page, tup->size)))
                        {
                            page = GetFreeSpacePage(relInfo, tup->size, PAGE_NEW);
                        }


                        itemOffset = PutTupleToPage(page, tup);


                        newItem = page->item + PAGE_ITEM_INDEX(itemOffset);    
                        ITEM_SETDATA(newItem);
                        relInfo->relstorage.isDirt = 1;


                        if(oldItem != NULL)
                        {
                            oldItem->offset = itemOffset;
                        }

                    在数据表的所有数据块中查找空闲空间,并在新数据行插入数据表中后,它对应的数据行头信息标志要设置为仅数据类型,只有UPDATE命令会产生这样的数据行。

                    四、总结 


                    本节内容在exam_63 目录的pages.c文件中。

                    当再次UPDATE相同数据行时,新的数据行优先存储在重定向数据行所在数据块中,这样可以消除重定向的数据行,避够二次读数据。

                    当然,UPDATE命令产生的数据行的链最多也就两次寻址就可以找到真正的数据,即使多次更新同一行数据,也是如此。

                    在UPDATE数据行时,为什么旧数据行的位置要保持不变呢?留一个思考题,大家想一想。


                    🌟 点赞收藏,分享给身边的技术伙伴,关注我们,持续获取数据库内核开发的硬核干货!一起从源码级实现到分布式架构,解锁数据库技术的每一个核心细节!🚀



                    【往期精彩推荐】

                    【手写数据库核心揭秘系列】第107节 UPDATE执行器框架,考试完橡皮擦别扔

                    【手写数据库核心揭秘系列】第105节 UPDATE命令中SET子句的实现细节,复用SELECT类似逻辑,你敢相信吗?

                    【手写数据库核心揭秘系列】第99节 多表联合查询演示,高复杂度查询SQL,四张数据表混合联合类型的查询


                    文章转载自开源无限,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论