許多現代編程語言都將哈希表作為基本數據類型。從表面上看,哈希表看起來像一個常規數組,使用任何數據類型(例如字符串)建立索引,而不僅是使用整數。PostgreSQL中的哈希索引也是以類似的方式構造的。這是如何運作的呢?
作為一個規則,數據類型允許的值范圍非常大:在一個類型為«text»的列中,我們可以設想有多少不同的字符串?同時,在某個表的«text»的列中實際存儲了多少不同的值?通常不會有那么多。
哈希的思想是將一個小數字(從0到N−1,N個值)與任意數據類型的值相關聯。這樣的關聯稱為哈希函數。獲得的數字可以用作常規數組的索引,其中存儲對表行(tid)的引用。這個數組的元素稱為哈希表bucket——如果同一個索引值出現在不同的行中,那么一個bucket可以存儲多個tid。
哈希函數越均勻地按桶分配原值,效果就越好。但即使是一個好的哈希函數,有時也會對不同的原值產生相同的結果——這叫做沖突。因此,一個bucket可以存儲對應於不同鍵的TIDs,因此,需要重新檢查從索引中獲得的TIDs。
舉個例子,我們能想到字符串的哈希函數是什么?讓桶的數目為256。然后,以桶號為例,我們可以獲取第一個字符的代碼(假設采用單字節字符編碼)。這是個好的哈希函數嗎?顯然不是:如果所有字符串都以相同的字符開頭,那么它們都將進入一個bucket,因此一致性是不可能的,所有的值都需要重新檢查,哈希就沒有任何意義了。如果我們把所有字符的編碼以256的模加起來會怎么樣?這樣會好得多,但還遠遠不夠理想。如果您對PostgreSQL中這樣一個哈希函數的內部特性感興趣,請查看hashfunction.c中的hash_any()定義。
索引結構
讓我們回到哈希索引。對於某種數據類型的值(索引鍵),我們的任務是快速找到匹配的TID。
當插入到索引中時,讓我們計算鍵的哈希函數。PostgreSQL中的哈希函數總是返回«integer»類型,其范圍為2的32次方≈40億個值。桶的數量最初等於兩個,然后根據數據大小動態增加。桶數可以使用位算法從哈希碼計算出來。這是我們要放TID的桶。
但是這是不夠的,因為匹配不同鍵的TIDs可以放在同一個桶中。我們該怎么辦?除了TID之外,還可以將鍵的原值存儲在桶中,但是這將大大增加索引大小。為了節省空間,bucket存儲的不是鍵值,而是鍵值的哈希代碼。
在搜索索引時,我們計算鍵的哈希函數並獲得桶號。現在剩下的工作是遍歷bucket的內容,只返回具有適當哈希碼的匹配tid。這是有效的,因為存儲的«哈希碼--TID»對是有序的。
然而,兩個不同的鍵可能不僅進入了一個bucket,而且具有相同的四字節哈希碼——沖突還未消除。因此,訪問方法要求常規索引引擎通過重新檢查表行中的條件來驗證每個TID(引擎可以在進行可見性檢查的同時執行此操作)。
將數據結構映射到頁
如果我們從buffer cache管理器查看索引,而不是從查詢計划和執行的角度來看,那么所有信息和所有索引行都必須打包到頁中。這樣的索引頁存儲在buffer cache中,並以與表頁完全相同的方式從那里刷出。
如圖所示,哈希索引使用了四種頁面(灰色矩形):
·元數據頁--零頁號,它包含了索引內部的信息。
·桶頁--索引的主要頁,它將數據存儲為«哈希碼--TID»對。
·溢出頁--結構與桶頁相同,當一個頁面不足以容納一個桶時使用。
·位圖頁--跟蹤當前清除的溢出頁面,這些溢出頁面可以被其他桶重用。
從索引頁元素開始的向下箭頭表示TIDs,即對表行的引用。
每次索引增加時,PostgreSQL會立即創建兩倍於上次創建的桶(因此,頁面也是如此)。為了避免一次分配大量的頁面,version 10進行改進。至於溢出頁,它們是在需要出現時分配的,並在位圖頁中跟蹤,位圖頁也是在需要出現時分配的。
注意,哈希索引不能減小大小。如果我們刪除一些索引行,分配的頁將不會返回到操作系統,而只會在vacuuming后用於新數據。減少索引大小的唯一選項是使用REINDEX或VACUUM FULL命令從頭重新構建。
示例
讓我們看看哈希索引是如何創建的。為了避免設計自己的表,從現在開始,我們將使用air transport的演示數據庫,這次我們將考慮flights表。
demo=# create index on flights using hash(flight_no); WARNING: hash indexes are not WAL-logged and their use is discouraged CREATE INDEX demo=# explain (costs off) select * from flights where flight_no = 'PG0001'; QUERY PLAN ---------------------------------------------------- Bitmap Heap Scan on flights Recheck Cond: (flight_no = 'PG0001'::bpchar) -> Bitmap Index Scan on flights_flight_no_idx Index Cond: (flight_no = 'PG0001'::bpchar) (4 rows)
當前哈希索引實現的不便之處在於,與索引相關的操作沒有記錄在寫WAL中(創建索引時,PostgreSQL會發出警告)。因此,哈希索引在失敗后無法恢復,並且不參與復制。此外,哈希索引的通用性遠遠低於«btree»,其效率也值得懷疑。所以現在使用這些索引是不切實際的。
然而,這種情況最早將在今年秋天(2017年)PostgreSQL的第10版發布后發生改變。在這個版本中,哈希索引最終支持寫wal文件;此外,內存分配得到了優化,並發工作效率更高。
哈希的語義
但為什么哈希索引幾乎從PostgreSQL誕生之初就存在到9.6無法使用?問題是DBMS廣泛使用了哈希算法(特別是哈希連接和分組),系統必須知道哪個哈希函數應用於哪個數據類型。但是這種通信不是靜態的,也不能一次性地設置,因為PostgreSQL允許動態地添加新的數據類型。這是一種通過哈希的訪問方法,這種對應被存儲、用輔助函數和操作符族的聯系來表示。
demo=# select opf.opfname as opfamily_name, amproc.amproc::regproc AS opfamily_procedure from pg_am am, pg_opfamily opf, pg_amproc amproc where opf.opfmethod = am.oid and amproc.amprocfamily = opf.oid and am.amname = 'hash' order by opfamily_name, opfamily_procedure; opfamily_name | opfamily_procedure --------------------+-------------------- abstime_ops | hashint4 aclitem_ops | hash_aclitem array_ops | hash_array bool_ops | hashchar ...
雖然這些函數沒有文檔記錄,但它們可用於計算適當數據類型值的哈希碼。例如,«hashtext»函數如果用於«text_ops»操作符家族:
demo=# select hashtext('one'); hashtext ----------- 127722028 (1 row) demo=# select hashtext('two'); hashtext ----------- 345620034 (1 row)
屬性
讓我們看看哈希索引的屬性,其中該訪問方法向系統提供關於自身的信息。上次我們提供了查詢。
name | pg_indexam_has_property ---------------+------------------------- can_order | f can_unique | f can_multi_col | f can_exclude | t name | pg_index_has_property ---------------+----------------------- clusterable | f index_scan | t bitmap_scan | t backward_scan | t name | pg_index_column_has_property --------------------+------------------------------ asc | f desc | f nulls_first | f nulls_last | f orderable | f distance_orderable | f returnable | f search_array | f search_nulls | f
哈希函數不保留順序關系:如果哈希函數的一個鍵值小於另一個鍵值,不可能得出鍵本身是如何排序的結論。因此,通常哈希索引只能支持«equals»操作:
demo=# select opf.opfname AS opfamily_name, amop.amopopr::regoperator AS opfamily_operator from pg_am am, pg_opfamily opf, pg_amop amop where opf.opfmethod = am.oid and amop.amopfamily = opf.oid and am.amname = 'hash' order by opfamily_name, opfamily_operator; opfamily_name | opfamily_operator ---------------+---------------------- abstime_ops | =(abstime,abstime) aclitem_ops | =(aclitem,aclitem) array_ops | =(anyarray,anyarray) bool_ops | =(boolean,boolean) ...
因此,哈希索引不能返回有序數據(«can_order»,«orderable»)。哈希索引不操作空值也是出於同樣的原因:對於空值,«equals»操作沒有意義(«search_nulls»)。
因為哈希索引不存儲鍵(但只存儲它們的哈希碼),所以它不能用於index-only訪問(«returnable»)。
此訪問方法也不支持多列索引(«can_multi_col»)。
內部原理
從version 10開始,可以通過“pageinspect”擴展來查看哈希索引的內部內容。這是它的樣子:
demo=# create extension pageinspect;
元數據頁(我們得到索引中的行數和最大使用桶數):
demo=# select hash_page_type(get_raw_page('flights_flight_no_idx',0)); hash_page_type ---------------- metapage (1 row) demo=# select ntuples, maxbucket from hash_metapage_info(get_raw_page('flights_flight_no_idx',0)); ntuples | maxbucket ---------+----------- 33121 | 127 (1 row)
一個桶頁(我們得到live的元組和死元組的數量,也就是那些可以被vacuum的元組):
demo=# select hash_page_type(get_raw_page('flights_flight_no_idx',1)); hash_page_type ---------------- bucket (1 row) demo=# select live_items, dead_items from hash_page_stats(get_raw_page('flights_flight_no_idx',1)); live_items | dead_items ------------+------------ 407 | 0 (1 row)
但是,如果不檢查源代碼,幾乎不可能弄清所有可用字段的含義。
如果您希望這樣做,您應該從https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/hash/README;hb=HEAD開始。