結論先講
「怎麼寫」比「用什麼 DB 寫」影響更大。 批次 INSERT 比逐筆快 26 倍——這個差距比 PG vs MySQL(2.5 倍)大 10 倍。正確建 Index 比什麼都不建快 3-5 倍,但過度 Index 讓寫入慢 30%。先優化寫入方式,再考慮換 DB。
Bulk Insert:26 倍差距的來源
為什麼逐筆 INSERT 這麼慢
每一筆 INSERT 都是一個完整的 roundtrip:
- Application 送 SQL 到 DB
- DB 解析 SQL
- DB 執行寫入
- DB 回傳結果
- 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 | 方法 | 效率 |
|---|---|---|
| Sequelize | Model.bulkCreate([...]) | 好(單一 INSERT) |
| TypeORM | repo.insert([...]) | 好 |
| Django ORM | Model.objects.bulk_create([...]) | 好 |
| Eloquent | Model::insert([...]) | 好 |
| GORM | db.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 建議
- WHERE 子句的欄位: 一定要建
- JOIN 的外鍵: 一定要建
- ORDER BY 的欄位: 看查詢頻率決定
- 很少被查詢的欄位: 不建
- 布林值欄位: 通常不值得建(只有 true/false 兩個值,selectivity 太低)
Transaction 鎖競爭
場景:50 個 hot rows 的搶奪
模擬搶購場景——50 個商品,高併發下每個 request 都要 SELECT FOR UPDATE 鎖定一行再更新。
| DB | Max RPS | 備註 |
|---|---|---|
| PG | 高 40% | 讀不阻塞寫 |
| MySQL | 基準 | gap lock 更積極 |
PG 為什麼更好
PG 的 MVCC:讀取拿舊版本,不需要等寫入的鎖釋放。只有寫-寫之間才會互相等鎖。
MySQL 的 InnoDB:SELECT ... FOR UPDATE 會拿 exclusive lock,而且在某些隔離等級下會加 gap lock(鎖住一個範圍,不只是那一行),導致更多的排隊。
減少鎖競爭的方法
- 縮小鎖的粒度: 只鎖需要的 row,不要鎖整張表
- 縮短持鎖時間: Transaction 裡不要做 bcrypt 或外部 API call
- 樂觀鎖替代悲觀鎖: 用 version number 做 optimistic locking
- Redis 做前置過濾: 搶購場景先在 Redis 扣庫存,成功再寫 DB
所有數字放一起看
| 優化項目 | 效能提升 | 難度 |
|---|---|---|
| 逐筆 → Bulk INSERT | 26 倍 | 低(改一行程式碼) |
| 無 Index → 正確 Index | 3-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:連接的學問