PostgreSQL技術內幕(七)索引掃描

HashData發表於2023-04-21

索引概述
資料庫索引,是將一個表的某些欄位的資料進行重新組織的資料庫物件。透過使用索引,可以大大加速資料庫的一些操作,其背後的思想也很簡單樸素:空間換時間。

資料庫中的索引,可以類比為一本書的目錄,當我們在書中查詢某資訊的時候,藉助目錄,可以快速定位到對應的章節,從而避免了從整本書中去翻閱,加速了查詢的過程。

索引分類
Postgres 中常見的索引大致有下面的這幾種,其中 BTree 索引是使用最廣泛的,也是建立索引時預設的選項。


索引掃描的例子
下面透過一個例子來體會索引對錶掃描的效能的影響。我們首先建立一個測試表,例如叫 articles,並向其中插入一些測試的資料。

CREATE TABLE articles (  id SERIAL8 NOT NULL PRIMARY KEY,  a text,  b text,  c text);INSERT INTO articles(a, b, c)SELECTmd5(random()::text),md5(random()::text),md5(random()::text)from (  SELECT * FROM generate_series(1,1000000) AS id) AS x;

我們從這個表中查詢一條資料,例如查詢 a = '65c966eb2be73daf418c126df8dc33b5' 的資料,其查詢計劃如下:
圖片
可以看到這裡使用了順序掃描(Seq Scan),並且代價(Cost)是 22450。如果我們給欄位 a 加上一個索引(預設是 BTree),create index on articles (a),然後再執行這個 sql 語句,其查詢計劃如下:
圖片
可以看到這裡使用到了索引掃描(Index Scan),並且代價是 8,相較於順序掃描的 22450,查詢的代價大大降低了,查詢的效能由此得到了大幅的提升。

掃描方法
順序掃描
當對無索引的欄位進行查詢,或者判斷到查詢將返回大多數資料時,查詢最佳化器將會使用順序掃描方法。還是以之前的 articles 表為例,這裡我們查詢了 id > 100 的資料,包含了大部分該表中的資料,所以儘管 id 列上有索引,但還是會使用順序掃描。
圖片
索引掃描
如果判斷到查詢將會命中非常少量的資料時,查詢最佳化器將會選擇索引掃描方法,上面的例子已經有對應的展示了。下面是一個掃描索引範圍的例子,可以看到命中資料佔表資料的少量,選擇索引掃描是最高效的。
圖片
點陣圖索引掃描
儘管索引掃描的資料量一般較少,但是這個掃描需要隨機 IO 操作,因此對比順序掃描使用的順序 IO 操作,它的代價並不總是更小。所以在命中適中資料(少量與多數之間),順序掃描和索引掃描各自都有缺陷。針對這種情況,一般可以採用點陣圖索引掃描,其原理是將需要訪問的頁面有序化,將隨機 IO 轉為順序 IO。
圖片
大致操作步驟如下:

  • 使用索引掃描到滿足條件的所有 TID
  • 用 TID 列表按照頁面的訪問順序構建一個點陣圖讀取資料記錄時,同一個頁面只需要
  • 讀取一次下圖描述了 Postgres 中幾種表資料掃描的方式,查詢最佳化器會根據計算的代價選擇最優的掃描方法。
    圖片
    索引物理儲存postgres 中的索引是一種二級索引,即在物理儲存上,索引資料和對應的表資料是分離開的。每個特定的索引物件都儲存為了一張獨立的關係表,並且都能夠在 pg_class 系統表中查詢到。
    圖片
    以 BTree 為例,其大致的結構如下:
    圖片
    B+ 樹的大致特點:
  • 樹層級更少:每個內部節點不再儲存資料,因此能儲存的鍵值會更多,於是導致樹的層級更少且查詢資料也更快(減少了隨機IO)。
  • 查詢速度更穩定:因為所有資料都存在葉子節點上,因此每次查詢的次數(樹的高度次隨機IO操作)都相同,查詢速度也要更穩定。
  • 遍歷查詢更方便:B+Tree的葉子節點資料構成了一個有序連結串列,在遍歷查詢時,首先定位第一個鍵值的位置,然後沿著連結串列即可訪問到全部資料。BTree 中的每一個節點在物理結構上儲存為一個 page,page 的結構和 heap 表的類似,如下:
    圖片
    以 BTree 為例,索引中的內容可以理解為一個由鍵值到資料元組 TID 的對映,其中 TID 由一個塊號和偏移組成。
    圖片
    索引建立
    當使用者使用 create index on table (col) 語句後,將會經過語法解析、許可權檢查等階段,然後建立索引關係,更新系統後設資料,最後使用表中的資料構建一個完整的B-Tree 索引。
    主要的函式呼叫路徑如下:
ProcessUtility() Utility語句的處理入口DefineIndex() 定義一個索引(異常判斷,準備index_create()的輸入引數)index_create() 建立一個索引(建立關係檔案並更新系統表資料)index_build() 構建索引的外層介面bt_build() B-Tree的索引構建邏輯

以 BTree 為例,使用表中的資料來構建 B-Tree 索引總體分為兩步,一是將表中的資料排序,二是根據有序的資料元組,遍歷自底向上構建整個 BTree。這裡主要是會針對不同的索引型別,呼叫不同的 ambuild 方法,其中 BTree 對應的方法是 btbuild,下圖是索引相關介面的訪問關係,不同的索引訪問方法透過 IndexAM 進行抽象,供上層執行器呼叫。
圖片
索引掃描
索引掃描在執行器中的三個步驟分別是

  • ExecInitIndexScan
  • ExecIndexScan
  • ExecEndIndexScan

ExecInitIndexScan
主要負責初始化索引掃描的狀態結構體 IndexScanState 核心任務是將索引掃描的過濾條件轉換為各種型別的掃描鍵 ScanKey。

  • ScanKey 主要儲存了索引列的資訊,操作函式以及待比較的函式,ScanKey 描述了一個完整的過濾條件,並用於索引掃描
  • 但如果過濾條件是一個複雜的表示式,引入了 iss_RuntimeKeys 來處理
    IndexScanState 的主要欄位:

型別 欄位 描述

Init 階段主要關注的是 ExecIndexBuildScanKeys 方法,此方法的作用是將掃描過濾條件轉化為各種型別的掃描鍵 ScanKey。

索引的過濾條件分為了以下五種情況:

  • 常數或普通運算,直接存入 ScanKey
  • 非常數的值表示式運算,此時執行器節點無法在初始階段得到表示式的結果,需要暫時存入 iss_RuntimeKeys
  • RowCompareExpr,比如過濾條件是“(indexkey1,indexkey2)> (1,2)”,表示多個過濾條件的組合,遍歷所有的子過濾條件,分別存入 iss_ScanKeys 或者 iss_RuntimeKeys
  • ScalarArrayOpExpr,比如過濾條件是“indexkey1 = ANY(1,10,20)”,如果索引支援處理基於陣列的搜尋,分別將常數存入 ScanKey 或者 RuntimeKey,如果不支援陣列搜尋,例如 Hash、GIN、Gist 索引,則將過濾條件存入 arrayKeys
  • NullTest,索引鍵是否為 NULL,例如_"indexkey IS NULL/IS NOT NULL",設定 ScanKey 對應的值即可_

ExecIndexScan
負責基於索引讀取元組,並返回給執行器上層節點。函式 IndexNext 不斷進行索引掃描,讀取元組,並將元組封裝進 TupleTableSlot 傳遞給上層節點。

  • 此函式的主要引數是 IndexScanDesc,儲存了 scan 過程中的狀態資訊
  • 透過 xs_heap_continue 判斷是否在 HOT 鏈上,如果是的話不做任何操作
  • 否則呼叫 index_getnext_tid 返回一個 TID
    在 pg_am 表中查詢 amgettuple 對應的內層介面函式
    呼叫這個函式(例如 BTree 中的 btgettuple),根據具體的索引實現返回一個 TID
  • 呼叫 index_fetch_heap 獲取實際的元組

ExecEndIndexScan
主要負責清理工作,釋放計算 RuntimeKey 的記憶體上下文,並關閉相關索引表和資料表。

相關文章