postgresql 内核源码分析 btree索引的增删查代码基本原理流程分析,索引膨胀的原因在这里
B-Tree索引代码流程分析
专栏内容:
- postgresql内核源码分析
- 手写数据库toadb
- 并发编程
开源贡献:
- toadb开源库
个人主页:我的主页
管理社区:开源数据库
座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物.
概述
在postgresql最常用的索引就是btree,它支持范围和等值查询。
本文主要介绍btree的代码的入口,接口定义,主要涉及索引的查询,插入,删除,和数据的清理操作。
前言
索引是为了更快的找到实际数据表中的数据,那么索引键值就非常小,可以一次性从磁盘读取大量的索引数据。
但是有些索引值中存储了实际数据,与数据是一一对应的,就是密集型索引,而有一些索引并不存储实际数据,而是存储范围内的最大最小值,此类型索引叫做稀疏索引;对于密集型索引,如主键,直接可以得到对应的数据位置或对应列的数据,btree算法就可以支持此类型的索引;
而稀疏索引,查到索引后,需要再遍历数据表,或者二级索引才能命中目标数据。
代码入口
postgresql中为了代码的解耦,定义了索引操作的结构体,基成员是一组统一的操作和标识选项;
对于btree的定义如下,可以在这里找到btree索引的操作接口名称,在实际实用的只是调用结构体的成员,也就是函数指针。
/** Btree handler function: return IndexAmRoutine with access method parameters* and callbacks.*/
Datum
bthandler(PG_FUNCTION_ARGS)
{IndexAmRoutine *amroutine = makeNode(IndexAmRoutine);amroutine->amstrategies = BTMaxStrategyNumber;amroutine->amsupport = BTNProcs;amroutine->amoptsprocnum = BTOPTIONS_PROC;amroutine->amcanorder = true;amroutine->amcanorderbyop = false;amroutine->amcanbackward = true;amroutine->amcanunique = true;amroutine->amcanmulticol = true;amroutine->amoptionalkey = true;amroutine->amsearcharray = true;amroutine->amsearchnulls = true;amroutine->amstorage = false;amroutine->amclusterable = true;amroutine->ampredlocks = true;amroutine->amcanparallel = true;amroutine->amcaninclude = true;amroutine->amusemaintenanceworkmem = false;amroutine->amsummarizing = false;amroutine->amparallelvacuumoptions =VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP;amroutine->amkeytype = InvalidOid;amroutine->ambuild = btbuild;amroutine->ambuildempty = btbuildempty;amroutine->aminsert = btinsert;amroutine->ambulkdelete = btbulkdelete;amroutine->amvacuumcleanup = btvacuumcleanup;amroutine->amcanreturn = btcanreturn;amroutine->amcostestimate = btcostestimate;amroutine->amoptions = btoptions;amroutine->amproperty = btproperty;amroutine->ambuildphasename = btbuildphasename;amroutine->amvalidate = btvalidate;amroutine->amadjustmembers = btadjustmembers;amroutine->ambeginscan = btbeginscan;amroutine->amrescan = btrescan;amroutine->amgettuple = btgettuple;amroutine->amgetbitmap = btgetbitmap;amroutine->amendscan = btendscan;amroutine->ammarkpos = btmarkpos;amroutine->amrestrpos = btrestrpos;amroutine->amestimateparallelscan = btestimateparallelscan;amroutine->aminitparallelscan = btinitparallelscan;amroutine->amparallelrescan = btparallelrescan;PG_RETURN_POINTER(amroutine);
}
我们首先来看索引的基本操作,查询btgettuple,插入btinsert和删除。
索引查询
索引查询的调用栈
- ExecIndexScan
在执行计划中会有索引查询的节点,如ExecIndexScan, 发起索引查询,通过索引查找到数据表的tuple;
- -> IndexNext
返回数据表的tuple, 如果是稀疏索引,此处会进行二次查找;
- -> index_getnext_slot
返回数据表的tuple,此处会使用索引找到的tid,在数据表中查找,并检查可见性,如果不可见,那继续查找下一条;
- -> index_getnext_tid
返回索引键中的记录的tid;
- ->btgettuple
在索引中查找, 通过遍历比较,命中查找键对应的索引项
查找索引数据的基本流程
索引的查找大致分为两个步骤:
- 找到起始点,也就是查找键值
- 从起始点开始扫描,返回符合条件的索引项
代码分析
索引的查询入口函数是 btgettuple,下面是它的实现;
bool
btgettuple(IndexScanDesc scan, ScanDirection dir)
{BTScanOpaque so = (BTScanOpaque) scan->opaque;bool res;/* btree indexes are never lossy */scan->xs_recheck = false;/** If we have any array keys, initialize them during first call for a* scan. We can't do this in btrescan because we don't know the scan* direction at that time.*/if (so->numArrayKeys && !BTScanPosIsValid(so->currPos)){/* punt if we have any unsatisfiable array keys */if (so->numArrayKeys < 0)return false;_bt_start_array_keys(scan, dir);}/* This loop handles advancing to the next array elements, if any */do{/** If we've already initialized this scan, we can just advance it in* the appropriate direction. If we haven't done so yet, we call* _bt_first() to get the first item in the scan.*/if (!BTScanPosIsValid(so->currPos))res = _bt_first(scan, dir); else{/** Check to see if we should kill the previously-fetched tuple.*/if (scan->kill_prior_tuple){/** Yes, remember it for later. (We'll deal with all such* tuples at once right before leaving the index page.) The* test for numKilled overrun is not just paranoia: if the* caller reverses direction in the indexscan then the same* item might get entered multiple times. It's not worth* trying to optimize that, so we don't detect it, but instead* just forget any excess entries.*/if (so->killedItems == NULL)so->killedItems = (int *)palloc(MaxTIDsPerBTreePage * sizeof(int));if (so->numKilled < MaxTIDsPerBTreePage)so->killedItems[so->numKilled++] = so->currPos.itemIndex;}/** Now continue the scan.*/res = _bt_next(scan, dir);}/* If we have a tuple, return it ... */if (res)break;/* ... otherwise see if we have more array keys to deal with */} while (so->numArrayKeys && _bt_advance_array_keys(scan, dir));return res;
}
- 初始化查找点;从代码来看,进入循环后,先 BTScanPosIsValid(so->currPos) 判断currPos是否有效,也就是查找点是否已经初始化;如果没有初始化,则调用 _bt_first 进行初始化;
- 扫描索引项; 初始化查找点后,调用 _bt_next 获取一条索引项数据,找到有效索引后就会返回;
索引插入
索引插入调用栈
从insert来看,调用路径如下
- ExecInsert
SQL insert语句的执行入口函数
- -> ExecInsertIndexTuples
如果当前表中建有索引,在表数据tuple插入后,调用此函数插入索引,有可能存在多个索引,循环对每个索引调用下级函数进行插入;
- index_insert
索引插入的公共调用接口,实际调用对应索引的插入定义接口;
- btinsert
btree索引插入的操作的入口函数; 在此函数中,首先拼装一个索引tuple,然后调用下级函数进行插入;
- _bt_doinsert
执行索引项的插入,会经过查找位置,检查唯一性,插入等一系列流程环节;
索引插入的基本流程
索引插入的大体流程主要有以下环节:
- 查找索引项插入的位置,因为btree是一个有序的树,所以先要找到插入的位置,保持顺序; 此时会与索引查询类似,先初始化查找键,并找到查询点;
- 唯一性约束的检查,如果索引中属性列都为NULL,是不进行唯一性检查的;
- 索引的插入环节,调用_bt_insertonpg来完成,其中会有查找空闲空间,可能会索引分裂等;
代码分析
索引插入的入函数是 btinsert,实际执行是 _bt_doinsert,下面来看一下执行的代码流程;
bool
_bt_doinsert(Relation rel, IndexTuple itup,IndexUniqueCheck checkUnique, bool indexUnchanged,Relation heapRel)
{bool is_unique = false;BTInsertStateData insertstate;BTScanInsert itup_key;BTStack stack;bool checkingunique = (checkUnique != UNIQUE_CHECK_NO);/* we need an insertion scan key to do our search, so build one */itup_key = _bt_mkscankey(rel, itup);if (checkingunique){if (!itup_key->anynullkeys){/* No (heapkeyspace) scantid until uniqueness established */itup_key->scantid = NULL;}else{checkingunique = false;/* Tuple is unique in the sense that core code cares about */Assert(checkUnique != UNIQUE_CHECK_EXISTING);is_unique = true;}}insertstate.itup = itup;insertstate.itemsz = MAXALIGN(IndexTupleSize(itup));insertstate.itup_key = itup_key;insertstate.bounds_valid = false;insertstate.buf = InvalidBuffer;insertstate.postingoff = 0;search:stack = _bt_search_insert(rel, heapRel, &insertstate);if (checkingunique){TransactionId xwait;uint32 speculativeToken;xwait = _bt_check_unique(rel, &insertstate, heapRel, checkUnique,&is_unique, &speculativeToken);if (unlikely(TransactionIdIsValid(xwait))){/* Have to wait for the other guy ... */_bt_relbuf(rel, insertstate.buf);insertstate.buf = InvalidBuffer;if (speculativeToken)SpeculativeInsertionWait(xwait, speculativeToken);elseXactLockTableWait(xwait, rel, &itup->t_tid, XLTW_InsertIndex);/* start over... */if (stack)_bt_freestack(stack);goto search;}/* Uniqueness is established -- restore heap tid as scantid */if (itup_key->heapkeyspace)itup_key->scantid = &itup->t_tid;}if (checkUnique != UNIQUE_CHECK_EXISTING){OffsetNumber newitemoff;CheckForSerializableConflictIn(rel, NULL, BufferGetBlockNumber(insertstate.buf));newitemoff = _bt_findinsertloc(rel, &insertstate, checkingunique,indexUnchanged, stack, heapRel);_bt_insertonpg(rel, heapRel, itup_key, insertstate.buf, InvalidBuffer,stack, itup, insertstate.itemsz, newitemoff,insertstate.postingoff, false);}else{/* just release the buffer */_bt_relbuf(rel, insertstate.buf);}/* be tidy */if (stack)_bt_freestack(stack);pfree(itup_key);return is_unique;
}
代码流程如下:
- 初始化工作; 初始化查找键;
- 查找插入位置; 调用 _bt_search_insert 进行查询到一个有足够空闲空间的叶子节点page;
- 检查唯一性约束;检查唯一性约束,如果有冲突事务,则等待冲突事务执行完成后,再重新查询位置,再检查唯一性约束;然后对结果的判断checkUnique != UNIQUE_CHECK_EXISTING,如果违返那么插入结束;否则执行插入动作;
- 索引插入;先确定插入位置,再调用_bt_insertonpg;
索引删除
索引的更新,就是删除和插入操作,这里我们来看一下索引删除的概要流程。
对于数据表的tuple的删除,数据并没有真实删除,所以对应的索引项也不会删除,那么什么时候删除索引项呢?
删除索引基本流程
在进行vacuum 或进行 prune paga时,对于HOT链都会在每个page上留下最后一个数据元组,因为同一个page内的HOT链只对应一个索引项,留下这最后一个也是为了删除索引项。
当进行vacuum 索引时,就会通过这个dead tuple找到对应的索引项,先删除索引项,再删除dead tuple。
常常说索引的性能下降了,其实就是索引膨胀导致,也就是deadtuple变多,导致待删除索引项变多,查询效率大降低,同时也会带来索引IO的增加。
代码分析
- vac_bulkdel_one_index
调用 通用索引处理接口;
- ->index_bulk_delete
这里通用索引处理接口,其中调用对应索引的处理接口,这里是调用btree索引处理;
- ->btbulkdelete
btree对应的批量删除接口; 避免退出的影响,在开始时会注册退出的回调函数,在解除共享内存前处理善后;然后调用 btvacuumscan 对所有page进行索引删除清理。
结尾
非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!
作者邮箱:study@senllang.onaliyun.com
如有错误或者疏漏欢迎指出,互相学习。
注:未经同意,不得转载!
相关文章:
postgresql 内核源码分析 btree索引的增删查代码基本原理流程分析,索引膨胀的原因在这里
B-Tree索引代码流程分析 专栏内容: postgresql内核源码分析手写数据库toadb并发编程 开源贡献: toadb开源库 个人主页:我的主页 管理社区:开源数据库 座右铭:天行健,君子以自强不息;地势坤&…...

详细了解G1、了解G1、G1垃圾收集器详解、G1垃圾回收器简单调优
4.详细了解G1: 4.1.一:什么是垃圾回收 4.2.了解G1 4.3.G1 Yong GC 4.4.G1 Mix GC 4.5.三色标记算法 4.6.调优实践 5.G1垃圾收集器详解 5.1.G1垃圾收集器 5.2.G1的堆内存划分 5.3.G1的运行过程 5.4.三色标记 5.4.1.漏标问题 5.5.记忆集与卡表 5.6.安全点与…...
vue项目中 package.json 详解
在 Vue 项目中,package.json 是一个重要的配置文件,它包含了项目的名称、版本、作者、依赖等信息。下面是一份详细的 Vue 项目 package.json 配置说明: 1.name:项目的名称,用于标识项目,例如:&q…...

为什么要进行管网水位监测,管网水位监测的作用是什么
管网水位监测是城市排水系统管理的重要手段,对于保障城市排水设施安全运行和提升城市管理水平具有重要意义。通过对排水管网的水位进行实时监测和分析,能够及时发现问题并采取措施,提高排水系统的运行效率和管理水平。本文将详细介绍为什么要…...
webpack学习笔记
1. webpack基本概念 webpack: JavaScript 应用程序的静态模块打包器,是目前最为流行的JavaScript打包工具之一。webpack会以一个或多个js文件为入口,递归检查每个js模块的依赖,从而构建一个依赖关系图,然后依据该关系…...

解析代理IP在跨境电商和社媒营销中的关键作用
跨境电商和社媒营销领域的从业者深知,代理IP的价值愈发凸显。在推广营销的过程中,频繁遇到因IP关联而封禁账号的情况,或因使用不安全IP而导致异常问题。 这些问题促使人们开始高度重视代理IP的作用。但实际上,代理IP究竟是何物&a…...

Unity 之 Start 与Update 方法的区别
文章目录 当谈论Unity中的 Start和 Update方法时,我们实际上是在讨论MonoBehaviour类中的两个常用方法,用于编写游戏逻辑。这两个方法在不同的时机被调用,因此您可以根据需要选择在哪个方法中编写特定的代码。 Start 方法: Start…...
Spring Boot中如何编写优雅的单元测试
单元测试是指对软件中的最小可测试单元进行检查和验证。在Java中,单元测试的最小单元是类。通过编写针对类或方法的小段代码,来检验被测代码是否符合预期结果或行为。执行单元测试可以帮助开发者验证代码是否正确实现了功能需求,以及是否能够…...

三星Galaxy S23与iPhone 15的对比分析:谁会胜出?
三星Galaxy S23与iPhone 15的对决将于下个月进入高潮,这将是今年智能手机中最大的一场较量。毕竟,这是两家领先的移动设备制造商的旗舰手机。他们的手机的比较将在很大程度上决定谁能获得最佳手机的称号。 我们已经知道有利于三星Galaxy S23的情况,该产品自春季以来一直在推…...

MySQL索引 事物 存储引擎
一 索引 索引的概念 索引就是一种帮助系统能够更快速的查找信息的结构 索引的作用 索引的副作用 创建索引的规则 MySQL的优化 哪些字段/场景适合创建索引 哪些不适合 小字段唯一性强的字段更新不频繁,但查询率比较高的字段表记录超过 300行主键,外键…...
【谷粒学院】报错记录
无法从Nacos获取动态配置 原先gulimall-common中SpringCloud Alibaba的版本是2.1.0.RELEASE,无法从Nacos中获取配置文件信息 <dependencyManagement><dependencies><dependency><groupId>com.alibaba.cloud</groupId><artifactId&…...

微积分基本概念
微分 函数的微分是指对函数的局部变化的一种线性描述。微分可以近似地描述当函数自变量的取值作足够小的改变时,函数的值是怎样改变的。。对于函数 y f ( x ) y f(x) yf(x) 的微分记作: d y f ′ ( x ) d x d_y f^{}(x)d_x dyf′(x)dx 微分和…...

【业务功能篇78】微服务-前端后端校验- 统一异常处理-JSR-303-validation注解
5. 前端校验 我们在前端提交的表单数据,我们也是需要对提交的数据做相关的校验的 Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可 校验的页面效果 前端数据…...

pytorch的用法
...
Qt 设置窗口背景
窗口背景无非两种:背景色、背景图片。Qt中窗口背景如何设置? 一、QPalette设置背景 二、实现paintEvent,使用QPainter来绘制背景 三、使用QSS来设置背景 关于QSS的使用不想多说,一般我不用QSS设置窗口背景,也不建议…...

大模型是什么?泰迪大模型能够解决企业哪些痛点?
什么是大模型? 大模型是指模型具有庞大的参数规模和复杂程度的机器学习模型。在深度学习领域,大模型通常是指具有数百万到数十亿参数的神经网络模型。这些模型需要大量的计算资源和存储空间来训练和存储,并且往往需要进行分布式计算和特殊…...

YOLOv7-tracker 目标追踪 输入视频帧
目录 1 项目安装1.1 环境搭建1.2 项目下载1.3 权重下载1.4 环境安装1.5 上传待检测的视频帧 2 视频帧检测与追踪2.1 检测与追踪2.3 结果 参考项目:https://github.com/JackWoo0831/Yolov7-tracker/tree/master github链接:https://github.com/Whiffe/Yo…...

C语言二——C语言编写一段代码,求一元二次方程的根
这段代码实现了解一元二次方程的根的计算。用户需要输入方程的系数a、b、c,然后根据判别式的值确定方程的根的情况,并进行相应的输出。 如果判别式大于0,说明方程有两个实根,分别计算并输出。如果判别式等于0,说明方程…...

用Idea把SpringBoot项目打包镜像上传至docker
1、设置docker把2375端口开起来 命令查看docker装在哪里 vim docker.service 新增 -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock 2、配置Dockerfile 我在跟pom同一层 3、配置docker-maven-plugin <plugin><groupId>com.spotify</groupId><arti…...

基于 SVG 的图形交互方案实践
不知道从什么时候起,人们开始喜欢上数字大屏这种“花里胡哨”的东西,仿佛只要用上“科技蓝”这样神奇的色调,就可以让一家公司焕然一新,瞬间变得科技感满满。不管数字大屏的实际意义,是用来帮助企业监控和决策…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...

什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...

HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...
unix/linux,sudo,其发展历程详细时间线、由来、历史背景
sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...

k8s业务程序联调工具-KtConnect
概述 原理 工具作用是建立了一个从本地到集群的单向VPN,根据VPN原理,打通两个内网必然需要借助一个公共中继节点,ktconnect工具巧妙的利用k8s原生的portforward能力,简化了建立连接的过程,apiserver间接起到了中继节…...

tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)
参考官方文档:https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java(供 Kotlin 使用) 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...

有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...