PostgreSQL 關聯式資料庫 (PostgreSQL)

一句話定位

從 PostgreSQL 出發,再說明為何需要偏離它,比從小眾方案出發再證明它比 PG 好更有說服力。
面試官不是找 DBA —— 不要鑽 MVCC/WAL 內部,也別丟「NoSQL 擴展性比 PG 好」這種籠統論斷。聚焦:讀寫效能、一致性、複製四個取捨。

四大核心能力

能力 關鍵
讀取效能 B-tree + 進階索引(GIN/PostGIS)+ 涵蓋/部分索引
寫入效能 buffer + WAL + 背景寫;批次 / 分區 / sharding 擴展
一致性 ACID 交易 + 隔離等級 + Row-Level Locking
複製 同步/非同步 replica → 擴展讀取 + 高可用

讀取效能

讀取通常遠多於寫入。面試聚焦「不同索引何時說得通」,不需深入 query planner。

基礎索引(B-tree,預設)

適合:精確匹配WHERE email = ...)、範圍查詢created_at > ...)、排序ORDER BY,欄位順序需符合索引)。

CREATE INDEX idx_users_email ON users(email);
-- 複合索引:針對常見查詢模式
CREATE INDEX idx_posts_user_date ON posts(user_id, created_at);
不要對每個欄位都加索引

每個索引會:① 拖慢寫入(索引要跟著更新)② 佔磁碟空間 ③ 不一定被用到(planner 可能判定全表掃描更快)。只對高頻查詢 + 選擇性高的欄位建索引。

進階索引:常常省掉一個專用資料庫

索引 用途 取代了什麼
GIN 全文搜尋(tsvector) 簡單搜尋不必上 Elasticsearch
JSONB + GIN 靈活 metadata、半結構化 給你類 NoSQL 的彈性,不必上 MongoDB
PostGIS(GIST) 地理空間查詢 不必上專用地理 DB(Uber 早期就用它)

查詢優化進階技巧

效能基準(粗估)

操作 量級
簡單索引查找 每核心每秒數萬
複雜 JOIN 每核心每秒數千
開始吃力的規模 單表 > 1 千萬~1 億筆

工作集(頻繁存取資料)超過 RAM 後效能急劇下降。盡量讓工作集留在記憶體裡


寫入效能

寫入路徑

1. 修改記憶體 buffer(標記為髒頁)+ 產生 WAL 記錄  ← 都在記憶體
2. commit 時:WAL 循序寫進磁碟(fsync)→ 交易即持久化(崩潰可重播 WAL 恢復)
3. 背景寫入程序:髒頁非同步刷進資料檔(可批次提升效能)
4. 索引更新:每個索引都要更新(→ 太多索引大幅拖慢寫入)

大部分工作在記憶體 + WAL 確保持久性 → 寫入可以很快。瓶頸通常卡在 WAL 的磁碟 I/O

吞吐量(單節點、中等硬體)

操作 每核心每秒
簡單 INSERT ~5,000
有索引更新的 UPDATE ~1,000–2,000
複雜多表交易 幾百
批次操作 幾萬

需要 > 5,000 寫入/秒不代表 PG 不行,只代表要把資料分片到多節點

寫入優化策略(由簡到繁)

  1. 垂直擴展:NVMe(改善 WAL)、更多 RAM、更多核心 —— 合理起點
  2. 批次處理:多筆寫入合成一個交易(風險:中途崩潰整批遺失)
  3. 寫入卸載(Write Offloading):分析/日誌/聚合指標 → 丟 Kafka → 背景 worker 批次寫
  4. 表格分區(Partitioning):常依時間 PARTITION BY RANGE (created_at);並行寫不同分區、索引更新只在相關分區、讀只掃最近分區。可把舊分區移到便宜儲存
  5. 分片(Sharding):按查詢最頻繁的欄位(如 user_id,讓同用戶資料同 shard,避免 scatter-gather)。PG 無內建 sharding,要自己做或用 Citus
只在較簡單的優化不夠用時才 sharding。詳見 05-Database-Advanced/02-Sharding

資料一致性

別只說「我們用 PG 因為它支援 ACID」

要說清楚具體怎麼用 ACID 解決一致性需求。

交易

一組必須一起成功或一起失敗的操作(轉帳:扣款 + 入帳,不能只做一半)。

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

並發競態(拍賣出價)

預設隔離等級 Read Committed 下,即使包在交易裡,兩個用戶同時出價仍可能不一致:

1. A 讀到最高價 90
2. B 讀到最高價 90
3. A 出 100,commit
4. B 出 95,commit  ← 不合法!95 被接受了,但已有 100

兩個解法:

解法 機制 適用
Row-Level Locking SELECT ... FOR UPDATE 鎖住該行 清楚知道要鎖哪幾行(首選)
更高隔離等級 SET TRANSACTION ISOLATION SERIALIZABLE 複雜到難以預知要鎖什麼
面試別只說「我們用交易」

更準確:「我們用交易加上對拍賣記錄的列層級加鎖(FOR UPDATE」。

比較 Serializable Row-Level Locking
並行度 較低(衝突需重試) 較高(碰同一行才衝突)
效能開銷 較高(追蹤所有讀寫依賴) 較低(只追蹤特定鎖)
錯誤處理 處理序列化失敗(重試) 處理死鎖
典型例子 跨多表複雜財務計算 拍賣出價、庫存更新

隔離等級

等級 Dirty Non-repeat Phantom 備註
Read Uncommitted (PG 等同 RC) 可能 可能 PG 實際不允許 dirty read
Read Committed(預設) 可能 可能 效能好
Repeatable Read ❌(PG 特有 PG 此級比 SQL 標準更強
Serializable 防所有異常,需重試邏輯
PG 的 Repeatable Read 比標準強

它在交易開始建一致性快照,同時防 non-repeatable read 與 phantom read。很多在別的 DB 需要 Serializable 的情況,PG 用 Repeatable Read 就夠。對照 05-Database-Advanced/01-Transactions


複製(Replication)

幾乎所有真實部署都用複製,兩個原因:擴展讀取 + 高可用

模式 行為 取捨
同步 primary 等 replica 確認才回成功 強一致,延遲高
非同步 primary 立刻回成功,背景複製 效能好,replica 可能短暫落後

很多組織混用:少數同步 replica 保強一致 + 額外非同步 replica 擴展讀取。

擴展讀取 + Replication Lag

讀取分散到多 replica,寫入走 primary(大多應用讀多寫少)→ 讀取吞吐 ×N。

Read-Your-Writes 問題

用戶剛寫完立刻讀,可能打到還沒跟上的 replica → 看不到自己的變更。解法:剛寫入的請求短暫路由到 primary,或在應用層處理。對照 05-Database-Advanced/03-Replication

高可用 + Failover

primary 掛掉 → 偵測下線 → 提升一個 replica 為 primary → 更新連線資訊 → 應用指向新 primary。多數團隊用托管服務(RDS、Cloud SQL)自動處理。面試知道「failover 可行 + 大致流程」就夠。


什麼時候選 PostgreSQL(與不選)

選 PG:複雜關係、強一致需求、豐富查詢、結構化+非結構化混合(JSONB)、內建全文搜尋、地理空間查詢。
完美場景:電商(庫存/訂單)、金融(交易/稽核)、CMS、合理規模的分析平台。

考慮替代方案

情況 問題 替代
極端寫入吞吐 每筆寫入要 WAL + 索引更新 → I/O 瓶頸 Cassandra(事件流)、Redis(計數器)
全球多區域多寫 單主架構,只能一個 Region 主寫 CockroachDB、Cassandra、DynamoDB Global Tables
純 Key-Value 存取 MVCC/WAL/planner 是殺雞用牛刀 Redis、DynamoDB
單純「需要擴展」不是棄用 PG 的好理由

設計得當的 PostgreSQL 能處理相當大的規模。


自我測驗重點

問題 重點
預設索引 B-tree(精確/範圍/排序)
GIN / JSONB / PostGIS 各解什麼 全文搜尋 / 半結構化 / 地理空間
寫入為何快 大部分在記憶體 + WAL;瓶頸在 WAL I/O
拍賣競態怎麼解 SELECT FOR UPDATE(首選)或 Serializable
PG Repeatable Read 特別在哪 同時防 phantom read(比 SQL 標準強)
Read-Your-Writes 怎麼解 剛寫入請求路由到 primary
何時棄用 PG 極端寫入 / 全球多寫 / 純 KV