PostgreSQL 會建立額外的外鍵系統目錄資料列,因為對分割資料表的參照也必須能跨越其實體分割區運作。若要判斷這些資料列代表同一個關係還是分開宣告的關係,請檢查其父項連結和被參照的資料表。

本指南適用於使用 Mac 的開發者,協助調查資料庫瀏覽器或 Node.js 應用程式中意料之外的限制條件清單。目標是透過合成文章資料列重現此行為、追蹤限制條件家族,並判斷邏輯結構描述報告中應包含哪些內容。

為什麼一個外鍵會產生多筆限制條件資料列?

分割資料表提供一個邏輯資料表,其資料列儲存在各個分割區中。父資料表本身沒有儲存空間。當另一個資料表參照它時,這項差異就很重要:PostgreSQL 必須在實體分割區中的資料列變更時維護此關係。請參閱官方資料表分割說明。

PostgreSQL 18 的實作清楚呈現了此機制。其 addFkRecurseReferenced 常式會建立外鍵動作觸發程序,並為每個被參照的分割區遞迴建立額外的 pg_constraint 資料列。其驗證程式碼說明,這些被參照端資料列是為了支援動作觸發程序,而非進行個別的驗證掃描。請參閱 PostgreSQL 的外鍵建立與驗證實作。

因此,這些資料列並不表示每個參照值都必須存在於每個分割區中。宣告的關係仍以分割資料表的父資料表為目標。PostgreSQL 也必須處理被參照端的變更,包括刪除被參照資料列或變更其索引鍵時所設定的行為。這些行為在外鍵中有說明。

哪些 PostgreSQL 版本支援此功能?

PostgreSQL 12 開始支援參照分割資料表的外鍵。較早的版本無法使用相同的父資料表目標宣告。PostgreSQL 12 現已列為不受支援;它是此功能的歷史版本界線,不是建議安裝的目標。本文使用目前的 PostgreSQL 18 文件及其維護中的原始碼分支。請參閱 PostgreSQL 12 版本資訊。

使用合成文章資料列重現此關係

使用現有的本機 PostgreSQL 開發資料庫和可正常運作的 psql 安裝。在 Mac 上開啟 Terminal,使用 psql -d postgres 連線,並執行 SHOW server_version; 檢查伺服器。必要時,請改用你的開發資料庫名稱。官方 psql 文件說明了如何選取資料庫及執行互動式 SQL。

執行以下完整設定一次。它會建立專用結構描述,不會移除現有物件。如果該結構描述已存在,請停止操作並改用全新的開發資料庫,或在所有範例中一致地重新命名結構描述。

BEGIN;

CREATE SCHEMA fk_partition_demo;

CREATE TABLE fk_partition_demo.articles (
    article_id integer NOT NULL,
    publication_year integer NOT NULL,
    title text NOT NULL,
    PRIMARY KEY (article_id, publication_year)
) PARTITION BY RANGE (publication_year);

CREATE TABLE fk_partition_demo.articles_2025
    PARTITION OF fk_partition_demo.articles
    FOR VALUES FROM (2025) TO (2026);

CREATE TABLE fk_partition_demo.articles_2026
    PARTITION OF fk_partition_demo.articles
    FOR VALUES FROM (2026) TO (2027);

CREATE TABLE fk_partition_demo.article_notes (
    note_id integer PRIMARY KEY,
    article_id integer NOT NULL,
    publication_year integer NOT NULL,
    note_text text NOT NULL,
    CONSTRAINT article_notes_article_fk
        FOREIGN KEY (article_id, publication_year)
        REFERENCES fk_partition_demo.articles
            (article_id, publication_year)
);

INSERT INTO fk_partition_demo.articles
    (article_id, publication_year, title)
VALUES
    (101, 2025, 'A guide to paper models'),
    (102, 2026, 'Choosing a notebook layout');

INSERT INTO fk_partition_demo.article_notes
    (note_id, article_id, publication_year, note_text)
VALUES
    (1, 101, 2025, 'Add a materials list'),
    (2, 102, 2026, 'Include a grid example');

COMMIT;

複合主索引鍵是刻意這樣設計的。PostgreSQL 要求分割資料表的主索引鍵或唯一性限制條件必須包含每個分割索引鍵資料行,且分割索引鍵中的運算式還有其他限制。由於此資料表按 publication_year 分割,主索引鍵包含該資料行,而外鍵則參照完整的一對欄位。請參閱宣告式分割:限制。

因此,此範例以 (article_id, publication_year) 識別文章。它並未確立 article_id 單獨具有全域唯一性。如果你的應用程式需要只使用 article_id 的參照,請先重新檢視分割方式和索引鍵設計,再採用此範例。

追蹤限制條件家族,而不是比較名稱

有用的系統目錄欄位包括 conrelid(參照資料表)、confrelid(被參照資料表),以及 conparentid(父限制條件的 OID)。限制條件名稱不一定唯一,因此單憑名稱無法判斷兩筆資料列是否彼此獨立。這些欄位記載於 pg_constraint。

執行設定後,請執行此查詢。它會從宣告在 article_notes 上的根外鍵開始,並遞迴追蹤其子限制條件。

WITH RECURSIVE fk_family AS (
    SELECT c.oid, c.conparentid, c.oid AS root_oid, 0 AS depth
    FROM pg_catalog.pg_constraint AS c
    WHERE c.contype = 'f'
      AND c.conrelid =
          'fk_partition_demo.article_notes'::regclass
      AND c.conparentid = 0

    UNION ALL

    SELECT child.oid, child.conparentid,
           parent.root_oid, parent.depth + 1
    FROM pg_catalog.pg_constraint AS child
    JOIN fk_family AS parent
      ON child.conparentid = parent.oid
    WHERE child.contype = 'f'
)
SELECT
    family.root_oid,
    family.depth,
    c.oid AS constraint_oid,
    c.conname,
    c.conparentid,
    c.conrelid::regclass AS referencing_table,
    c.confrelid::regclass AS referenced_table,
    pg_get_constraintdef(c.oid) AS definition
FROM fk_family AS family
JOIN pg_catalog.pg_constraint AS c ON c.oid = family.oid
ORDER BY family.root_oid, family.depth, c.oid;

對於這個單層範例,根據實作應會有三筆外鍵資料列:一筆以 articles 為目標,另外兩筆分別以它的兩個分割區為目標。這是根據原始碼推導出的預期結果,不是擷取的執行輸出。請勿將自動產生的名稱和 OID 值寫死。同一份原始碼也會在稍後建立或附加分割區時建立被參照端限制條件。請參閱 tablecmds.c。

此範例中的資料列 參照資料表 被參照資料表 父項連結
根關係 article_notes articles conparentid = 0
被參照分割區的子限制條件 article_notes articles_2025 根限制條件 OID
被參照分割區的子限制條件 article_notes articles_2026 根限制條件 OID

容易忽略的細節是,這三筆資料列中的參照資料表都維持為 article_notes。請同時檢查兩個資料表欄位和父項連結;只查看與限制條件相關聯的資料表,可能會掩蓋被參照分割區的結構。

此查詢也會透過 pg_get_constraintdef() 顯示重建後的定義。PostgreSQL 說明此函式會重建定義,而不是還原最初提交的 SQL。請用它理解關係,不要用來取回確切的遷移文字。請參閱系統資訊函式與運算子。

在 Mac 上使用 node-postgres 檢視相同資料列

node-postgres 會傳回伺服器的查詢結果;與分割區相關的資料列則由 PostgreSQL 的限制條件機制建立。其文件所述的查詢 API 會將 SQL 文字與參數值陣列分開傳入。此處的關聯名稱會以文字值傳入,再由 PostgreSQL 轉換為 regclass。請參閱查詢。

安裝目前受支援的 Node.js 版本後,請建立工作目錄並安裝 pg。安裝指令和執行環境相容性指引見於 node-postgres 簡介。

mkdir pg-fk-inspection
cd pg-fk-inspection
npm init -y
npm install pg

將以下完整程式儲存為 inspect-fks.mjs。它需要上方已提交的 SQL 設定。與遞迴 SQL 報告不同,此程式會列出直接附屬於所提供參照資料表的資料列;對於此範例中未分割的 article_notes 資料表,這樣就足夠了。

import pg from 'pg';

const { Client } = pg;
const client = new Client({ connectionTimeoutMillis: 5000 });
const relation = process.argv[2] ??
  'fk_partition_demo.article_notes';

try {
  await client.connect();

  const version = await client.query('SHOW server_version');
  console.log('PostgreSQL:', version.rows[0].server_version);

  const result = await client.query(`
    SELECT oid, conname,
           conrelid::regclass::text AS referencing_table,
           confrelid::regclass::text AS referenced_table,
           conparentid,
           conparentid = 0 AS is_root,
           pg_get_constraintdef(oid) AS definition
    FROM pg_catalog.pg_constraint
    WHERE contype = 'f' AND conrelid = $1::regclass
    ORDER BY conparentid, oid
  `, [relation]);

  console.table(result.rows);
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
} finally {
  await client.end();
}

請使用執行 SQL 設定時相同的資料庫來執行程式。node-postgres 會使用 PostgreSQL 連線環境變數,包括 PGDATABASE,並在連線中說明這些變數的預設值。

PGDATABASE=postgres node inspect-fks.mjs

如果找不到關聯,請確認兩個工作階段使用相同的資料庫,且設定已成功提交。空結果表示此查詢找不到直接附屬於該關聯的外鍵;它不會列出其他資料表對此關聯的參照。本文準備期間已對 JavaScript 執行語法檢查。由於無法執行資料庫查詢,因此 SQL 範例和預期的系統目錄結構是根據取得的文件與原始碼檢視。

決定要顯示或調查的內容

當限制條件清單看起來意外地長時,請依照以下步驟處理。這能區分邏輯關係報告和實作清單,同時保留有用的證據。

  1. 確認關聯和伺服器版本。 使用結構描述限定的關聯名稱,並記錄 SHOW server_version; 的結果,再與文件中的行為進行比較。
  2. 追蹤父項連結。 在此範例中,共用根 OID 的子資料列屬於已宣告關係的限制條件家族。
  3. 比較兩端。 若參照資料表相同,但 confrelid 不同,就能解釋上方所示的被參照分割區資料列。
  4. 依報告用途選擇。 若要列出所選資料表上宣告的根關係,請使用 contype = 'f' AND conparentid = 0 篩選。若要列出完整家族,則保留遞迴報告。
  5. 逐一調查不同的根。 在判斷兩筆宣告是否重複之前,請比較資料行配對、被參照資料表、動作、驗證狀態和可延遲性。

這種報告方式遵循 pg_constraint 中記載的識別碼和關係欄位。僅列出根項目的篩選範圍刻意有所限制:分割區可能透過繼承的子限制條件參與,因此只篩選葉資料表的根項目,無法完整列出影響該資料表的所有關係。

如果你需要檢視強制執行物件的證據,pg_trigger.tgconstraint 會將觸發程序連結到其限制條件,而 tgrelid 則會指出承載觸發程序的資料表。系統目錄也會標示內部產生的觸發程序。這些欄位提供進一步的檢查途徑,詳見 pg_trigger。

只要已宣告的關係仍有必要,就應保留產生的資料列。對於重現的案例,實務上的處理方式是在邏輯結構描述檢視中顯示根關係,並在診斷檢視中保留其子項。一旦能看見父項連結和兩端的資料表,這些額外資料列就能說明 PostgreSQL 如何實作跨分割區的參照。

來源