開啟中的 PostgreSQL 唯讀交易可能會阻擋 DROP TABLE,因為其中的 SELECT 會持有 ACCESS SHARE 資料表鎖,直到交易結束。刪除操作需要與之衝突的 ACCESS EXCLUSIVE 鎖,因此會等待,即使查詢已經返回也一樣。PostgreSQL:明確鎖定
本指南說明如何在 Mac 上重現這種等待、找出阻擋中的工作階段,並在 node-postgres 中正確結束交易。範例只使用合成的文章資料列。SQL 以 PostgreSQL 18 為目標版本;本文取得的官方最新版文件適用於此版本。套用特定版本的設定前,請先查閱與你的伺服器版本相符的手冊。
PostgreSQL 的鎖定中,唯讀代表什麼?
READ ONLY 會限制交易可以執行哪些命令。例如,它會禁止修改一般資料表,以及執行 DROP TABLE 等命令。但讀取仍然會取得資料表鎖。在這個情境中,讀取的工作階段與嘗試刪除資料表的工作階段屬於不同的交易。PostgreSQL:SET TRANSACTION
一般的 SELECT 會自動對參照到的資料表取得 ACCESS SHARE 鎖。DROP TABLE 需要 ACCESS EXCLUSIVE 鎖,兩者互相衝突。鎖通常會持續到交易結束;有一個文件記載的例外情況是,回復至取得鎖之前建立的儲存點。以下範例不使用儲存點。PostgreSQL:明確鎖定
關鍵在於區分查詢完成與交易完成。在終端機中看到文章資料列,表示查詢已返回。請確認外層交易是否也已結束。在 psql 中,自動提交通常預設為啟用,但明確執行 BEGIN 會讓交易保持開啟,直到你將它結束為止。PostgreSQL:psql
如何在 Mac 上重現遭阻擋的刪除操作?
使用現有的本機 PostgreSQL 18 測試資料庫,並確認 Mac 的 Terminal 中已可使用 psql。開啟三個連到同一資料庫的終端機分頁,分別稱為 A、B 和 C。這些必須是不同的連線;單靠不同的編輯器窗格並不能建立不同連線。
以下連線命令假設你的本機測試資料庫名為 postgres。如有需要,請代換成你現有的測試資料庫名稱。每個分頁都執行一次。-X 選項可避免載入個人 psql 啟動檔。PostgreSQL:psql
psql -X -d postgres
在每個連線中明確啟用自動提交,並檢查伺服器版本。以下區塊依序構成操作步驟:後續區塊會依賴前面建立的設定與工作階段狀態。
\set AUTOCOMMIT on
SHOW server_version;
建立合成的文章資料表
在分頁 C 中執行一次以下設定。請使用尚未使用的資料表名稱:如果 public.article_lock_demo 已存在,請停止並改選其他名稱,且在所有範例中一致使用。此設定刻意不會移除現有資料表。
CREATE TABLE public.article_lock_demo (
id integer PRIMARY KEY,
title text NOT NULL
);
INSERT INTO public.article_lock_demo (id, title)
VALUES
(1, 'Choosing a notebook'),
(2, 'Organizing a reading list');
讓讀取者交易保持開啟
在分頁 A 中執行此區塊,並讓連線停留在提示字元處。先不要提交。
SET application_name = 'article_reader_demo';
BEGIN READ ONLY;
SHOW transaction_read_only;
SELECT id, title
FROM public.article_lock_demo
ORDER BY id;
預期 transaction_read_only 會顯示 on,接著列出兩筆合成資料列。讀取已完成,但明確開啟的交易仍然保持開啟。
從另一個連線嘗試刪除
在分頁 B 中執行以下區塊。將 DROP TABLE 作為最後一個陳述式提交,不要在後面附加其他命令。
SET application_name = 'article_drop_demo';
BEGIN;
SET LOCAL lock_timeout = '60s';
DROP TABLE public.article_lock_demo;
只要 A 仍保持開啟,刪除操作就應該等待。範例中的 60 秒上限是為了讓你有時間檢查等待狀態,並非普遍適用的建議設定。lock_timeout 會中止等待取得鎖超時的陳述式,且每次鎖定取得嘗試都會分別套用此設定。PostgreSQL:用戶端連線預設值
DROP TABLE 返回後,仍要讓 B 的明確交易保持開啟。稍後你會將它回復,確保示範資料表仍然存在,即使刪除成功也一樣。提交交易後,DROP TABLE 會移除資料表及其相關索引和條件約束。PostgreSQL:DROP TABLE
如何確認是哪個工作階段阻擋了刪除?
B 等待時,在 C 中執行下一個查詢。它會找出兩個示範連線,並查詢 PostgreSQL 哪些工作階段正在阻擋各連線的鎖定請求。若程序遭到阻擋,pg_blocking_pids() 會返回一組阻擋程序 ID;否則會返回空陣列。PostgreSQL:系統資訊函式與運算子
SELECT
pid,
application_name,
state,
xact_start,
wait_event_type,
wait_event,
pg_blocking_pids(pid) AS blocking_pids,
query
FROM pg_stat_activity
WHERE datname = current_database()
AND application_name IN (
'article_reader_demo',
'article_drop_demo'
)
ORDER BY application_name;
預期 A 會顯示 idle in transaction:它有一筆開啟中的交易,但目前沒有執行查詢。等待中的 B 可能會顯示 active,且等待事件類型為 Lock。PostgreSQL 說明,執行狀態與等待事件彼此獨立,因此單憑 active 無法確認命令正在取得進展。PostgreSQL:累積統計系統
接著,檢查實際的資料表鎖定請求。在結束 A 的交易之前,先在 C 中執行以下查詢。
SELECT
a.application_name,
l.pid,
l.mode,
l.granted
FROM pg_locks AS l
JOIN pg_stat_activity AS a ON a.pid = l.pid
WHERE l.locktype = 'relation'
AND l.database = (
SELECT oid
FROM pg_database
WHERE datname = current_database()
)
AND l.relation = 'public.article_lock_demo'::regclass
ORDER BY a.application_name, l.mode;
以下預期的模式僅供說明,並非實際執行結果。在 pg_locks 中,granted = true 表示程序持有該鎖;false 表示程序正在等待取得該鎖。PostgreSQL:pg_locks
| 工作階段 | 鎖定模式 | granted |
說明 |
|---|---|---|---|
article_reader_demo |
AccessShareLock |
true |
讀取者持有資料表鎖 |
article_drop_demo |
AccessExclusiveLock |
false |
刪除操作正在等待取得資料表鎖 |
將這兩個查詢合併作為診斷流程:先確認阻擋關係,再檢查相關資料表已持有和正在請求的鎖。工作階段處於閒置狀態本身不足以作為證據;阻擋關係與鎖定資料列可將它連結到這次刪除操作。
如果 B 已逾時,它等待中的鎖定請求就不會再出現。在 B 中執行 ROLLBACK;,再重複 B 的交易區塊,並在 A 仍保持開啟時儘快檢查。
如何釋放讀取者並完成示範?
在 A 中結束唯讀交易:
COMMIT;
如果 B 仍在等待,而且 A 是唯一的阻擋者,現在刪除操作就可以完成。在 B 中立即回復此次示範用的刪除操作,讓合成資料表繼續保留:
ROLLBACK;
如果 B 在你提交 A 之前就已逾時,請先在 B 中執行 ROLLBACK;。A 結束後,再重複執行 B 的區塊,然後在刪除成功後再次執行 ROLLBACK;。如此無論出現哪種結果,都不會讓 B 的交易保持未完成狀態。
以下判斷輔助表可協助區分交易生命週期問題與其他原因。工作狀態標籤依據 PostgreSQL 的文件說明。PostgreSQL:累積統計系統
| 觀察結果 | 下一步 |
|---|---|
A 為 idle in transaction,且 B 的阻擋者陣列包含 A 的 PID |
在 A 的連線中結束 A 的交易 |
| A 已結束,但 B 仍回報阻擋 PID | 檢查其餘阻擋者 |
| B 回報鎖定逾時 | 重試前先回復 B 的明確交易 |
| B 回報相依性錯誤 | 檢查相依物件,不要將它當成鎖定等待 |
| B 立即返回 | 確認預期中的讀取者交易是否仍然開啟 |
IF EXISTS 可處理資料表不存在的情況,而 CASCADE 則控制相依物件的移除方式。兩者都不是設定鎖定等待的選項。此練習請保留預設的相依物件處理方式;官方的 DROP TABLE 語法沒有 CONCURRENTLY 變體。PostgreSQL:DROP TABLE
node-postgres 應如何結束唯讀交易?
使用 node-postgres 時,BEGIN、交易中的每個陳述式,以及最後的 COMMIT 或 ROLLBACK,都必須使用同一個已借出的用戶端。官方交易文件明確警告,不要在交易中使用 pool.query(),因為陳述式可能會被分派到不同的用戶端。node-postgres:交易
將完成交易與釋放用戶端視為應用程式中的兩個獨立步驟。client.release() 會將已借出的用戶端歸還連線池;請先完成交易再呼叫它。如果回復交易失敗,連線池 API 支援在 release() 中傳入真值,以銷毀該用戶端。node-postgres:連線池 API
以下完整程式會保留操作流程中建立的合成資料表。請使用支援 ES 模組與 AggregateError 的已安裝 Node.js 執行階段,並在工作專案目錄中安裝 pg:
npm install pg
將以下程式儲存為 read-articles.mjs。設定其連線,使用與 psql 相同的本機測試資料庫。node-postgres 支援 PostgreSQL 的連線環境變數,包括 PGDATABASE。node-postgres:連線
import pg from 'pg';
const { Pool } = pg;
const pool = new Pool({ connectionTimeoutMillis: 5000 });
async function main() {
try {
const client = await pool.connect();
let discard = false;
try {
await client.query('BEGIN READ ONLY');
const result = await client.query(
'SELECT id, title FROM public.article_lock_demo ORDER BY id'
);
await client.query('COMMIT');
console.table(result.rows);
} catch (error) {
try {
await client.query('ROLLBACK');
} catch (rollbackError) {
discard = true;
throw new AggregateError(
[error, rollbackError],
'Query and rollback failed'
);
}
throw error;
} finally {
client.release(discard);
}
} finally {
await pool.end();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
PGDATABASE=postgres node read-articles.mjs
程式會在顯示資料列之前提交交易,發生錯誤時則回復交易,並在兩種情況下都釋放用戶端。此 JavaScript 語法已使用 Node.js 檢查;由於研究環境中沒有 PostgreSQL,因此本文只依據文件檢視資料庫操作流程,並未實際執行。
對於單次且獨立的讀取,文件建議使用 pool.query() 這個便利方法,這樣就不必手動借出用戶端。只有在工作需要明確交易時才使用它,並讓無關的檔案處理或桌面互動在交易生命週期之外進行。node-postgres:連線池 API
哪種逾時設定適用於尚未結束的讀取者?
請依據等待的對象選用逾時設定。lock_timeout 會限制刪除操作等待取得鎖的時間。statement_timeout 會限制陳述式執行時間,因此應用程式在讀取成功後停止傳送命令,不會因此讓交易結束。idle_in_transaction_session_timeout 則會特別終止在開啟中的交易內閒置的工作階段。這些都是 PostgreSQL 18 文件所載的設定;請根據應用程式預期的工作時間選擇持續時間。PostgreSQL:用戶端連線預設值
對於這項工作,關鍵在於確認讀取者的交易是否已結束。確認阻擋 PID、檢查資料表鎖,並在擁有該交易的連線中將它結束。在 node-postgres 中,將最後的交易命令納入一般成功與錯誤處理流程,避免讀取已完成後,下一次資料表刪除仍然必須等待。
資料來源
- 13.3. 明確鎖定 — PostgreSQL 全球開發群組
- SET TRANSACTION — PostgreSQL 全球開發群組
- psql — PostgreSQL 全球開發群組
- 19.11. 用戶端連線預設值 — PostgreSQL 全球開發群組
- DROP TABLE — PostgreSQL 全球開發群組
- 9.27. 系統資訊函式與運算子 — PostgreSQL 全球開發群組
- 27.2. 累積統計系統 — PostgreSQL 全球開發群組
- 53.13. pg_locks — PostgreSQL 全球開發群組
- 交易 — node-postgres
- 連線池 API — node-postgres
- 連線 — node-postgres