首次發布:2026-06-17;更新:2026-09-15。本次修正鎖的說明,並新增 PostgreSQL 18.6 本機實驗。

《PostgreSQL 指南:內幕探索》書封

有一陣子工作常寫 SELECT ... FOR UPDATE。寫著寫著,我發現自己講不清楚它到底鎖了什麼。別的 transaction 還能讀那筆資料嗎?能改嗎?鎖到什麼時候?

我就是為了這個問題去讀 The Internals of PostgreSQL。每天都在用的東西,總得知道它在做什麼。

第一個幫我想通的是 row version。PostgreSQL 的 MVCC 更新會建立新的資料版本,舊版本可能還被其他 transaction 的 snapshot 看見。我原本把這件事寫成「UPDATE 根本不會更新任何東西」,講太滿了。資料確實更新了,只是儲存方式跟我原本想的不一樣。

一般查詢會看到哪個版本,要看 snapshot。Read Committed 每個 statement 會拿新的 snapshot;Repeatable Read 則從 transaction 裡第一個非交易控制的 statement 建立 snapshot,後續一般查詢繼續用它。所以別人更新並 commit 之後,自己再查一次,還是可能看到舊值。

但 FOR UPDATE 還多了鎖的行為。它會鎖住查到的 row,擋住衝突的操作,也會取得表級的 ROW SHARE 鎖。這不代表整張表每筆資料都被鎖住,一般讀取和更新其他 row 仍然可以進行。所以原本寫「鎖的不是表」,也不夠準確。

這次整理文章,我用 PostgreSQL 18.6 在本機跑了一個小例子。這是本機驗證,資料都是測試用的,不是正式環境事故。兩個 session 要連到同一個資料庫。

先建立一次測試資料:

CREATE TABLE lock_demo (
    id integer PRIMARY KEY,
    status text NOT NULL
);
INSERT INTO lock_demo VALUES (1, 'pending');

Session A 先鎖住這筆,transaction 留著不要結束:

BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT * FROM lock_demo WHERE id = 1 FOR UPDATE;

Session B 使用 autocommit,先讀,再試著更新:

SELECT * FROM lock_demo WHERE id = 1;

SET lock_timeout = '1s';
UPDATE lock_demo SET status = 'done' WHERE id = 1;

一般 SELECT 可以讀到已提交的資料。但 UPDATE 跟 A 的 row lock 衝突,得等 A 放掉鎖。這裡設一秒的 timeout,就是讓等待以 lock timeout 錯誤呈現,不用一直掛著。

把 A commit,再到 B 重跑更新:

-- Session A
COMMIT;

-- Session B
UPDATE lock_demo SET status = 'done' WHERE id = 1;
SELECT * FROM lock_demo WHERE id = 1;

A 的 transaction 結束後,B 就能更新了。我想看清楚的就是這個差別:一般讀取可以看到它有權看見的版本,另一個寫入卻要等。如果 B 也用 SELECT ... FOR UPDATE 查同一筆,一樣會衝突。

這個例子刻意用 Read Committed,而且 A 只有鎖,沒有修改資料,不能拿來解釋所有併發情況。Repeatable Read 下,如果要鎖的 row 在 snapshot 之後被另一個 transaction 改過,可能會收到 serialization error。官方鎖文件有說明這個差別和各種 row lock 的衝突關係。

回到程式裡,我還會看 transaction 到哪裡才結束。通常不是 SELECT 回傳就放鎖,而是等 transaction commit 或 rollback。如果中間還在等外部 API,另一個想更新的人就可能跟著等。Rollback 到 savepoint 可以釋放那之後取得的鎖,所以還是要看實際的交易範圍。

MVCC 也讓我分清楚另一種問題。舊版本會占空間,保留舊 snapshot 的 transaction 可能讓部分版本暫時不能回收,但不能說只要有 transaction 開著,vacuum 就什麼都不能清。一般 VACUUM 主要讓空間可以在表內重用,也不代表檔案會直接縮小。這些差別在官方 vacuum 文件裡有寫。

現在 review 資料庫程式,我會問得比較具體:會鎖哪些 row、誰會被擋住、transaction 在哪裡結束?如果 AI 建議加 FOR UPDATE,我會要它說清楚要防哪個競爭情況,再把兩個 transaction 的操作跑給我看。SQL 能執行,只回答了一部分問題。