PostgreSQL中的索引(三) --Hash


許多現代編程語言都將哈希表作為基本數據類型。從表面上看,哈希表看起來像一個常規數組,使用任何數據類型(例如字符串)建立索引,而不僅是使用整數。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開始。

 

原文地址:https://habr.com/en/company/postgrespro/blog/442776/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM