PostgreSQL 的 NOT VALID 外键会跳过对现有行的初始检查,同时强制检查新的引用。较早的不匹配行会保留,直到修复并完成验证。PostgreSQL 17:ALTER TABLE
本指南面向在 Mac 的终端中工作的开发者,介绍如何为已有数据的表添加外键并理解操作结果。完整的 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 版本。在终端中进入项目目录,运行以下命令安装软件包,然后将完整程序保存为 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
每个操作都会使用单独的 await 查询。值通过 $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。