POSTGRESQL 哈希索引性能

作者簡介

Laurenz Albe

cybertec 公司工程師

譯者簡介

王志斌

PostgreSQL 愛好者

校對者簡介

崔鵬

PostgreSQL 愛好者

在 PostgreSQL 中有很多種索引類型,其中哈希索引是最廣泛被忽視的。最近有人問我一個關於哈希索引性能的問題,這讓我意識到了這一點。是時候探索一下這個鮮爲人知的 PostgreSQL 角落並進行一些基準測試了!

哈希索引的歷史

PostgreSQL 歷來擁有哈希索引(我查看了 4.2 版本的源代碼),但在 v10 版本之前它們在崩潰時不夠安全。因此在舊版本中基本不能使用它們。正是由於這個原因,哈希索引在 PostgreSQL 開發人員中受到了很少的關注。但即使在它們成爲一等公民之後,它們也一直處於邊緣地位。現在是時候讓它們登臺展示它們的能力了。

哈希索引實現細節

您可以在 hash README 文件中找到詳細的描述。

哈希索引有兩個或更多的桶頁,用於存儲系統定義哈希函數索引值的結果。每當索引中的條目平均數超過依賴於 fillfactor 的某個特定值時,PostgreSQL 會通過拆分每個桶頁來將桶頁的數量翻倍。對於大型索引,PostgreSQL 將這種翻倍操作分成四個批次執行,以便在多個 DML 操作中分攤工作量。因此,對具有哈希索引的表進行 INSERT 操作有時可能會變得非常緩慢,類似於在 GIN 索引中清理未決列表的效果。如果某個哈希值不適合其所屬的桶頁(但尚未到達翻倍索引的時間),PostgreSQL 會將其放入特定目的的溢出頁中。除非一些被索引的值非常頻繁出現,否則這種情況不應經常發生。

哈希索引的潛在用途

哈希索引只能有單個列,它們只支持相等搜索,並且它們不能強制唯一性。因此,它們實際上不能做任何 B-tree 索引無法做到的事情,只有一個例外:在 B-tree 索引中,條目的長度限制是頁面大小的三分之一,而在哈希索引中,您可以用於任意大小的值,因爲索引中僅存儲哈希值。不過,這應該是一個不常見的情況。

您有多麼頻繁的需要實現高效查詢:

SELECT id, name FROM person WHERE mugshot =  '\xffd8ffdb...' ;

文檔 暗示哈希索引在某些情況下可能比 B-tree 索引更好:

哈希索引最適用於在較大表上執行等值掃描的 SELECT 和 UPDATE-heavy 工作負載。在 B-tree 索引中,搜索必須通過樹下降直到找到葉子頁。在包含數百萬行的表中,這種下降可能會增加對數據的訪問時間。哈希索引中等價於葉子頁的部分被稱爲桶頁。相反,哈希索引允許直接訪問桶頁,從而在較大表中可能減少索引訪問時間。在索引 / 數據大於 shared_buffers/RAM 時,這種 “邏輯 I/O” 的減少會更加明顯。

由於溢出情況,我們可以說哈希索引最適用於唯一的、幾乎唯一的數據或每個哈希桶中行數較少的數據。

一個用於測試哈希索引性能的測試平臺

我們將使用如下函數和表:

CREATE FUNCTION mkbig(integer) RETURNS text
   LANGUAGE sql AS
'SELECT repeat(md5($1::text), 20)';
CREATE TABLE hashtest (
   c1 bigint,
   c2 bigint,
   c3 text,
   c4 text
);

然後我們在其中一個列上創建一個哈希索引或 B-tree 索引。之後我們將加載 1000 萬行並查看所需的時間:

\timing on
INSERT INTO hashtest
SELECT i, i / 10000,  mkbig(i), mkbig(i / 10000)
FROM generate_series(1, 10000000) AS g(i);

對於 text 列,初始的想法是插入一些既不小也不足以觸發 PostgreSQL 的 TOAST 機制的值。

第二列和第四列包含重複 10000 次的值,以便我們看看哈希索引是如何處理這種情況的。如果有的話,我期望哈希索引在第三列上表現出色,因爲該列包含大且唯一的值。

創建索引後,我將設置提示位並收集統計信息,希望能獲得良好的執行計劃和可重複的測量結果:

VACUUM (ANALYZE) hashtest;

之後,我將在一個 DO 語句中執行 100000 次索引掃描:

\timing on
DO
$$DECLARE
   i bigint;
   r text;
BEGIN
   FOR i IN 1..100000 LOOP
      SELECT * INTO r
      FROM hashtest
      -- for c3 and c4, we will use mkbig(i / 100)
      WHERE c1 = i / 100;
   END LOOP;
END;$$;

基準測試結果

這個基準測試是在我的筆記本電腦上進行的,它配備了 8 個 Intel Core i7 CPU 核心,一塊運行 Linux 6.6.6 和 PostgreSQL 16.1 的 NVMe 硬盤,配置與標準配置相同。爲了得到索引修改或索引掃描的淨時間,我將沒有索引運行時的 INSERT 時間減去,並把 INSERT 開銷和索引掃描時間除以迭代次數。所有測量都進行了三次,並選擇了中位數值。

MLRgJc

對基準測試結果的討論

對於 bigint 列,使用哈希索引在插入數據時比 b-tree 索引慢得多。對於重複值(c2),當查詢表時,哈希索引也比較慢。唯一 bigint 列的 select 性能能夠競爭。

當涉及到長文本列時,哈希索引在插入過程中比 b-tree 索引要好得多。這很容易解釋,因爲對一個 4 字節的哈希值進行索引要比對一個 644 字節的字符串(包括 varlena 頭)更容易。

對於唯一字符串來說,哈希索引的查詢性能也要好兩倍。其原因在於,對於大索引條目,每個 b-tree 頁只能容納少量的索引條目,索引樹變得非常窄並且很深。PostgreSQL 必須遍歷許多中間的 b-tree 頁才能到達葉節點。對每個條目重複 10000 次的情況,b-tree 索引能夠競爭的原因有兩個:

• 在範圍掃描時,PostgreSQL 可以保持在葉頁級別,並且不必反覆遍歷深度 b-tree

• 對於重複條目,PostgreSQL 可以從 v13 引入的 index key deduplication(索引關鍵鍵去重)功能中受益 - 這會顯著減小索引的大小,從而加快掃描索引的速度

結論

如果你認爲哈希索引比 b-tree 索引更好,請不要忘記我構造了一個特別設計的異類測試案例,這個案例特別適合哈希索引。在普通情況下,b-tree 索引的性能優於哈希索引。這並不一定是因爲哈希索引從根本上比 b-tree 索引差:如果它們得到與 b-tree 索引同樣多的關注和優化,情況可能會有所不同。我認爲沒有理由認爲去重或底部索引元組刪除(這是兩個最近的 b-tree 索引性能特性)不能同樣適用於哈希索引。

在當前的情況下,你可以安全地忽略哈希索引。如果你有類似需要快速進行自由形式長用戶評論的等值搜索等瘋狂要求,你可能會重新考慮。或者你可以在 hashtext(comment) 上創建一個 b-tree 索引,然後對其進行搜索。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/PsLmkjqCkOECkTSJ5O6bEA