浅谈Postgres的Access Method

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,写锁的粒度是单行数据。

对索引进行更新时有几个必须遵循的规则:

  1. 先在heap上产生新数据,然后才建索引条目。
  2. 先删掉索引,再删掉heap数据。
  3. 每一个进行中的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需要存储多个版本的条目。所以,所谓“唯一性”的限制仅限于同一个版本。如果要在支持唯一性的索引中插入新的条目,我们需要分如下几种情况考虑:

  1. 发现了冲突,但有冲突的条目在当前的transaction中已经被删掉了,这种情况没问题。
  2. 有冲突,而且有冲突的条目是另外一个transaction插入的,并且那个transaction还没有被提交。那么我们只能等待。如果那个transaction被roll back了,那么冲突就不存在了。否则,就会产生唯一性的冲突。
  3. 同样,发现了冲突,不过有冲突的条目在另一个尚未提交的transaction里被删掉了,我们还是需要等待,和2是相同的道理。

Tips: create unique index concurrently
对一个很大的表创建索引是很耗时的,为了能在创建索引的时候不至于长时间block其他需要对同一个表进行写操作的session,pg提供了concurrently这个关键词。如果用并行模式创建索引,其他的session可以同时对数据表进行增删改的操作(读的操作任何时候都不受建索引的影响)。
但是这个特性也给我们的一致性检查带来了麻烦。如果我们在并行创建唯一索引时发现了冲突,在正式报警之前,需要再次确认有冲突的那一行heap数据没有被concurrent的其他session删掉,以防止错误报警。当然,这个再次确认的操作是需要access method自己去做的,在我们的程序里需要显式写出去heap查找某一行数据状态(是不是还活着的)的代码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容