PostgreSQL 的 NOT VALID 外鍵會略過對現有資料列的初始檢查,同時強制檢查新的參照。較早的不匹配資料列會保留,直到修復並完成驗證。PostgreSQL 17:ALTER TABLE
本指南適用於在 Mac 的 Terminal 中工作的開發者,協助你將外鍵加入已有資料的資料表,並了解結果。完整的 node-postgres 範例只使用合成的文章資料,並追蹤一筆不匹配的參照,從建立限制條件到修復及成功驗證。
現有資料列與新寫入會發生什麼事?
假設有兩個資料表:articles 包含文章 ID 和標題,article_links 則包含對這些 ID 的參照。若連結中的非空 article_id 沒有對應的文章,該連結就是不匹配的。使用 NOT VALID 加入外鍵,可讓較早的不匹配狀況繼續存在,同時對之後的寫入建立限制。PostgreSQL 17:ALTER TABLE
| 加入限制條件後的操作 | 此範例中的結果 |
|---|---|
| 不修改較早的不匹配連結 | 該連結仍會保留 |
| 插入參照現有文章的連結 | 成功 |
| 插入參照不存在文章的連結 | 失敗 |
| 將連結的文章 ID 改成另一個不存在的 ID | 失敗 |
| 不匹配連結仍存在時進行驗證 | 驗證失敗 |
| 修復所有不匹配的參照後再驗證 | 驗證可成功 |
這些結果源自文件所述的限制條件加入與驗證流程分離機制。PostgreSQL 17:ALTER TABLE
有一個重要例外:更新若未改變外鍵值,結果可能不同。在 PostgreSQL 18 中,若新舊鍵值比較相等,實作可以略過外鍵檢查。因此,只修改較早的不匹配連結的標籤,可能會成功,而不必修復其參照。實作也對目前交易所插入的資料列設有特殊處理,因此不應將此行為概括套用到所有更新順序。PostgreSQL 18 外鍵實作
實際上,標籤修改成功並不能證明清理工作已完成。請直接檢查不匹配的參照,並使用驗證確認整個資料表都符合限制條件。
需要哪個 PostgreSQL 版本和資料表結構?
以下範例使用一般的非分割資料表,適用於 PostgreSQL 17 和 18。分割資料表有一個重要的版本差異:PostgreSQL 17 明確禁止將分割資料表上的外鍵宣告為 NOT VALID;PostgreSQL 18 則新增了這項功能。請檢查伺服器版本,不要根據已安裝的 Node 套件推測是否支援。PostgreSQL 17:ALTER TABLE、PostgreSQL 18 版本說明
若 PostgreSQL 17 上的參照資料表是分割資料表,可直接採用的支援流程是先修正參照,再以一般方式加入外鍵,讓系統執行初始驗證掃描。若必須在分割資料表的父資料表上分階段驗證,PostgreSQL 18 提供了文件記載的功能。本教學採用簡單的資料表配置,避免分割階層細節模糊了資料列行為。
被參照的鍵也需要適當的唯一性保證。在此,articles.article_id 是主鍵。參照欄位刻意允許 NULL:根據預設的 MATCH SIMPLE 規則,空值參照不需要對應的文章。若每個連結都必須指向文章,請另外加入 NOT NULL。PostgreSQL 18:CREATE TABLE
在 Mac 上執行完整的合成資料範例
你需要一個正在執行的 PostgreSQL 資料庫、一份已設定完成且可用的本機連線設定,以及 node-postgres 支援的 Node.js 版本。在 Terminal 的專案目錄中,使用以下命令安裝套件,然後將完整程式儲存為 not-valid-demo.mjs。node-postgres 官方入門文件說明了套件安裝方式。node-postgres 文件
npm install pg
程式使用 new pg.Client(),它會讀取 PostgreSQL 連線環境變數,例如 PGHOST、PGPORT、PGDATABASE 和 PGUSER。請使用目前已能連線至本機資料庫的設定。node-postgres:連線
兩個資料表都是暫存資料表,只在目前連線的工作階段中存在。PostgreSQL 規定暫存資料表的外鍵只能參照暫存資料表,因此程式會同時建立關聯兩端的資料表。PostgreSQL 18:CREATE TABLE
每項操作都會使用獨立的已等待查詢。值會透過 $1 和 $2 等 node-postgres 參數傳入;資料表名稱和限制條件名稱則固定寫在 SQL 中。node-postgres:查詢
import pg from 'pg';
const client = new pg.Client({ connectionTimeoutMillis: 5000 });
const validate = `ALTER TABLE pg_temp.article_links
VALIDATE CONSTRAINT article_links_article_fk`;
async function expectForeignKeyError(label, sql, values = []) {
try {
await client.query(sql, values);
} catch (error) {
if (error.code !== '23503' ||
error.constraint !== 'article_links_article_fk') throw error;
console.log(label + ': 23503 (expected)');
return;
}
throw new Error(label + ': unexpectedly succeeded');
}
async function showStatus(label) {
const result = await client.query(`
SELECT conname, convalidated
FROM pg_constraint
WHERE conrelid = 'pg_temp.article_links'::regclass
AND conname = 'article_links_article_fk'
AND contype = 'f'
`);
console.log(label, result.rows);
}
try {
await client.connect();
console.log((await client.query('SHOW server_version')).rows);
await client.query(`CREATE TEMP TABLE articles (
article_id integer PRIMARY KEY,
title text NOT NULL
)`);
await client.query(`CREATE TEMP TABLE article_links (
link_id integer PRIMARY KEY,
article_id integer,
label text NOT NULL
)`);
await client.query(
'INSERT INTO pg_temp.articles VALUES ($1, $2)',
[1, 'Desk arrangement']
);
await client.query(`INSERT INTO pg_temp.article_links VALUES
(10, 1, 'Matching article'),
(11, 99, 'Older unmatched reference')`);
await client.query(`ALTER TABLE pg_temp.article_links
ADD CONSTRAINT article_links_article_fk
FOREIGN KEY (article_id)
REFERENCES pg_temp.articles (article_id) NOT VALID`);
await showStatus('After adding');
await expectForeignKeyError(
'New unmatched reference',
'INSERT INTO pg_temp.article_links VALUES ($1, $2, $3)',
[12, 99, 'New reference']
);
await expectForeignKeyError(
'Changed unmatched reference',
'UPDATE pg_temp.article_links SET article_id = $1 WHERE link_id = $2',
[98, 11]
);
await client.query(
'UPDATE pg_temp.article_links SET label = $1 WHERE link_id = $2',
['Revised label', 11]
);
console.log('Label-only update succeeded');
const unmatched = await client.query(`
SELECT l.link_id, l.article_id, l.label
FROM pg_temp.article_links AS l
WHERE l.article_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM pg_temp.articles AS a
WHERE a.article_id = l.article_id
)
ORDER BY l.link_id
`);
console.log('Unmatched references', unmatched.rows);
await expectForeignKeyError('Validation before repair', validate);
await showStatus('After failed validation');
await client.query(
'UPDATE pg_temp.article_links SET article_id = $1 WHERE link_id = $2',
[1, 11]
);
await client.query(validate);
await showStatus('After successful validation');
} finally {
await client.end();
}
在同一個專案目錄中執行已儲存的程式:
node not-valid-demo.mjs
準備期間,JavaScript 程式通過了 Node 語法檢查。研究環境無法執行資料庫;以下結果是依據文件記載的行為推導出的預期結果,並非擷取到的執行記錄。
如何解讀結果?
首先,限制條件應會出現在目錄中,其 convalidated 值為 false。系統目錄將此欄位定義為限制條件是否已驗證。查詢會同時依資料表名稱和限制條件名稱篩選,因為限制條件名稱在資料庫中不一定唯一。PostgreSQL:pg_constraint
接著,新增不匹配連結的插入操作,以及變更為不匹配參照的操作,都應各自產生 23503。PostgreSQL 將該 SQLSTATE 標示為 foreign_key_violation,並建議檢查錯誤代碼,而非比對可能因語系而異的訊息文字。輔助函式只接受預期的代碼和限制條件;任何非預期錯誤都會讓程式停止。PostgreSQL 錯誤代碼
只更新標籤後,連結 11 仍會指向文章 99。因此,診斷查詢應只回傳這筆連結,其標籤現在為 Revised label。明確指定的 IS NOT NULL 條件會排除可選的空值參照,而 NOT EXISTS 則會找出不存在於 articles 中的非空 ID。這些都是根據提供的合成資料列推導出的示意結果。
此時進行驗證應會失敗。修復操作將連結 11 改為參照文章 1 後,驗證應會成功,而 convalidated 應會變成 true。VALIDATE CONSTRAINT 會掃描資料表,檢查先前未驗證的限制條件;它本身不會修復資料。PostgreSQL:ALTER TABLE
在此示範中,修復方式是明確的編輯決定:我們預期兩個連結都指向 Desk arrangement。對於你自己的文章資料,請先檢查每筆不匹配參照,再決定要修正 ID、補上缺少的文章,還是移除已過時的連結。將所有不匹配的 ID 自動重新導向至同一篇文章,會掩蓋資料庫無法判斷的差異。
如何將此流程套用到已有資料的資料表?
分成三個獨立階段:加入並提交 NOT VALID 限制條件、檢查並修復不匹配的參照,然後進行驗證。加入外鍵會對兩個資料表取得 SHARE ROW EXCLUSIVE 鎖。驗證會對參照資料表取得 SHARE UPDATE EXCLUSIVE 鎖,並對被參照資料表取得 ROW SHARE 鎖,因此驗證掃描期間仍可執行一般的並行更新。不過,取得鎖仍可能需要等待。PostgreSQL:ALTER TABLE
將初次加入與後續驗證分開放在不同交易中,可保留分階段作法的用意:較長的掃描開始前,初次加入限制條件的交易即可先完成。請將加入限制條件安排為短時間操作,並將診斷查詢視為協助清理的工具,以成功驗證作為完成步驟。
此示範刻意不使用明確的交易區塊,讓程式能逐一處理預期中的陳述式失敗。如果你要將流程改用 node-postgres 交易,請自行執行 BEGIN、COMMIT 和 ROLLBACK,並確保交易中的每個陳述式都使用同一個用戶端。node-postgres 文件明確指出,交易陳述式不可使用 pool.query()。node-postgres:交易
請一併參考範例中的三項觀察結果:不匹配的資料列指出需要修復的項目,拒絕新的不匹配參照顯示限制仍在生效,而 convalidated = true 則記錄驗證成功。這些結果共同提供一個明確的完成標準,讓你能將關聯加入已有資料的文章資料表。
來源
- PostgreSQL 17:ALTER TABLE — PostgreSQL 全球開發組織。
- PostgreSQL:ALTER TABLE — PostgreSQL 全球開發組織。
- PostgreSQL 18:版本 18 — PostgreSQL 全球開發組織。
- PostgreSQL 18:CREATE TABLE — PostgreSQL 全球開發組織。
- PostgreSQL:pg_constraint — PostgreSQL 全球開發組織。
- PostgreSQL 18:PostgreSQL 錯誤代碼 — PostgreSQL 全球開發組織。
- PostgreSQL 18 原始碼:ri_triggers.c — PostgreSQL 專案。
- node-postgres 文件 — node-postgres。
- 連線 — node-postgres。
- 查詢 — node-postgres。
- 交易 — node-postgres。