文章目录
引言(Introduction)
索引是数据库快速查询满足要求的元组的结构,是每个数据库必备的结构。当然,使用索引虽然能提高查询速率,但也会带来内存以及维护上的开销,只不过这些开销对于索引带来的好处而言是可以忽略的。本章针对PG14中的索引及其源码进行学习。
概述
在数据库的使用中,常常需要查询某个字段为给定值的元组。由于PG在创建relation的时候并不会自动创建索引,这种查询常常需要遍历整个表格寻找所有匹配的元组。当表中含有的元组数据太多时,这种顺序遍历的方式不仅要扫描大量无关的元组,每次扫描都会带来大量的IO操作,搜索的效率也差强人意。而如果让数据库能够在该字段(或多个字段)维护一个索引用于快速定位目标元组,那么数据库就能够以很快的速度找到满足要求的元组,这会大大增加查询的效率。
PG14.0支持的索引结构有B-Tree,Hash,GIST,SP-GIST,GIN和BGIN。当前支持的四种索引方式为:
- 单一索引(Unique Indexes)。如果索引声明为唯一索引,那么就不允许出现多个索引值相同的元组。唯一索引可以迫使某个字段的值不重复(主键功能类似,但唯一索引允许NULL存在,NULL并不会被认为是相等的),也可以让联合字段组成的值唯一。在创建索引时添加UNIQUE关键字可以生成单一索引,目前只有B-tree索引支持单一索引的用法。
- 多字段索引(Multicolumn Indexes)。表格多个字段构成的组合键的索引为多字段索引。目前支持多字段索引的索引结构有B-tree,GIST,GIN以及BGIN,多字段索引的字段数量最多可以达到32个。
- 部分索引(Partial Indexes)。对表格的部分元组建立的索引是部分索引。引入部分索引最大的一个好处是可以避免对公共值进行索引,这样能够避免索引的浪费。这里需要避免的是不要使用部分索引代替group by或partition by的功能。
- 表达式索引(Indexes on Expressions)。当需要对表中多个字段在某个函数下的映射值构建索引时,可以构建表达式索引。但是这样构建的索引只能在查询时使用相同的表达式才能起作用。例如可以对字段的小写函数构建一个索引,那么该索引只有在查询上加入lower()才能起作用。
PG中所有的索引都是从属索引,即索引也是一种单独的数据结构,存在自己的物理数据。
但是要记住,数据库的索引是冗余的,删除索引并不会丢失数据,索引的作用仅仅是为了快速查询。
索引相关的系统表
索引也是一种表,因此也可以通过系统表进行管理。PG中与索引相关的系统表有:
- pg_am:PG支持的索引结构都存储在pg_am中;
- pg_class:每个创建的索引都会作为一个元组保存在pg_class中;
- pg_index:存储创建元组的部分信息;
- pg_opclass:索引结构可以运用在不同数据类型的字段上,因此索引结构并不直接管理目标字段的类型,这些信息由pg_opclass系统表维护。pg_opclass定义索引访问方法操作符类。 每个操作符类为一种特定数据类型和一种特定索引访问方法定义索引字段的语义。比如说在复数字段中使用B树索引,那么用户可以提供该索引的排序方式,并提供相应的运算符,例如’=‘,’>'。如果不提供则使用缺省的操作符类;
- pg_opfamily:管理不同索引下的不同数据库的操作符集合,比如说b树索引下的数组操作符集合的元组的opfmethod与opfname字段值为“403,array_ops”;而hash索引下的数组操作符集合的元组的对应字段值为“405,arr_ops”;
- pg_amop:存储具体操作符的关联信息。因为pg_opfamily只是提供了操作符集合,具体的操作符实现存储在这里;
- pg_amproc:存储具体操作符的过程函数信息,与pg_amop功能相似。
索引操作函数
在PG中,每种索引类型都在pg_am中注册了操作函数,不同索引支持的操作函数的数量并不相同,最多可以支持22个操作函数(PG14.0):
操作函数 | 描述 |
---|---|
ambuild | 构造新的索引 |
ambuildempty | 构造空索引 |
aminsert | 插入一个索引元组 |
ambulkdelete | 批量删除索引元组 |
amvacuumcleanup | 在VACUUM之后调用,主要完成一些额外的清理工作,比如说对索引的FSM文件进行清理 |
amcanreturn | 判断索引是否能够返回一个索引元组 |
amcostestimate | 估计索引查找花费的代价 |
amoptions | 解析索引的表达关系式 |
amproperty | 报告索引的字段属性 |
ambuildphasename | 返回amproperty所处阶段的名称? |
amvalidate | 验证该am的运算符类的定义 |
amadjustmember | 验证要添加的运算符操作函数 |
ambeginscan | 准备索引扫描 |
amrescan | 索引重新扫描 |
amgettuple | 返回一个索引元组 |
amgetbitmap | 返回查找位图,即所有合法的元组 |
amendscan | 终止索引扫描 |
ammarkpos | 标记当前扫描的位置 |
amrestrpos | 保存当前扫描的位置 |
amestimateparallelscan | 估计索引并行扫描的运算符大小 |
aminitparallelscan | 准备并行扫描 |
amparallelrescan | 重新开始并行扫描 |
为提供给上层模块一个使用索引的统一接口,以上操作函数都保存在结构体IndexAmRoutine中,该结构体又保存在索引扫描的描述信息IndexScanDesc中。上层模块的操作函数定义在文件"src/backend/access/index/indexam.c"与文件"src/backend/catalog/index.c"中。
B-Tree索引
页面结构
PG的B-Tree索引的实现是基于Lehman与Yao的高并发B树管理的算法实现的,他们在原有B-Tree的基础上,在每个非根结点中添加了指向兄弟的指针,还在每个索引页面中添加了一个"high key"的属性,该属性表示该页面关键字的上界。high key并不是一个真正的关键字,其只是起到快速扫描的作用,这种元组也被称为pivot tuple。图中linp0存储的是high key。
与CMU15-445相似,这里整个B-Tree由多个索引结点页面组成,每个页面表示单一结点。每个索引页面的数据存组织形式与表文件中元组的组织形式相同,也就是说一个页面(索引结点)最多只有4096个索引元组。为存放兄弟结点的指针,每个索引页面的末尾都会保存一个一个BTPageOpaqueData的数据结构:
typedef struct BTPageOpaqueData
{
BlockNumber btpo_prev; /* left sibling, or P_NONE if leftmost */
BlockNumber btpo_next; /* right sibling, or P_NONE if rightmost */
uint32 btpo_level; /* tree level --- zero for leaf pages */
uint16 btpo_flags; /* flag bits, see below */
BTCycleId btpo_cycleid; /* vacuum cycle ID of latest split */
} BTPageOpaqueData;
其中btpo_flags用于表示页面结点的类型:
/* Bits defined in btpo_flags */
#define BTP_LEAF (1 << 0) /* leaf page, i.e. not internal page */
#define BTP_ROOT (1 << 1) /* root page (has no parent) */
#define BTP_DELETED (1 << 2) /* page has been deleted from tree */
#define BTP_META (1 << 3) /* meta-pa