PostgreSQL 18 预览: 支持以 “NOT VALID” 方式添加非空约束

John Doe 四月 25, 2025

当我们往数据库导入数据时,经常会出现违反约束报错的情况。现在,PostgreSQL 支持以 “NOT VALID” 方式添加非空约束了。

非洲大草原上的一头大象

特性提交日志

支持以 “NOT VALID” 方式添加非空约束。

这使得在添加这些约束时无需扫描表,并且在删除或修正了所有违反约束的行之后,验证约束时也无需对表持有排他访问锁。

对于存在无效非空约束的列,执行ALTER TABLE... SET NOT NULL操作会验证该约束。同时也支持ALTER TABLE.. VALIDATE CONSTRAINT操作。当父表存在有效约束时,对于子表中是否允许存在无效约束会有各种检查;这应该与我们对已实施 / 未实施约束的处理方式一致。

现在pg_attribute.attnotnull仅用于指示某列是否存在非空约束;该约束有效与否必须通过查询pg_constraint来确定。应用程序可以像以前一样继续查询pg_attribute.attnotnull,但现在即使该值设置为true,列中也可能存在NULL行。

pg_dump处理这些约束的方式,是将无效非空约束的 OID 存储在一个单独的数组中,并运行一个查询来获取它们的属性。常规的表创建 SQL 会完全忽略它们。然后,以与处理 “单独的” CHECK 约束相同的方式来处理这些约束,并在数据加载完成后转储它们。

讨论https://postgr.es/m/CAGPqQf0KitkNack4F5CFkFi-9Dqvp29Ro=EpcWt=4_hs-Rt+bQ@mail.gmail.com

示例

在深入了解该特性之前,让我们先看看 PostgreSQL 17 及更早版本在创建 “NOT NULL” 约束时是如何处理的。让我们来创建一个简单的表:

select version();
                                                      version
----------------------------------------------------------------------------------------------------------
 PostgreSQL 17.2 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 14.2.1 20250110 (Red Hat 14.2.1-7), 64-bit
(1 row)

create table t ( a int not null, b text );
postgres=# \dt
                   Table "public.t"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 a      | integer |           | not null |
 b      | text    |           |          |

尝试向该表插入违反约束的数据,当然会失败:

insert into t select null,1 from generate_series(1,2);
ERROR:  null value in column "a" of relation "t" violates not-null constraint
DETAIL:  Failing row contains (null, 1).

如果想这么做,唯一的选择是删除约束:

alter table t alter column a drop not null;

insert into t select null, 1 from generate_series(1,2);
postgres=# \dt
                   Table "public.t"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 a      | integer |           |          |
 b      | text    |           |          |

这种情况的一个使用场景是数据加载。假设你要加载的数据可能会违反约束,但你打算在加载之后手动修正,然后再重新启用约束,就像这样:

update t set a = 1;

alter table t alter column a set not null;
postgres=# \dt
                   Table "public.t"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 a      | integer |           | not null |
 b      | text    |           |          |
insert into t select null, 1 from generate_series(1,2);
ERROR:  null value in column "a" of relation "t" violates not-null constraint
DETAIL:  Failing row contains (null, 1).

到了 PostgreSQL 18,情况有所变化。从这个版本开始,你有了更多选择。下面的操作和以前的行为一致:

select version();
                              version
--------------------------------------------------------------------
 PostgreSQL 18devel on x86_64-linux, compiled by gcc-14.2.1, 64-bit
(1 row)

create table t ( a int, b text );

alter table t add constraint c1 not null a;
postgres=# \dt
                   Table "public.t"
  Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 a      | integer |           | not null |
 b      | text    |           |          |
insert into t select null, 1 from generate_series(1,2);
ERROR:  null value in column "a" of relation "t" violates not-null constraint
DETAIL:  Failing row contains (null, 1).

这和上面 PostgreSQL 17 的行为是一样的。但现在你还可以这样操作:

create table t ( a int, b text );

insert into t select null,1 from generate_series(1,2);

alter table t add constraint c1 not null a not valid;

这样我们就创建了一个 “NOT NULL” 约束,并且在创建时不会强制执行该约束。在 PostgreSQL 17 及更早版本中执行同样的操作,会扫描表并强制执行约束:

select version();
                                                      version
----------------------------------------------------------------------------------------------------------
 PostgreSQL 17.2 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 14.2.1 20250110 (Red Hat 14.2.1-7), 64-bit
(1 row)

create table t ( a int, b text );

insert into t select null,1 from generate_series(1,2);

alter table t add constraint c1 not null a not valid;
ERROR:  syntax error at or near "not"
LINE 1: alter table t add constraint c1 not null a not valid;
                                        ^

alter table t alter column a set not null;
ERROR:  column "a" of relation "t" contains null values

可以看到,旧版本不支持这种语法,添加 “NOT NULL” 约束会扫描表并强制执行约束。 回到 PostgreSQL 18 的实例。由于我们现在的数据会违反约束:

select * from t;
 a | b
---+---
   | 1
   | 1
(2 rows)
postgres=# \dt
                   Table "public.t"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 a      | integer |           | not null |
 b      | text    |           |          |

我们可以手动修正数据,然后再验证约束:

update t set a = 1;

alter table t validate constraint c1;

insert into t values (null, 'a');
ERROR:  null value in column "a" of relation "t" violates not-null constraint
DETAIL:  Failing row contains (null, a).

非常不错的体验。感谢社区的所有相关人员。

参考

提交日志:https://git.postgresql.org/pg/commitdiff/a379061a22a8fdf421e1a457cc6af8503def6252