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。可以用它理解关系,但不要用它恢复迁移文件中的原始 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 示例和预期目录结构是根据检索到的文档和源代码检查的。
决定要显示或调查哪些内容
当约束列表看起来异常冗长时,请按以下顺序处理。这样可以区分逻辑关系报告与实现清单,同时保留有用的证据。
- 确认关系和服务器版本。 使用带模式限定的关系名称,并记录
SHOW server_version;的结果,再根据文档比较行为。 - 追踪父级链接。 在此示例中,共享根 OID 的子行属于已声明关系的约束族。
- 比较两端。 如果引用表相同而
confrelid不同,就能解释上面显示的被引用分区行。 - 明确报告用途。 如果要报告选定表上声明的根关系,请使用
contype = 'f' AND conparentid = 0进行筛选。若需查看完整约束族,请保留递归报告。 - 单独调查不同的根约束。 在判定两项声明是否冗余之前,请比较列对、被引用表、动作、验证状态和可延迟性。
这种报告方式依据 pg_constraint 中记录的标识符和关系字段。仅筛选根约束的方式有意限定了范围:分区可能通过继承的子约束参与关系,因此对叶分区表筛选根约束,并不能完整报告影响该表的所有关系。
如果需要查看强制执行这些关系的对象,pg_trigger.tgconstraint 会将触发器链接到其约束,而 tgrelid 会标识承载该触发器的表。目录还会标识内部生成的触发器。这些字段提供了进一步的检查途径,详情见 pg_trigger。
如果声明的关系仍有必要,就应保留生成的行。对于重现的案例,实际处理方式是在逻辑架构视图中显示根关系,并在诊断视图中保留其子行。一旦父级链接和两端表的信息都清楚可见,这些额外行便能说明 PostgreSQL 如何跨分区实现该引用。
来源
表分区 — PostgreSQL 全球开发组。
pg_constraint — PostgreSQL 全球开发组。
发行版 12 — PostgreSQL 全球开发组。
tablecmds.c, REL_18_STABLE — PostgreSQL 全球开发组。
约束 — PostgreSQL 全球开发组。
psql — PostgreSQL 全球开发组。
系统信息函数和运算符 — PostgreSQL 全球开发组。
pg_trigger — PostgreSQL 全球开发组。
node-postgres 入门 — node-postgres。
查询 — node-postgres。
连接 — node-postgres。