結論先講

「怎麼寫」比「用什麼 DB 寫」影響更大。 批次 INSERT 比逐筆快 26 倍——這個差距比 PG vs MySQL(2.5 倍)大 10 倍。正確建 Index 比什麼都不建快 3-5 倍,但過度 Index 讓寫入慢 30%。先優化寫入方式,再考慮換 DB。


Bulk Insert:26 倍差距的來源

為什麼逐筆 INSERT 這麼慢

每一筆 INSERT 都是一個完整的 roundtrip:

  1. Application 送 SQL 到 DB
  2. DB 解析 SQL
  3. DB 執行寫入
  4. DB 回傳結果
  5. Application 收到回應

100 筆 = 100 個 roundtrip。即使每個 roundtrip 只有 1ms,100 個也要 100ms。

批次 INSERT 只需要 1 個 roundtrip

INSERT INTO posts (title, body) VALUES
  ('Post 1', 'Content 1'),
  ('Post 2', 'Content 2'),
  -- ...
  ('Post 100', 'Content 100');

1 個 roundtrip,DB 一次寫入 100 筆。省掉了 99 個 roundtrip + 99 次 SQL parse + 99 次 transaction commit。

ORM 的 bulk 方法

ORM方法效率
SequelizeModel.bulkCreate([...])好(單一 INSERT)
TypeORMrepo.insert([...])
Django ORMModel.objects.bulk_create([...])
EloquentModel::insert([...])
GORMdb.Create(&posts) 傳 slice

大部分 ORM 都有 bulk 方法,但很多開發者不知道或懶得用——在 for loop 裡一筆一筆 Model.create() 的程式碼太常見了。

什麼時候該用 Bulk

  • 批次匯入: CSV 匯入、資料遷移 → 一定要用
  • 批次建立: API 一次收到多筆資料 → 用
  • 單筆建立: 用戶提交一個表單 → 不需要
  • 超大批次: 超過 10,000 筆 → 要分批(每批 1,000),避免 DB 記憶體爆掉

Index 策略:不是越多越好

三種 Index 狀態的壓測結果(100K rows)

狀態讀取效能寫入效能說明
無 Index慢(全表掃描)不建議
正確 Index略慢推薦
過度 Index快(和正確 Index 差不多)慢 30%不建議

為什麼過度 Index 讓寫入變慢

每個 Index 都是一棵 B-tree。每次 INSERT/UPDATE/DELETE 都要同步更新所有 Index。

無 Index:    INSERT → 寫 1 次(heap)
3 個 Index:  INSERT → 寫 4 次(heap + 3 個 B-tree)
10 個 Index: INSERT → 寫 11 次(heap + 10 個 B-tree)

而且 Index 更新不是簡單的 append——B-tree 可能需要 split,這會觸發額外的磁碟 I/O。

怎麼判斷 Index 建對了

-- PostgreSQL: 找出沒在用的 Index
SELECT indexrelid::regclass AS index_name,
       idx_scan AS times_used
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

idx_scan = 0 的 Index 就是浪費——佔空間、拖慢寫入,但從來沒被查詢用到。

Index 建議

  1. WHERE 子句的欄位: 一定要建
  2. JOIN 的外鍵: 一定要建
  3. ORDER BY 的欄位: 看查詢頻率決定
  4. 很少被查詢的欄位: 不建
  5. 布林值欄位: 通常不值得建(只有 true/false 兩個值,selectivity 太低)

Transaction 鎖競爭

場景:50 個 hot rows 的搶奪

模擬搶購場景——50 個商品,高併發下每個 request 都要 SELECT FOR UPDATE 鎖定一行再更新。

DBMax RPS備註
PG高 40%讀不阻塞寫
MySQL基準gap lock 更積極

PG 為什麼更好

PG 的 MVCC:讀取拿舊版本,不需要等寫入的鎖釋放。只有寫-寫之間才會互相等鎖。

MySQL 的 InnoDB:SELECT ... FOR UPDATE 會拿 exclusive lock,而且在某些隔離等級下會加 gap lock(鎖住一個範圍,不只是那一行),導致更多的排隊。

減少鎖競爭的方法

  1. 縮小鎖的粒度: 只鎖需要的 row,不要鎖整張表
  2. 縮短持鎖時間: Transaction 裡不要做 bcrypt 或外部 API call
  3. 樂觀鎖替代悲觀鎖: 用 version number 做 optimistic locking
  4. Redis 做前置過濾: 搶購場景先在 Redis 扣庫存,成功再寫 DB

所有數字放一起看

優化項目效能提升難度
逐筆 → Bulk INSERT26 倍低(改一行程式碼)
無 Index → 正確 Index3-5 倍低(加 migration)
MySQL → PG(JSON 場景)3.2 倍高(換 DB)
PG → MySQL(寫入場景)2.5 倍高(換 DB)
過度 Index → 正確 Index+30% 寫入低(刪 migration)
ORM → Raw SQL+10%中(手寫 SQL)

ROI 最高的是 Bulk INSERT 和正確 Index——改動最小、效果最大。 換 DB 的效果也不小,但成本高很多。

這和 bcrypt 篇 的結論一致:先優化使用方式,再考慮換技術棧。


下一篇

連線池與 MongoDB:連接的學問 — pool=5 vs pool=50 高壓下差 70%。MongoDB 純 CRUD 快 8.6 倍但不適合 Transaction 密集場景。連線池大小要跟著水平擴展一起算。


本系列文章

完整 68 篇目錄見 系列首頁

← 上一篇:PostgreSQL vs MySQL:不是誰比較好,是場景不同 → 下一篇:連線池與 MongoDB:連接的學問