PostgreSQL 內部機制深度解析:從 MVCC 到查詢執行引擎


0. 前言與生成說明

本文是一篇深度技術解析文章,目標是讓讀者完整理解 PostgreSQL 的內部運作機制。

文章目標

讀完本文,你將理解:

  • PostgreSQL 的整體架構與模塊劃分
  • MVCC(多版本並發控制)的實現原理
  • Tuple 的物理結構與可見性判斷
  • 查詢處理的完整流程:Parser → Analyzer → Optimizer → Executor
  • Buffer Pool 管理機制
  • 各類索引的實現原理

1. PostgreSQL 整體架構

PostgreSQL 是一個基於 進程模型 的關係型資料庫管理系統,使用 C 語言開發。其核心優勢包括:

特性說明
ACID 完整支援完整的事務保證
MVCC讀寫不互相阻塞
豐富的索引類型B-tree、Hash、GiST、GIN、BRIN、SP-GiST
可擴展性支援自定義資料類型、函數、索引方法
SQL 標準兼容高度遵循 SQL 標準

1.1 源代碼結構

從 PostgreSQL 源代碼結構可以看出其模塊化設計:

postgres-src/
├── src/
│   ├── backend/          # 後端核心
│   │   ├── access/       # 存儲訪問層(heap、index)
│   │   ├── catalog/      # 系統目錄
│   │   ├── commands/     # SQL 命令處理
│   │   ├── executor/     # 查詢執行器
│   │   ├── optimizer/    # 查詢優化器
│   │   ├── parser/       # SQL 解析器
│   │   ├── postmaster/   # 進程管理
│   │   ├── replication/  # 複製
│   │   ├── storage/      # 存儲層
│   │   │   ├── buffer/   # Buffer Pool
│   │   │   ├── lmgr/     # Lock Manager
│   │   │   └── smgr/     # Storage Manager
│   │   ├── tcop/         # Traffic Cop(查詢入口)
│   │   └── utils/        # 工具函數
│   ├── include/          # 頭文件
│   └── interfaces/       # 客戶端接口(libpq)
└── contrib/              # 擴展模塊

1.2 進程架構

PostgreSQL 採用 多進程架構,每個客戶端連接對應一個獨立的後端進程:

flowchart TB
    subgraph Client["客戶端"]
        C1["Client 1"]
        C2["Client 2"]
        C3["Client N"]
    end

    subgraph Postmaster["Postmaster (主進程)"]
        PM["postmaster<br/>監聽連接請求<br/>Fork 子進程"]
    end

    subgraph BackendProcesses["Backend Processes"]
        B1["Backend 1<br/>處理 Client 1"]
        B2["Backend 2<br/>處理 Client 2"]
        B3["Backend N<br/>處理 Client N"]
    end

    subgraph BackgroundWorkers["Background Workers"]
        BW["Background Writer<br/>定期寫髒頁"]
        WW["WAL Writer<br/>寫 WAL 日誌"]
        CP["Checkpointer<br/>執行 Checkpoint"]
        AV["Autovacuum<br/>自動清理"]
        SS["Stats Collector<br/>統計收集"]
    end

    subgraph SharedMemory["Shared Memory"]
        BP["Buffer Pool"]
        WAL["WAL Buffers"]
        LOCK["Lock Tables"]
        CLOG["Commit Log"]
    end

    C1 --> PM
    C2 --> PM
    C3 --> PM
    PM -->|fork| B1
    PM -->|fork| B2
    PM -->|fork| B3

    B1 <--> BP
    B2 <--> BP
    B3 <--> BP

    BW <--> BP
    WW <--> WAL
    CP <--> BP

關鍵角色說明:

進程職責
Postmaster主進程,監聽連接請求,fork 後端進程
Backend處理單一客戶端的所有查詢
Background Writer定期將髒頁寫入磁碟,減少 checkpoint 壓力
WAL Writer將 WAL buffer 寫入磁碟
Checkpointer執行 checkpoint,確保資料持久化
Autovacuum自動執行 VACUUM 和 ANALYZE

2. MVCC:多版本並發控制

2.1 MVCC 核心理念

PostgreSQL 的 MVCC 實現基於 Snapshot Isolation (SI),核心原則:

讀操作永不阻塞寫操作,寫操作永不阻塞讀操作

與使用 Rollback Segment 的資料庫(如 Oracle)不同,PostgreSQL 將新版本資料 直接寫入原表

flowchart LR
    subgraph Oracle方式["Oracle: Rollback Segment"]
        O1["原始資料"] -->|更新| O2["新資料<br/>(覆蓋原位置)"]
        O1 -->|舊版本| RS["Rollback Segment"]
    end

    subgraph PG方式["PostgreSQL: 多版本存儲"]
        P1["Tuple v1<br/>(xmin=100)"]
        P2["Tuple v2<br/>(xmin=101)"]
        P3["Tuple v3<br/>(xmin=102)"]
        P1 -->|t_ctid| P2
        P2 -->|t_ctid| P3
    end

2.2 Transaction ID (XID)

每個事務都有唯一的 Transaction ID (XID)

// src/include/access/transam.h
typedef uint32 TransactionId;

#define InvalidTransactionId    ((TransactionId) 0)
#define BootstrapTransactionId  ((TransactionId) 1)
#define FrozenTransactionId     ((TransactionId) 2)
#define FirstNormalTransactionId ((TransactionId) 3)

XID 是 32 位無符號整數,存在 wraparound 問題(約 40 億事務後會繞回)。PostgreSQL 通過 VACUUM FREEZE 機制解決此問題。

2.3 Tuple 結構

每個 Tuple(資料行)包含 23 字節的固定頭部 加上實際資料:

// src/include/access/htup_details.h
struct HeapTupleHeaderData
{
    union
    {
        HeapTupleFields t_heap;   // 存儲在表中時使用
        DatumTupleFields t_datum; // 記憶體中使用
    } t_choice;

    ItemPointerData t_ctid;       // 當前或更新後的 TID (6 bytes)
    uint16      t_infomask2;      // 屬性數量 + 標誌位
    uint16      t_infomask;       // 各種標誌位
    uint8       t_hoff;           // 頭部偏移量
    bits8       t_bits[];         // NULL bitmap
    // 之後是實際的用戶資料
};

typedef struct HeapTupleFields
{
    TransactionId t_xmin;         // 插入此 tuple 的事務 ID
    TransactionId t_xmax;         // 刪除/鎖定此 tuple 的事務 ID
    union
    {
        CommandId   t_cid;        // 插入/刪除的命令 ID
        TransactionId t_xvac;     // VACUUM FULL 的事務 ID
    } t_field3;
} HeapTupleFields;

Tuple 物理結構圖:

┌─────────────────────────────────────────────────────────┐
│                    HeapTupleHeader                      │
├──────────────┬──────────────┬───────────┬───────────────┤
│   t_xmin     │    t_xmax    │  t_ctid   │ t_infomask/2  │
│   (4 bytes)  │   (4 bytes)  │ (6 bytes) │   (4 bytes)   │
├──────────────┴──────────────┴───────────┴───────────────┤
│                   t_hoff (1 byte)                       │
├─────────────────────────────────────────────────────────┤
│                NULL Bitmap (變長)                       │
├─────────────────────────────────────────────────────────┤
│                   Padding (對齊)                        │
├─────────────────────────────────────────────────────────┤
│                   User Data                             │
│              (實際的欄位資料)                           │
└─────────────────────────────────────────────────────────┘

2.4 可見性判斷規則

PostgreSQL 使用複雜的規則判斷一個 tuple 對當前事務是否可見:

flowchart TD
    Start["檢查 Tuple 可見性"]

    CheckXmin["檢查 t_xmin"]
    XminInvalid{"t_xmin<br/>無效/已回滾?"}
    XminCommitted{"t_xmin<br/>已提交?"}
    XminCurrent{"t_xmin =<br/>當前事務?"}
    XminInSnapshot{"t_xmin<br/>在 snapshot 中?"}

    CheckXmax["檢查 t_xmax"]
    XmaxInvalid{"t_xmax<br/>無效?"}
    XmaxCommitted{"t_xmax<br/>已提交?"}
    XmaxCurrent{"t_xmax =<br/>當前事務?"}
    XmaxInSnapshot{"t_xmax<br/>在 snapshot 中?"}

    Visible["可見"]
    Invisible["不可見"]

    Start --> CheckXmin
    CheckXmin --> XminInvalid
    XminInvalid -->|是| Invisible
    XminInvalid -->|否| XminCommitted

    XminCommitted -->|否| XminCurrent
    XminCurrent -->|否| Invisible
    XminCurrent -->|是| CheckXmax

    XminCommitted -->|是| XminInSnapshot
    XminInSnapshot -->|是| Invisible
    XminInSnapshot -->|否| CheckXmax

    CheckXmax --> XmaxInvalid
    XmaxInvalid -->|是| Visible
    XmaxInvalid -->|否| XmaxCommitted

    XmaxCommitted -->|否| XmaxCurrent
    XmaxCurrent -->|是| Invisible
    XmaxCurrent -->|否| Visible

    XmaxCommitted -->|是| XmaxInSnapshot
    XmaxInSnapshot -->|是| Visible
    XmaxInSnapshot -->|否| Invisible

核心邏輯(簡化):

// 簡化的可見性判斷邏輯
bool HeapTupleSatisfiesMVCC(HeapTuple tuple, Snapshot snapshot)
{
    // 1. 檢查插入事務 (t_xmin)
    if (!TransactionIdIsValid(tuple->t_xmin))
        return false;  // 無效的 tuple

    if (TransactionIdIsCurrentTransactionId(tuple->t_xmin)) {
        // 當前事務插入的,需檢查是否被當前事務刪除
        if (TransactionIdIsValid(tuple->t_xmax))
            return false;
        return true;
    }

    if (!XidInMVCCSnapshot(tuple->t_xmin, snapshot))
        return false;  // 插入事務對當前 snapshot 不可見

    // 2. 檢查刪除事務 (t_xmax)
    if (!TransactionIdIsValid(tuple->t_xmax))
        return true;  // 未被刪除

    if (XidInMVCCSnapshot(tuple->t_xmax, snapshot))
        return true;  // 刪除事務對當前 snapshot 不可見

    return false;  // 已被刪除
}

2.5 Commit Log (CLOG)

事務狀態存儲在 Commit Log (CLOG) 中,每個事務使用 2 bits:

#define TRANSACTION_STATUS_IN_PROGRESS      0x00
#define TRANSACTION_STATUS_COMMITTED        0x01
#define TRANSACTION_STATUS_ABORTED          0x02
#define TRANSACTION_STATUS_SUB_COMMITTED    0x03

CLOG 存儲在 pg_xact/ 目錄下,每個文件 256KB,可存儲 1M 個事務狀態。

2.6 事務隔離級別

PostgreSQL 支援三種隔離級別:

隔離級別現象防止實現方式
READ COMMITTED髒讀每條語句取新 snapshot
REPEATABLE READ髒讀、不可重複讀、幻讀事務開始時取 snapshot
SERIALIZABLE所有異常SSI (Serializable Snapshot Isolation)

Serializable Snapshot Isolation (SSI) 是 PostgreSQL 9.1 引入的真正可串行化實現,能檢測 Write Skew 等異常。


3. 查詢處理流程

一條 SQL 從接收到執行,經過多個階段:

flowchart TB
    subgraph Client
        SQL["SQL Query"]
    end

    subgraph TrafficCop["Traffic Cop (tcop/postgres.c)"]
        TC["PostgresMain()"]
    end

    subgraph Parser["Parser 階段"]
        LEX["Lexer<br/>詞法分析"]
        SYN["Parser<br/>語法分析"]
        PT["Parse Tree"]
    end

    subgraph Analyzer["Analyzer 階段"]
        AN["transformStmt()"]
        QT["Query Tree"]
    end

    subgraph Rewriter["Rewriter 階段"]
        RW["QueryRewrite()"]
        RWT["Rewritten Query"]
    end

    subgraph Planner["Planner/Optimizer 階段"]
        PL["planner()"]
        PATH["Path Selection"]
        PLAN["Plan Tree"]
    end

    subgraph Executor["Executor 階段"]
        EX["ExecutorRun()"]
        RESULT["Query Result"]
    end

    SQL --> TC
    TC --> LEX
    LEX --> SYN
    SYN --> PT
    PT --> AN
    AN --> QT
    QT --> RW
    RW --> RWT
    RWT --> PL
    PL --> PATH
    PATH --> PLAN
    PLAN --> EX
    EX --> RESULT

3.1 Parser:詞法與語法分析

Parser 使用 Flex (詞法)Bison (語法) 生成:

src/backend/parser/
├── scan.l          # Flex 詞法規則
├── gram.y          # Bison 語法規則
├── parser.c        # 入口
└── analyze.c       # 語義分析

範例:解析 SELECT 語句

SELECT name, age FROM users WHERE age > 18;

解析後生成的 Parse Tree 結構:

typedef struct SelectStmt
{
    NodeTag     type;           // T_SelectStmt
    List       *distinctClause; // DISTINCT
    IntoClause *intoClause;     // INTO
    List       *targetList;     // SELECT 列表 [name, age]
    List       *fromClause;     // FROM 子句 [users]
    Node       *whereClause;    // WHERE 條件 [age > 18]
    List       *groupClause;    // GROUP BY
    Node       *havingClause;   // HAVING
    List       *sortClause;     // ORDER BY
    Node       *limitCount;     // LIMIT
    Node       *limitOffset;    // OFFSET
    ...
} SelectStmt;

3.2 Analyzer:語義分析

Analyzer 將 Parse Tree 轉換為 Query Tree,進行:

  • 名稱解析:將表名、欄位名解析為 OID
  • 類型檢查:確保表達式類型正確
  • 權限初步檢查
  • 視圖/規則展開準備
// src/backend/parser/analyze.c
Query *
transformStmt(ParseState *pstate, Node *parseTree)
{
    switch (nodeTag(parseTree))
    {
        case T_SelectStmt:
            return transformSelectStmt(pstate, (SelectStmt *) parseTree);
        case T_InsertStmt:
            return transformInsertStmt(pstate, (InsertStmt *) parseTree);
        case T_UpdateStmt:
            return transformUpdateStmt(pstate, (UpdateStmt *) parseTree);
        case T_DeleteStmt:
            return transformDeleteStmt(pstate, (DeleteStmt *) parseTree);
        ...
    }
}

3.3 Rewriter:規則重寫

Rewriter 處理:

  • 視圖展開:將視圖查詢替換為底層表查詢
  • 規則應用:應用 CREATE RULE 定義的規則
  • 安全性屏障
// src/backend/rewrite/rewriteHandler.c
List *
QueryRewrite(Query *parsetree)
{
    List       *querylist;
    List       *results;

    // 應用非 SELECT 規則
    querylist = RewriteQuery(parsetree, NIL, 0);

    // 處理每個重寫後的查詢
    foreach(lc, querylist)
    {
        Query *query = (Query *) lfirst(lc);
        results = lappend(results, fireRules(query, ...));
    }

    return results;
}

3.4 Planner/Optimizer:查詢優化

優化器是 PostgreSQL 最複雜的模塊之一,負責生成最優執行計劃。

3.4.1 成本模型

PostgreSQL 使用基於成本的優化 (CBO):

// 成本計算的核心參數
double seq_page_cost = 1.0;      // 順序讀取一頁的成本
double random_page_cost = 4.0;   // 隨機讀取一頁的成本
double cpu_tuple_cost = 0.01;    // 處理一個 tuple 的成本
double cpu_index_tuple_cost = 0.005;
double cpu_operator_cost = 0.0025;

總成本 = 啟動成本 + 運行成本

3.4.2 Path 與 Plan

優化器首先生成所有可能的 Path,然後選擇成本最低的轉換為 Plan

flowchart TB
    subgraph Paths["所有可能的 Path"]
        P1["SeqScan Path<br/>cost=1000"]
        P2["IndexScan Path<br/>cost=50"]
        P3["BitmapScan Path<br/>cost=100"]
    end

    SELECT["選擇最低成本"]

    PLAN["Plan Tree<br/>IndexScan"]

    P1 --> SELECT
    P2 --> SELECT
    P3 --> SELECT
    SELECT --> PLAN

3.4.3 Join 策略

PostgreSQL 支援三種 Join 算法:

算法適用場景特點
Nested Loop小表驅動大表通用,支援所有 join 條件
Merge Join兩表都已排序需要等值條件
Hash Join大表 join需要等值條件,內存敏感
flowchart TB
    subgraph NL["Nested Loop"]
        NL1["Outer: 掃描 A"]
        NL2["Inner: 對每行掃描 B"]
        NL1 --> NL2
    end

    subgraph MJ["Merge Join"]
        MJ1["Sort A"]
        MJ2["Sort B"]
        MJ3["合併掃描"]
        MJ1 --> MJ3
        MJ2 --> MJ3
    end

    subgraph HJ["Hash Join"]
        HJ1["Build: 構建 B 的 Hash 表"]
        HJ2["Probe: 掃描 A,查 Hash"]
        HJ1 --> HJ2
    end

3.4.4 動態規劃 Join 順序

對於多表 join,優化器使用 動態規劃 尋找最優順序:

4 表 Join 的探索過程:

Level 1: {A}, {B}, {C}, {D}
Level 2: {A,B}, {A,C}, {A,D}, {B,C}, {B,D}, {C,D}
Level 3: {A,B,C}, {A,B,D}, {A,C,D}, {B,C,D}
Level 4: {A,B,C,D}

當表數量超過 geqo_threshold(默認 12)時,使用 GEQO(遺傳算法) 進行啟發式搜索。

3.5 Executor:查詢執行

Executor 採用 Volcano 模型(迭代器模型)

// 每個節點實現三個函數
ExecInitNode()   // 初始化
ExecProcNode()   // 獲取下一個 tuple(拉取模型)
ExecEndNode()    // 清理

執行流程:

flowchart TB
    subgraph ExecutorModel["Volcano Iterator Model"]
        ROOT["Result Node"]
        JOIN["Join Node"]
        SCAN1["Scan Node (A)"]
        SCAN2["Scan Node (B)"]

        ROOT -->|"ExecProcNode()"| JOIN
        JOIN -->|"ExecProcNode()"| SCAN1
        JOIN -->|"ExecProcNode()"| SCAN2
    end

    subgraph DataFlow["資料流向"]
        D1["Tuple from A"] --> D2["Tuple from B"]
        D2 --> D3["Joined Tuple"]
        D3 --> D4["Result"]
    end

常見執行節點:

節點類型功能源代碼
SeqScan順序掃描nodeSeqscan.c
IndexScan索引掃描nodeIndexscan.c
BitmapHeapScan位圖堆掃描nodeBitmapHeapscan.c
NestLoop嵌套循環 JoinnodeNestloop.c
HashJoinHash JoinnodeHashjoin.c
MergeJoin合併 JoinnodeMergejoin.c
Sort排序nodeSort.c
Aggregate聚合nodeAgg.c
Hash構建 Hash 表nodeHash.c

4. 存儲引擎

4.1 Page 結構

PostgreSQL 的基本 I/O 單位是 Page(Block),默認 8KB:

┌─────────────────────────────────────────────────────────┐
│                    PageHeaderData                       │
│  (24 bytes)                                             │
├─────────────────────────────────────────────────────────┤
│                    ItemIdData Array                     │
│  (Line Pointers,每個 4 bytes)                         │
│  [Item 1] [Item 2] [Item 3] ... [Item N]               │
├─────────────────────────────────────────────────────────┤
│                                                         │
│                    Free Space                           │
│                                                         │
├─────────────────────────────────────────────────────────┤
│                    Tuple Data                           │
│  (從頁尾向上增長)                                       │
│  [Tuple N] ... [Tuple 3] [Tuple 2] [Tuple 1]           │
├─────────────────────────────────────────────────────────┤
│                    Special Space                        │
│  (索引頁使用)                                           │
└─────────────────────────────────────────────────────────┘
// src/include/storage/bufpage.h
typedef struct PageHeaderData
{
    PageXLogRecPtr pd_lsn;      // 最後修改的 WAL LSN
    uint16      pd_checksum;    // 校驗和
    uint16      pd_flags;       // 標誌位
    LocationIndex pd_lower;     // 空閒空間起始
    LocationIndex pd_upper;     // 空閒空間結束
    LocationIndex pd_special;   // 特殊區域起始
    uint16      pd_pagesize_version;
    TransactionId pd_prune_xid; // 可能需要 pruning 的最舊 xid
} PageHeaderData;

4.2 Heap 表存儲

PostgreSQL 使用 Heap 結構存儲表資料:

// src/backend/access/heap/heapam.c
// 主要接口函數
heap_beginscan()     // 開始掃描
heap_getnext()       // 獲取下一個 tuple
heap_fetch()         // 通過 TID 獲取 tuple
heap_insert()        // 插入 tuple
heap_delete()        // 刪除 tuple(設置 t_xmax)
heap_update()        // 更新 tuple(刪除舊的 + 插入新的)

TID(Tuple Identifier)結構:

typedef struct ItemPointerData
{
    BlockIdData ip_blkid;    // 區塊號
    OffsetNumber ip_posid;   // 頁內偏移
} ItemPointerData;

// 例如 (1234, 5) 表示第 1234 頁的第 5 個 item

4.3 TOAST:大對象存儲

當 tuple 超過約 2KB 時,PostgreSQL 使用 TOAST (The Oversized-Attribute Storage Technique) 壓縮或外置存儲:

flowchart LR
    subgraph MainTable["主表"]
        T1["Tuple<br/>含 TOAST 指針"]
    end

    subgraph TOASTTable["TOAST 表"]
        C1["Chunk 1"]
        C2["Chunk 2"]
        C3["Chunk 3"]
    end

    T1 --> C1
    C1 --> C2
    C2 --> C3

TOAST 策略:

策略說明
PLAIN不壓縮,不外置
EXTENDED先壓縮,再外置(默認)
EXTERNAL只外置,不壓縮
MAIN先壓縮,儘量不外置

5. Buffer Pool 管理

5.1 Buffer Pool 架構

Buffer Pool 是 PostgreSQL 的核心組件,管理所有的共享緩衝區:

flowchart TB
    subgraph SharedBuffers["Shared Buffers (shared_buffers)"]
        B1["Buffer 1<br/>Page from Table A"]
        B2["Buffer 2<br/>Page from Index B"]
        B3["Buffer 3<br/>Page from Table A"]
        BN["Buffer N<br/>..."]
    end

    subgraph BufferDesc["Buffer Descriptors"]
        D1["Desc 1<br/>tag, state, refcount"]
        D2["Desc 2<br/>tag, state, refcount"]
        D3["Desc 3<br/>tag, state, refcount"]
    end

    subgraph HashTable["Buffer Hash Table"]
        HT["(RelFileNode, ForkNum, BlockNum)<br/>→ Buffer ID"]
    end

    HT --> D1
    D1 --> B1
    D2 --> B2
    D3 --> B3

5.2 Buffer 狀態管理

// src/include/storage/buf_internals.h
typedef struct BufferDesc
{
    BufferTag   tag;            // 標識:哪個表的哪一頁
    int         buf_id;         // Buffer 池中的位置

    /* 狀態標誌 */
    pg_atomic_uint32 state;     // 引用計數 + 標誌位

    int         wait_backend_pgprocno;
    int         freeNext;       // 空閒列表鏈接

    LWLock      content_lock;   // 內容鎖
} BufferDesc;

// 狀態標誌
#define BM_LOCKED           (1U << 22)  // 正在修改
#define BM_DIRTY            (1U << 23)  // 髒頁
#define BM_VALID            (1U << 24)  // 內容有效
#define BM_TAG_VALID        (1U << 25)  // tag 有效
#define BM_IO_IN_PROGRESS   (1U << 26)  // 正在進行 I/O
#define BM_IO_ERROR         (1U << 27)  // I/O 錯誤

5.3 緩衝區替換算法

PostgreSQL 使用 Clock-Sweep 算法(類 LRU):

flowchart TB
    subgraph ClockSweep["Clock-Sweep 算法"]
        direction LR
        B1["Buffer 1<br/>usage=2"]
        B2["Buffer 2<br/>usage=0"]
        B3["Buffer 3<br/>usage=1"]
        B4["Buffer 4<br/>usage=3"]
        B5["Buffer 5<br/>usage=0"]

        B1 --> B2 --> B3 --> B4 --> B5 --> B1
    end

    HAND["時鐘指針"]
    HAND -.-> B2

    ACTION["找到 usage=0 的 buffer<br/>驅逐並使用"]

算法步驟:

  1. 時鐘指針順序掃描 buffer
  2. 如果 usage_count > 0,則減 1 並繼續
  3. 如果 usage_count = 0,則驅逐此 buffer
  4. 每次訪問 buffer 時,usage_count 增加(最大值限制)

5.4 Buffer I/O 流程

// src/backend/storage/buffer/bufmgr.c
Buffer
ReadBuffer(Relation reln, BlockNumber blockNum)
{
    // 1. 查找 buffer hash table
    buf_id = BufTableLookup(tag);

    if (buf_id >= 0) {
        // 2. 命中:增加引用計數
        PinBuffer(buf_id);
        return buf_id;
    }

    // 3. 未命中:分配新 buffer
    buf_id = StrategyGetBuffer();  // Clock-Sweep

    // 4. 如果是髒頁,先寫回
    if (BufferIsDirty(buf_id))
        FlushBuffer(buf_id);

    // 5. 從磁碟讀取
    smgrread(reln, blockNum, BufferGetBlock(buf_id));

    // 6. 加入 hash table
    BufTableInsert(tag, buf_id);

    return buf_id;
}

6. 索引系統

6.1 索引類型總覽

索引類型適用場景特點
B-tree範圍查詢、等值查詢默認類型,最通用
Hash純等值查詢較少使用
GiST幾何類型、全文搜索通用搜索樹
GIN全文搜索、數組、JSONB倒排索引
BRIN自然有序的大表塊範圍索引,極小
SP-GiST電話號碼、IP 地址空間分區 GiST

6.2 B-tree 索引

B-tree 是最常用的索引類型:

flowchart TB
    subgraph BTree["B-tree 結構"]
        ROOT["Root Page<br/>[10, 50, 100]"]

        I1["Internal<br/>[1,3,7]"]
        I2["Internal<br/>[15,30,45]"]
        I3["Internal<br/>[55,70,90]"]

        L1["Leaf: 1→TID1"]
        L2["Leaf: 3→TID2"]
        L3["Leaf: 7→TID3"]
        L4["Leaf: 15→TID4"]
        L5["Leaf: 30→TID5"]

        ROOT --> I1
        ROOT --> I2
        ROOT --> I3

        I1 --> L1
        I1 --> L2
        I1 --> L3
        I2 --> L4
        I2 --> L5
    end
// src/backend/access/nbtree/nbtinsert.c
// B-tree 插入流程
_bt_doinsert()
{
    // 1. 從根節點開始查找
    stack = _bt_search(rel, key);

    // 2. 定位到葉子節點
    buf = stack->bts_buf;

    // 3. 插入 item
    _bt_insertonpg(rel, buf, key, item);

    // 4. 如果需要,分裂頁面
    if (PageGetFreeSpace(page) < item_size)
        _bt_split(rel, buf);
}

6.3 GIN 索引(倒排索引)

GIN(Generalized Inverted Index)適用於全文搜索:

flowchart LR
    subgraph Documents["文檔"]
        D1["Doc1: hello world"]
        D2["Doc2: hello postgres"]
        D3["Doc3: world database"]
    end

    subgraph GINIndex["GIN 索引"]
        direction TB
        E1["hello → [1, 2]"]
        E2["world → [1, 3]"]
        E3["postgres → [2]"]
        E4["database → [3]"]
    end

    D1 --> E1
    D1 --> E2
    D2 --> E1
    D2 --> E3
    D3 --> E2
    D3 --> E4

6.4 BRIN 索引

BRIN(Block Range INdex)存儲資料塊範圍的摘要資訊:

flowchart TB
    subgraph Table["表資料(按時間順序插入)"]
        B1["Block 1-128<br/>date: 2024-01-01 ~ 2024-01-15"]
        B2["Block 129-256<br/>date: 2024-01-16 ~ 2024-01-31"]
        B3["Block 257-384<br/>date: 2024-02-01 ~ 2024-02-15"]
    end

    subgraph BRIN["BRIN 索引(極小)"]
        I1["Block 1-128: min=01-01, max=01-15"]
        I2["Block 129-256: min=01-16, max=01-31"]
        I3["Block 257-384: min=02-01, max=02-15"]
    end

    B1 -.-> I1
    B2 -.-> I2
    B3 -.-> I3

BRIN 索引非常小,適合:

  • 自然有序的資料(時間戳、自增 ID)
  • 超大表(TB 級)
  • 可接受近似匹配

7. VACUUM 與資料清理

7.1 為什麼需要 VACUUM

由於 MVCC 產生多版本資料,舊版本需要清理:

flowchart LR
    subgraph Before["VACUUM 前"]
        T1["Tuple v1<br/>(已刪除)"]
        T2["Tuple v2<br/>(已更新)"]
        T3["Tuple v3<br/>(當前版本)"]
    end

    VACUUM["VACUUM"]

    subgraph After["VACUUM 後"]
        T4["Tuple v3<br/>(當前版本)"]
        FS["Free Space"]
    end

    Before --> VACUUM --> After

7.2 VACUUM 類型

類型操作
VACUUM標記死 tuple 的空間可重用不阻塞讀寫
VACUUM FULL重寫整個表獨占鎖
VACUUM FREEZE凍結舊事務 ID不阻塞讀寫

7.3 Autovacuum

Autovacuum 後台進程自動執行清理:

// 觸發條件
autovacuum_vacuum_threshold = 50        // 最小變更行數
autovacuum_vacuum_scale_factor = 0.2    // 表大小的比例

// 觸發公式
dead_tuples > threshold + scale_factor * table_size

8. WAL 與恢復

8.1 Write-Ahead Logging

WAL (Write-Ahead Logging) 保證持久性:

sequenceDiagram
    participant Client
    participant Backend
    participant WALBuffer
    participant WALFile
    participant DataFile

    Client->>Backend: INSERT
    Backend->>WALBuffer: 寫 WAL 記錄
    Backend->>Backend: 修改 Buffer Pool
    Backend->>Client: 成功

    Note over WALBuffer,WALFile: COMMIT 時
    WALBuffer->>WALFile: fsync WAL

    Note over Backend,DataFile: 之後某時刻
    Backend->>DataFile: 寫髒頁

核心原則:先寫 WAL,再寫資料頁

8.2 WAL 段文件

pg_wal/
├── 000000010000000000000001
├── 000000010000000000000002
├── 000000010000000000000003
...

每個 WAL 段默認 16MB,檔名編碼:

  • 時間線 ID(8 位)
  • LogId(8 位)
  • LogSeg(8 位)

8.3 Checkpoint

Checkpoint 是恢復的安全點:

flowchart LR
    subgraph Timeline["時間線"]
        C1["Checkpoint 1"]
        W1["WAL 1"]
        W2["WAL 2"]
        C2["Checkpoint 2"]
        W3["WAL 3"]
        CRASH["崩潰"]
    end

    C1 --> W1 --> W2 --> C2 --> W3 --> CRASH

    RECOVERY["恢復:從 Checkpoint 2 開始重放 WAL 3"]
    C2 -.-> RECOVERY

9. 總結

PostgreSQL 的內部架構體現了優秀的軟體工程實踐:

核心設計決策

設計選擇優勢
並發控制MVCC + SI/SSI高並發,讀寫不阻塞
存儲Heap + TOAST簡單高效,大對象友好
進程模型多進程穩定性高,進程隔離
優化器基於成本的動態規劃查詢計劃品質高
索引多種類型適應不同場景

學習資源


參考資料