PostgreSQL erstellt zusätzliche Fremdschlüssel-Katalogzeilen, weil eine Referenz auf eine partitionierte Tabelle auch über deren physische Partitionen hinweg funktionieren muss. Um festzustellen, ob diese Zeilen eine Beziehung oder separate Deklarationen darstellen, untersuchen Sie ihre übergeordneten Verknüpfungen und referenzierten Tabellen.

Diese Anleitung richtet sich an Entwickler, die auf einem Mac eine unerwartete Einschränkungsliste in einem Datenbankbrowser oder einer Node.js-Anwendung untersuchen. Ziel ist es, das Verhalten anhand synthetischer Artikelzeilen nachzustellen, die Einschränkungsfamilie nachzuverfolgen und zu entscheiden, was in einen logischen Schemasbericht gehört.

Warum erzeugt ein Fremdschlüssel mehrere Einschränkungszeilen?

Eine partitionierte Tabelle stellt eine logische Tabelle bereit, deren Zeilen in ihren Partitionen gespeichert sind. Das übergeordnete Element hat keinen eigenen Speicher. Diese Unterscheidung ist wichtig, wenn eine andere Tabelle darauf verweist: PostgreSQL muss die Beziehung aufrechterhalten, wenn sich Zeilen in den physischen Partitionen ändern. Siehe die offizielle Dokumentation zur Tabellenpartitionierung.

Die PostgreSQL-18-Implementierung macht den Mechanismus deutlich. Ihre Routine addFkRecurseReferenced erstellt Fremdschlüssel-Aktionstrigger und rekursiv für jede referenzierte Partition eine zusätzliche Zeile pg_constraint. Der Validierungscode erklärt, dass diese einseitig referenzierten Zeilen Aktionstrigger unterstützen und keine separaten Validierungsscans darstellen. Siehe PostgreSQLs Implementierung der Erstellung und Validierung von Fremdschlüsseln.

Diese Zeilen bedeuten daher nicht, dass jeder referenzierende Wert in jeder Partition vorhanden sein muss. Die deklarierte Beziehung zielt weiterhin auf das partitionierte übergeordnete Element. PostgreSQL muss außerdem Änderungen auf der referenzierten Seite behandeln, einschließlich des konfigurierten Verhaltens, wenn eine referenzierte Zeile gelöscht oder ihr Schlüssel geändert wird. Diese Verhaltensweisen werden unter Fremdschlüssel beschrieben.

Welche PostgreSQL-Versionen unterstützen dies?

Fremdschlüssel, die auf eine partitionierte Tabelle verweisen, wurden in PostgreSQL 12 eingeführt. Frühere Versionen können dieselbe auf das übergeordnete Element zielende Deklaration nicht verwenden. PostgreSQL 12 wird inzwischen als nicht mehr unterstützt aufgeführt; es markiert die historische Grenze für die Einführung des Features und ist kein empfohlenes Installationsziel. Dieser Artikel verwendet die aktuelle PostgreSQL-18-Dokumentation und deren gepflegten Quellcodezweig. Siehe die Versionshinweise zu PostgreSQL 12.

Die Beziehung mit synthetischen Artikelzeilen nachstellen

Verwenden Sie eine vorhandene lokale PostgreSQL-Entwicklungsdatenbank und eine funktionierende Installation von psql. Öffnen Sie auf Ihrem Mac Terminal, verbinden Sie sich mit psql -d postgres und prüfen Sie den Server mit SHOW server_version;. Ersetzen Sie den Datenbanknamen bei Bedarf durch den Namen Ihrer Entwicklungsdatenbank. Die offizielle psql-Dokumentation beschreibt die Auswahl einer Datenbank und die interaktive Ausführung von SQL.

Führen Sie dieses vollständige Setup einmal aus. Es erstellt ein eigenes Schema, ohne vorhandene Objekte zu entfernen. Wenn dieses Schema bereits existiert, brechen Sie ab und verwenden Sie eine frische Entwicklungsdatenbank oder benennen Sie das Schema einheitlich in allen Beispielen um.

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;

Der zusammengesetzte Primärschlüssel ist bewusst gewählt. PostgreSQL verlangt, dass ein Primär- oder Unique-Constraint einer partitionierten Tabelle alle Spalten des Partitionsschlüssels enthält; außerdem gelten weitere Einschränkungen für Ausdrücke in Partitionsschlüsseln. Da diese Tabelle nach publication_year partitioniert ist, enthält der Primärschlüssel diese Spalte, und der Fremdschlüssel verweist auf das vollständige Spaltenpaar. Siehe Deklarative Partitionierung: Einschränkungen.

Das Beispiel identifiziert einen Artikel daher anhand von (article_id, publication_year). Es belegt nicht, dass allein article_id global eindeutig ist. Wenn Ihre Anwendung Referenzen ausschließlich mit article_id benötigt, sollten Sie Partitionierung und Schlüsselentwurf überarbeiten, bevor Sie dieses Beispiel übernehmen.

Die Einschränkungsfamilie nachverfolgen, statt Namen zu vergleichen

Die relevanten Katalogfelder sind conrelid, die referenzierende Tabelle; confrelid, die referenzierte Tabelle; und conparentid, die OID der übergeordneten Einschränkung. Namen von Einschränkungen sind nicht zwingend eindeutig. Daher lässt sich anhand der Namen allein nicht feststellen, ob zwei Zeilen unabhängig voneinander sind. Diese Felder sind in pg_constraint dokumentiert.

Führen Sie nach dem Setup diese Abfrage aus. Sie beginnt mit den auf article_notes deklarierten Fremdschlüsseln der obersten Ebene und folgt rekursiv ihren untergeordneten Einschränkungen.

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;

Für dieses Beispiel mit einer Partitionierungsebene impliziert die Implementierung drei Fremdschlüsselzeilen: eine, die auf articles zielt, und je eine für dessen zwei Partitionen. Dies ist ein aus dem Quellcode abgeleitetes erwartetes Ergebnis, keine aufgezeichnete Ausgabe einer Ausführung. Generierte Namen und OID-Werte sollten nicht fest codiert werden. Derselbe Quellcode erstellt auch einseitig referenzierte Einschränkungen, wenn später eine Partition angelegt oder angefügt wird. Siehe tablecmds.c.

Zeile in diesem Beispiel Referenzierende Tabelle Referenzierte Tabelle Übergeordnete Verknüpfung
Beziehung der obersten Ebene article_notes articles conparentid = 0
Untergeordnete Einschränkung für eine referenzierte Partition article_notes articles_2025 OID der Einschränkung der obersten Ebene
Untergeordnete Einschränkung für eine referenzierte Partition article_notes articles_2026 OID der Einschränkung der obersten Ebene

Das leicht zu übersehende Detail ist, dass die referenzierende Tabelle in allen drei Zeilen article_notes bleibt. Untersuchen Sie beide Tabellenspalten und die übergeordnete Verknüpfung; wenn Sie nur die der Einschränkung zugeordnete Tabelle betrachten, kann die Struktur der referenzierten Partitionen verborgen bleiben.

Die Abfrage zeigt außerdem rekonstruierte Definitionen über pg_get_constraintdef() an. PostgreSQL dokumentiert diese Funktion als Rekonstruktion und nicht als den ursprünglich übermittelten SQL-Code. Verwenden Sie sie, um die Beziehung zu verstehen, nicht um den exakten Migrationstext wiederherzustellen. Siehe Systeminformationsfunktionen und -operatoren.

Dieselben Zeilen mit node-postgres auf Ihrem Mac untersuchen

node-postgres gibt die Abfrageergebnisse des Servers zurück; die partitionierungsbezogenen Zeilen stammen aus der Einschränkungsmechanik von PostgreSQL. Die dokumentierte Abfrage-API akzeptiert SQL-Text und ein separates Array mit Parameterwerten. Hier wird der Beziehungsname als Textwert übergeben, den PostgreSQL in regclass umwandelt. Siehe Abfragen.

Installieren Sie eine aktuelle unterstützte Node.js-Version, erstellen Sie ein Arbeitsverzeichnis und installieren Sie pg. Den Installationsbefehl und Hinweise zur Laufzeitkompatibilität finden Sie in der Einführung zu node-postgres.

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

Speichern Sie das folgende vollständige Programm unter inspect-fks.mjs. Es setzt das oben ausgeführte SQL-Setup voraus. Anders als der rekursive SQL-Bericht listet dieses Programm die Zeilen auf, die direkt mit der angegebenen referenzierenden Tabelle verknüpft sind. Das genügt für die nicht partitionierte Tabelle article_notes in diesem Beispiel.

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();
}

Führen Sie das Programm mit derselben Datenbank aus, die für das SQL-Setup verwendet wurde. node-postgres nutzt die PostgreSQL-Verbindungsumgebungsvariablen, darunter PGDATABASE, und dokumentiert deren Standardwerte unter Verbindung herstellen.

PGDATABASE=postgres node inspect-fks.mjs

Wenn die Beziehung nicht gefunden werden kann, prüfen Sie, ob beide Sitzungen dieselbe Datenbank verwenden und das Setup erfolgreich abgeschlossen wurde. Ein leeres Ergebnis bedeutet, dass diese Abfrage keine direkt mit dieser Beziehung verknüpften Fremdschlüssel gefunden hat; eingehende Referenzen aus anderen Tabellen werden nicht angezeigt. Das JavaScript wurde bei der Vorbereitung dieses Artikels einer Syntaxprüfung unterzogen. Eine Datenbankausführung war nicht möglich, daher wurden die SQL-Beispiele und die erwartete Katalogstruktur anhand der abgerufenen Dokumentation und des Quellcodes geprüft.

Entscheiden, was angezeigt oder untersucht werden soll

Gehen Sie wie folgt vor, wenn eine Einschränkungsliste unerwartet lang erscheint. So lassen sich ein Bericht über logische Beziehungen und ein Inventar der Implementierung unterscheiden, ohne nützliche Hinweise zu verwerfen.

  1. Beziehung und Serverversion bestätigen. Verwenden Sie den Schema-qualifizierten Beziehungsnamen und notieren Sie SHOW server_version;, bevor Sie das Verhalten mit der Dokumentation vergleichen.
  2. Übergeordnete Verknüpfungen nachverfolgen. In diesem Beispiel gehören untergeordnete Zeilen mit derselben OID der obersten Ebene zur Einschränkungsfamilie der deklarierten Beziehung.
  3. Beide Enden vergleichen. Ein wechselndes confrelid bei gleichbleibender referenzierender Tabelle erklärt die oben gezeigten Zeilen für referenzierte Partitionen.
  4. Den Zweck des Berichts festlegen. Für Beziehungen der obersten Ebene, die auf der ausgewählten Tabelle deklariert sind, filtern Sie mit contype = 'f' AND conparentid = 0. Für die vollständige Familie verwenden Sie den rekursiven Bericht.
  5. Separate Beziehungen der obersten Ebene einzeln untersuchen. Vergleichen Sie Spaltenpaare, referenzierte Tabellen, Aktionen, Validierungsstatus und Aufschiebbarkeit, bevor Sie zwei Deklarationen als redundant einstufen.

Dieser Berichtsansatz folgt den in pg_constraint dokumentierten Kennungen und Beziehungsfeldern. Der Filter für Beziehungen der obersten Ebene ist bewusst eingeschränkt: Eine Partition kann über geerbte untergeordnete Einschränkungen beteiligt sein. Daher liefert das Filtern einer Blattpartition auf Beziehungen der obersten Ebene keinen vollständigen Bericht über alle Beziehungen, die sie betreffen.

Wenn Sie Hinweise auf die Durchsetzungsobjekte benötigen, verknüpft pg_trigger.tgconstraint einen Trigger mit seiner Einschränkung, während tgrelid die Tabelle angibt, die den Trigger enthält. Der Katalog kennzeichnet außerdem intern generierte Trigger. Diese Felder bieten einen weiteren Untersuchungsweg in pg_trigger.

Behalten Sie die generierten Zeilen bei, wenn die deklarierte Beziehung weiterhin erforderlich ist. Für den nachgestellten Fall empfiehlt es sich, die Beziehung der obersten Ebene in einer logischen Schemaansicht anzuzeigen und die untergeordneten Einträge in der Diagnoseansicht zu behalten. Sobald die übergeordneten Verknüpfungen und beide Tabellenenden sichtbar sind, erklären die zusätzlichen Zeilen, wie PostgreSQL die Referenz über Partitionen hinweg implementiert.

Quellen