Access Method是從9.4版本引入Postgres的蕉堰。它是Postgres為用戶自定義索引開的“后門”。本文簡單介紹了access method的一些基本知識再层,為讀者提供一個整體的概念柠逞。
關于PG索引的概念,讀者可以參考之前的文章酿联。
除了官方默認提供的索引種類,PG還給用戶打開了接口夺巩,用戶如果想要一個不一樣的索引贞让,完全可以自己通過寫代碼的方式來定義。
那么柳譬,自定義一個索引喳张,你需要寫哪些函數(shù)、每個函數(shù)的作用又是什么呢征绎?
如何步步為營造出一個屬于自己的索引類型
創(chuàng)建新索引
假設你已經(jīng)有了一堆數(shù)據(jù)蹲姐,并為這堆數(shù)據(jù)造了一個表。現(xiàn)在你需要為它創(chuàng)建一個索引人柿,讓它們能被方便地管理起來。我們這就創(chuàng)建一個新的索引忙厌,用來為這些初始數(shù)據(jù)創(chuàng)建“人口普查記錄”凫岖。
定義ambuild函數(shù),用你希望的方式創(chuàng)建索引逢净。PG可以給你提供的信息有:heap的信息和PG內部index的基本定義哥放。大多數(shù)情況,你是需要調用IndexBuildHeapScan這個函數(shù)來進行第一遍的heap表掃描爹土,來把索引數(shù)據(jù)生成好甥雕。咦?那我們自定義的部分在哪呢胀茵?別急社露,IndexBuildHeapScan這個函數(shù)接受一個回調函數(shù),此回調函數(shù)就是關鍵琼娘。PG會為每個tuple調用一次這個回調函數(shù)峭弟,tuple數(shù)據(jù)的信息會作為參數(shù)傳入,你想要如何為這條數(shù)據(jù)建立索引脱拼,就在這里下達指令吧瞒瘸。
另外PG也提供讓你在沒有數(shù)據(jù)的情況下創(chuàng)建一個空索引的函數(shù)ambuildempty。
增
恭喜你熄浓,現(xiàn)在你已經(jīng)有一個初始的世界(index)情臭,和一些原住居民了(初始的索引數(shù)據(jù))。現(xiàn)在有新生兒出生(表里又有新的數(shù)據(jù)),我們需要為新生兒創(chuàng)建戶口(新的索引數(shù)據(jù))俯在。
aminsert就是創(chuàng)建戶口的辦事員竟秫,需要的參數(shù)有:新數(shù)據(jù)的值、數(shù)據(jù)在heap上的位置朝巫,還有一個選項表示需不需要檢查新增的索引是不是“唯一”的(唯一性檢查的介紹見文末)鸿摇。
刪
你的世界需要新陳代謝,當有數(shù)據(jù)“死亡”的時候劈猿,需要給它辦理死亡證明拙吉。
ambulkdelete,注意新增的時候是一條一條增加揪荣,而刪除的時候可以批量刪除筷黔,返回值是刪除結果的統(tǒng)計信息。如果要刪掉的數(shù)據(jù)太多的時候仗颈,這個函數(shù)可能會分批調用佛舱,不過不用擔心,每一次調用的統(tǒng)計結果返回值都會傳入下一次調用挨决,保證最后統(tǒng)計信息的正確性请祖。
Vacuum
之前介紹索引的時候提到過,PG索引只會在vaccum的時候被刪除脖祈。Access Method里提供一個amvaccumcleanup的函數(shù)肆捕,讓你在刪掉索引之后,能夠在這里把應該做的事情做了盖高,比如對那些已被刪掉的索引占用的空間進行回收慎陵。
Index Property
一個索引有三層屬性,包括索引類型的屬性喻奥、創(chuàng)建的特定索引的屬性席纽、特定索引中一列的屬性。每個具體屬性的含義可參考文章撞蚕。
首先是索引類型的屬性润梯,所有同類型的索引都共享這些屬性。pg提供了一個函數(shù)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)
其次是創(chuàng)建的某特定索引的屬性仆救,這是每個索引的個性。查詢的函數(shù)名是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中每一列都有屬性,查詢函數(shù)是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)
這三個函數(shù)在access method中都可以用amproperty這個函數(shù)來自定義行為顿痪。
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. 其他的索引溯泣,想要實現(xiàn)排序,則需要將amcanorderbyop設成true榕茧,從名字就能知道垃沦,這時想要返回有序的數(shù)據(jù),需要使用比較operator對index_key進行排序操作用押。
對tuple的操作栏尚,支持兩種方式,plain index scan和bitmap index scan(關于bitmap index scan也請看之前介紹index的文章)只恨。如果支持plain index scan,那么必須提供amgettuple這個函數(shù)抬虽,同樣官觅,如果這個index類型支持bitmap index scan,就要提供amgetbitmap這個函數(shù)的定義阐污。
amgettuple函數(shù)有一個很有意思的參數(shù)叫direction休涤,能支持這個特征的index,可以指定這次取的tuple是“正向”還是“反向”笛辟,如果是反向的功氨,那么返回的就是“最后一個”能match的tuple,而不是第一個手幢。而且捷凄,每一次amgettuple調用,都可以指定和上一次不同的方向围来,雖然我暫時并未想到這個特性有什么實際應用場景跺涤。
amgetbitmap要比amgettuple高效的多匈睁,因為它是批處理的,能減少很多鎖操作桶错。當然航唆,這種情況下我們也不需要什么記錄和恢復scan的位置了,另外批處理沒有方向性院刁,direction也不需要了糯钙。排序?那也是沒有的退腥。
這兩個get函數(shù)任岸,你可以全部實現(xiàn),也可以挑其中一個實現(xiàn)阅虫,具體怎么做完全取決于你的索引的內部結構演闭。
并行scan
有些索引會想要實現(xiàn)并行的scan,同時起多個進程對同一個index進行scan颓帝。Access method提供了下面三個接口函數(shù):
- amestimateparallelscan米碰,用以計算進行并行scan額外需要的dynamic shared memory的數(shù)量,注意??因為是并行购城,需要進程間的內存共享吕座,所以是shared memory。
- aminitparallelscan瘪板,用來進行dynamic shared memory初始化的函數(shù)吴趴,如果沒有必要進行內存初始化,可以忽略侮攀。
- amparallelrescan锣枝,用來重新啟動并行scan的函數(shù),這時所有在共享內存里的數(shù)據(jù)都會被重置兰英。
其他函數(shù)
- amcanreturn 檢查某個列在這個index中是否支持index only scan撇叁。這個index only scan之前也介紹過,如果本次查詢所有需要的column都存在于index的key中畦贸,那么就可以使用index only scan陨闹。
- amoptions 向這個index設定一些option參數(shù),隨便舉一個參數(shù)的例子:“autovacuum_enabled = false”薄坏,禁止對索引進行自動vacuum趋厉。
鎖
用access method對索引進行更新,必須支持多個session“同時”操作胶坠,這就需要引入鎖君账。在scan的時候,只需要一個讀鎖就可以涵但,pg里用的是AccessShareLock杈绸,而更新的時候帖蔓,用的是RowExclusiveLock,寫鎖的粒度是單行數(shù)據(jù)瞳脓。
對索引進行更新時有幾個必須遵循的規(guī)則:
- 先在heap上產(chǎn)生新數(shù)據(jù)塑娇,然后才建索引條目。
- 先刪掉索引劫侧,再刪掉heap數(shù)據(jù)埋酬。
- 每一個進行中的index scan,當前最后一次調用amgettuple所返回的條目烧栋,它所在的index page都必須被pin住写妥,而且此page里面存儲的所有條目在此時都是不能被刪除的。這主要是出于non-MVCC snapshot的考慮审姓。如果我們pin住了index page珍特,那么索引就不可能被刪除,根據(jù)上一條規(guī)則魔吐,那么heap數(shù)據(jù)也不能被刪除扎筒,就可以避免一個session正在進行index scan,另外一個session把恰好要取的數(shù)據(jù)從heap上刪掉的情況酬姆。然而對于amgetbitmap嗜桌,上文說過這個操作是批處理的,當然也不會pin住index page辞色,所以bitmap scan只可以用在MVCC snapshot上骨宠。
唯一性檢查
注意,目前只有btree索引支持唯一性相满。唯一性意味著一個key值只能對應一條heap數(shù)據(jù)层亿。
其實在物理方面,一個key值只存儲一個條目是不可能的立美。因為我們需要支持MVCC棕所,一個key需要存儲多個版本的條目。所以悯辙,所謂“唯一性”的限制僅限于同一個版本。如果要在支持唯一性的索引中插入新的條目迎吵,我們需要分如下幾種情況考慮:
- 發(fā)現(xiàn)了沖突躲撰,但有沖突的條目在當前的transaction中已經(jīng)被刪掉了,這種情況沒問題击费。
- 有沖突拢蛋,而且有沖突的條目是另外一個transaction插入的,并且那個transaction還沒有被提交蔫巩。那么我們只能等待谆棱。如果那個transaction被roll back了,那么沖突就不存在了。否則直秆,就會產(chǎn)生唯一性的沖突船庇。
- 同樣,發(fā)現(xiàn)了沖突个从,不過有沖突的條目在另一個尚未提交的transaction里被刪掉了脉幢,我們還是需要等待,和2是相同的道理嗦锐。
Tips: create unique index concurrently
對一個很大的表創(chuàng)建索引是很耗時的嫌松,為了能在創(chuàng)建索引的時候不至于長時間block其他需要對同一個表進行寫操作的session,pg提供了concurrently這個關鍵詞奕污。如果用并行模式創(chuàng)建索引萎羔,其他的session可以同時對數(shù)據(jù)表進行增刪改的操作(讀的操作任何時候都不受建索引的影響)。
但是這個特性也給我們的一致性檢查帶來了麻煩碳默。如果我們在并行創(chuàng)建唯一索引時發(fā)現(xiàn)了沖突贾陷,在正式報警之前,需要再次確認有沖突的那一行heap數(shù)據(jù)沒有被concurrent的其他session刪掉腻窒,以防止錯誤報警昵宇。當然,這個再次確認的操作是需要access method自己去做的儿子,在我們的程序里需要顯式寫出去heap查找某一行數(shù)據(jù)狀態(tài)(是不是還活著的)的代碼瓦哎。