Access Method是从9.4版本引入Postgres的。它是Postgres为用户自定义索引开的“后门”。本文简单介绍了access method的一些基本知识,为读者提供一个整体的概念。
关于PG索引的概念,读者可以参考之前的文章。
除了官方默认提供的索引种类,PG还给用户打开了接口,用户如果想要一个不一样的索引,完全可以自己通过写代码的方式来定义。
那么,自定义一个索引,你需要写哪些函数、每个函数的作用又是什么呢?
如何步步为营造出一个属于自己的索引类型
创建新索引
假设你已经有了一堆数据,并为这堆数据造了一个表。现在你需要为它创建一个索引,让它们能被方便地管理起来。我们这就创建一个新的索引,用来为这些初始数据创建“人口普查记录”。
定义ambuild函数,用你希望的方式创建索引。PG可以给你提供的信息有:heap的信息和PG内部index的基本定义。大多数情况,你是需要调用IndexBuildHeapScan这个函数来进行第一遍的heap表扫描,来把索引数据生成好。咦?那我们自定义的部分在哪呢?别急,IndexBuildHeapScan这个函数接受一个回调函数,此回调函数就是关键。PG会为每个tuple调用一次这个回调函数,tuple数据的信息会作为参数传入,你想要如何为这条数据建立索引,就在这里下达指令吧。
另外PG也提供让你在没有数据的情况下创建一个空索引的函数ambuildempty。
增
恭喜你,现在你已经有一个初始的世界(index),和一些原住居民了(初始的索引数据)。现在有新生儿出生(表里又有新的数据),我们需要为新生儿创建户口(新的索引数据)。
aminsert就是创建户口的办事员,需要的参数有:新数据的值、数据在heap上的位置,还有一个选项表示需不需要检查新增的索引是不是“唯一”的(唯一性检查的介绍见文末)。
删
你的世界需要新陈代谢,当有数据“死亡”的时候,需要给它办理死亡证明。
ambulkdelete,注意新增的时候是一条一条增加,而删除的时候可以批量删除,返回值是删除结果的统计信息。如果要删掉的数据太多的时候,这个函数可能会分批调用,不过不用担心,每一次调用的统计结果返回值都会传入下一次调用,保证最后统计信息的正确性。
Vacuum
之前介绍索引的时候提到过,PG索引只会在vaccum的时候被删除。Access Method里提供一个amvaccumcleanup的函数,让你在删掉索引之后,能够在这里把应该做的事情做了,比如对那些已被删掉的索引占用的空间进行回收。
Index Property
一个索引有三层属性,包括索引类型的属性、创建的特定索引的属性、特定索引中一列的属性。每个具体属性的含义可参考文章。
首先是索引类型的属性,所有同类型的索引都共享这些属性。pg提供了一个函数pg_indexam_has_property以供查询索引类型属性。
/*查询btree这个索引类型的某些属性*/
select a.amname, p.name, pg_indexam_has_property(a.oid,p.name)
from pg_am a,
unnest(array['can_order','can_unique','can_multi_col','can_exclude']) p(name)
where a.amname = 'btree' order by a.amname;
amname | name | pg_indexam_has_property
-------+-------------------+-------------------------
btree | can_order | t
btree | can_unique | t
btree | can_multi_col | t
btree | can_exclude | t
(4 rows)
其次是创建的某特定索引的属性,这是每个索引的个性。查询的函数名是pg_index_has_property。
/* 查询名为t_a_idx这个索引的一系列属性 */
select p.name, pg_index_has_property('t_a_idx'::regclass,p.name)
from unnest(array['clusterable','index_scan','bitmap_scan','backward_scan']) p(name);
name | pg_index_has_property
---------------+-----------------------
clusterable | t
index_scan | t
bitmap_scan | t
backward_scan | t
(4 rows)
最后,index中每一列都有属性,查询函数是pg_index_column_has_property。
/* 查询t_a_idx 中序号为1的列的某些属性 */
select p.name, pg_index_column_has_property('t_a_idx'::regclass,1,p.name)
from unnest(array['asc','desc','nulls_first','nulls_last','orderable','distance_orderable','returnable','search_array','search_nulls']) p(name);
name | pg_index_column_has_property
--------------------+------------------------------
asc | t
desc | f
nulls_first | f
nulls_last | t
orderable | t
distance_orderable | f
returnable | t
search_array | t
search_nulls | t
(9 rows)
这三个函数在access method中都可以用amproperty这个函数来自定义行为。
Scan相关
索引scan的过程类似于事务,也有begin(ambeginscan)、start(amrescan)、end(amendscan)三个阶段,还支持记录及恢复scan进行到的位置(ammarkpos、amrestrpos)。
几个注意点⚠️:
- amrescan需要用户自己对where过滤条件作出合理的判断和预处理。比如Where x > 5 AND x > 15这个条件,虽然看似简单,但也不能指望pg来做条件合并,我们需要自己决定丢掉x > 5这个条件。
- 排序。Access method要支持排序可以有两种方法,1. btree是天然支持排序的,这时候吧amcanorder(上文的索引类型属性之一)设成true就可以。2. 其他的索引,想要实现排序,则需要将amcanorderbyop设成true,从名字就能知道,这时想要返回有序的数据,需要使用比较operator对index_key进行排序操作。
对tuple的操作,支持两种方式,plain index scan和bitmap index scan(关于bitmap index scan也请看之前介绍index的文章)。如果支持plain index scan,那么必须提供amgettuple这个函数,同样,如果这个index类型支持bitmap index scan,就要提供amgetbitmap这个函数的定义。
amgettuple函数有一个很有意思的参数叫direction,能支持这个特征的index,可以指定这次取的tuple是“正向”还是“反向”,如果是反向的,那么返回的就是“最后一个”能match的tuple,而不是第一个。而且,每一次amgettuple调用,都可以指定和上一次不同的方向,虽然我暂时并未想到这个特性有什么实际应用场景。
amgetbitmap要比amgettuple高效的多,因为它是批处理的,能减少很多锁操作。当然,这种情况下我们也不需要什么记录和恢复scan的位置了,另外批处理没有方向性,direction也不需要了。排序?那也是没有的。
这两个get函数,你可以全部实现,也可以挑其中一个实现,具体怎么做完全取决于你的索引的内部结构。
并行scan
有些索引会想要实现并行的scan,同时起多个进程对同一个index进行scan。Access method提供了下面三个接口函数:
- amestimateparallelscan,用以计算进行并行scan额外需要的dynamic shared memory的数量,注意⚠️因为是并行,需要进程间的内存共享,所以是shared memory。
- aminitparallelscan,用来进行dynamic shared memory初始化的函数,如果没有必要进行内存初始化,可以忽略。
- amparallelrescan,用来重新启动并行scan的函数,这时所有在共享内存里的数据都会被重置。
其他函数
- amcanreturn 检查某个列在这个index中是否支持index only scan。这个index only scan之前也介绍过,如果本次查询所有需要的column都存在于index的key中,那么就可以使用index only scan。
- amoptions 向这个index设定一些option参数,随便举一个参数的例子:“autovacuum_enabled = false”,禁止对索引进行自动vacuum。
锁
用access method对索引进行更新,必须支持多个session“同时”操作,这就需要引入锁。在scan的时候,只需要一个读锁就可以,pg里用的是AccessShareLock,而更新的时候,用的是RowExclusiveLock,写锁的粒度是单行数据。
对索引进行更新时有几个必须遵循的规则:
- 先在heap上产生新数据,然后才建索引条目。
- 先删掉索引,再删掉heap数据。
- 每一个进行中的index scan,当前最后一次调用amgettuple所返回的条目,它所在的index page都必须被pin住,而且此page里面存储的所有条目在此时都是不能被删除的。这主要是出于non-MVCC snapshot的考虑。如果我们pin住了index page,那么索引就不可能被删除,根据上一条规则,那么heap数据也不能被删除,就可以避免一个session正在进行index scan,另外一个session把恰好要取的数据从heap上删掉的情况。然而对于amgetbitmap,上文说过这个操作是批处理的,当然也不会pin住index page,所以bitmap scan只可以用在MVCC snapshot上。
唯一性检查
注意,目前只有btree索引支持唯一性。唯一性意味着一个key值只能对应一条heap数据。
其实在物理方面,一个key值只存储一个条目是不可能的。因为我们需要支持MVCC,一个key需要存储多个版本的条目。所以,所谓“唯一性”的限制仅限于同一个版本。如果要在支持唯一性的索引中插入新的条目,我们需要分如下几种情况考虑:
- 发现了冲突,但有冲突的条目在当前的transaction中已经被删掉了,这种情况没问题。
- 有冲突,而且有冲突的条目是另外一个transaction插入的,并且那个transaction还没有被提交。那么我们只能等待。如果那个transaction被roll back了,那么冲突就不存在了。否则,就会产生唯一性的冲突。
- 同样,发现了冲突,不过有冲突的条目在另一个尚未提交的transaction里被删掉了,我们还是需要等待,和2是相同的道理。
Tips: create unique index concurrently
对一个很大的表创建索引是很耗时的,为了能在创建索引的时候不至于长时间block其他需要对同一个表进行写操作的session,pg提供了concurrently这个关键词。如果用并行模式创建索引,其他的session可以同时对数据表进行增删改的操作(读的操作任何时候都不受建索引的影响)。
但是这个特性也给我们的一致性检查带来了麻烦。如果我们在并行创建唯一索引时发现了冲突,在正式报警之前,需要再次确认有冲突的那一行heap数据没有被concurrent的其他session删掉,以防止错误报警。当然,这个再次确认的操作是需要access method自己去做的,在我们的程序里需要显式写出去heap查找某一行数据状态(是不是还活着的)的代码。