PostgreSQL SERIAL 序列不隨 OVERRIDING SYSTEM VALUE 推進
PostgreSQL SERIAL 序列不隨 OVERRIDING SYSTEM VALUE 推進
測試 seed 用 OVERRIDING SYSTEM VALUE 手動指定 ID,插入成功,但之後的一般 INSERT 卻拿到 nextval=1,造成 duplicate key 錯誤。問題只在「全量執行」時出現,單獨執行單一模組時正常。
📖 背景:SERIAL 和序列(Sequence)是什麼?
SERIAL 是「自動產生遞增 ID」的語法糖,背後 PostgreSQL 會建一個獨立的計數器,叫做序列(Sequence):
1 | CREATE TABLE products ( |
每次 INSERT 不指定 id,就呼叫 nextval('products_id_seq'),計數器 +1,回傳新 ID:
1 | 第 1 次 INSERT → nextval() → 計數器: 0→1 → id=1 |
[!important] 關鍵:序列是獨立計數器,不追蹤表裡的最大值
序列只管自己數到幾,不會去看表裡現在有哪些 ID。這是造成本問題的根源。
🔍 復現場景
1 | -- Step 1:清空表,序列計數器歸零 |
🚨 根本原因:OVERRIDING SYSTEM VALUE 繞過計數器
OVERRIDING SYSTEM VALUE 的設計就是略過序列,直接把你指定的值寫進去,完全不碰計數器。
類比:就像一個叫號機,你直接喊「89 號請到 3 號窗口」,但叫號機還停在 1。下個客人按按鈕,拿到的還是 1 號,但 89 號已經有人在那裡了。
1 | TRUNCATE RESTART IDENTITY → 計數器歸零 = 0 |
✅ 解法:用 setval 手動推進計數器
seed 塞完資料後,把計數器手動對齊到表裡最大的 ID:
1 | -- 找出目前最大 id,把計數器設到那裡 |
setval('products_id_seq', 5) 的意思是:把這個計數器強制設定為 5,下次 nextval() 就會從 6 開始。
[!tip] 通用版本(自動處理所有資料表)
可以封裝成一段 PL/pgSQL,放在 seed script 最末尾執行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT schemaname, tablename, attname
FROM pg_stats
WHERE tablename IN ('products','customers','orders','live_sessions')
AND attname = 'id'
LOOP
EXECUTE format(
'SELECT setval(pg_get_serial_sequence(''%I.%I'', ''id''), COALESCE(MAX(id), 0) + 1, false) FROM %I.%I',
r.schemaname, r.tablename, r.schemaname, r.tablename
);
END LOOP;
END $$;⚠️
pg_stats是統計視圖,需要ANALYZE才有資料。在剛 TRUNCATE+INSERT 的測試環境可能為空,導致這段什麼都不執行。建議優先用上方的簡易寫法(直接列舉資料表呼叫setval),或改查pg_sequences系統目錄。
⚠️ 踩坑:為什麼只在全量執行時才爆?
測試用 filepath.Walk 按字母順序跑:
1 | tests/stories/ |
全量執行的時序:
livestream/seed 用 OVERRIDING SYSTEM VALUE 插入orders id=1,計數器沒動order/測試建立訂單,nextval()回傳 1 → 撞到livestream留下的 id=1 → 爆炸
單獨跑 order/ 時:
- TRUNCATE 重置計數器,沒有其他模組污染,正常執行,掩蓋了這個 bug
這種「單跑 OK、全跑壞」的 bug 特別難找,因為很難想到是執行順序造成的。
排查方式:加 -v 觀察執行順序,或臨時只跑 livestream/ + order/ 組合來重現。
📝 重點整理
| 觀念 | 記住這一句 |
|---|---|
| SERIAL | 背後是獨立計數器,不是「自動看表裡最大值」 |
| OVERRIDING SYSTEM VALUE | 繞過計數器直接寫值,計數器不會跟著動 |
| TRUNCATE RESTART IDENTITY | 計數器歸零,但不追蹤你之後手動塞的 ID |
| setval | 手動把計數器推進到正確位置的方法 |
| 這類 bug 的特徵 | 只在特定執行順序下出現,單一模組測試看不出來 |



