開啟中的 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 中,將最後的交易命令納入一般成功與錯誤處理流程,避免讀取已完成後,下一次資料表刪除仍然必須等待。

資料來源