Skip to content

继承与约束 ​

约束在继承结构下的行为是分裂的:CHECK 约束与 NOT NULL 约束会自动被所有子表继承,而 UNIQUE、PRIMARY KEY、FOREIGN KEY 以及索引只作用于单张表,不会向子表传播。这个不对称是继承最著名、也最容易造成数据完整性事故的地方。

本节逐类分析约束在继承下的实际语义,重点讲清「父表唯一约束不约束子表集合」这个坑,并给出用触发器、NO INHERIT、迁移到声明式分区等手段的应对方案。

前置知识

阅读本节前,建议先了解:表继承概述、创建与使用继承表、主键约束

基础概念 ​

约束传播速查表。 这是本节最核心的一张表:

约束类型是否继承到子表说明
CHECK是可用 NO INHERIT 阻断;同名约束会被合并,条件必须一致
NOT NULL是本质是列属性,随列一起复制
UNIQUE否父表唯一性不覆盖子表;子表也不会自动获得唯一约束
PRIMARY KEY否同上;复合主键也不传播
FOREIGN KEY否引用方向与被引用方向都不传播
索引否索引属于单张物理表,子表需独立创建
sql
-- 演示:父表 CHECK 会继承,UNIQUE 不会
CREATE TABLE p (
  id  int,
  val int CHECK (val > 0)          -- 会被继承
);
CREATE TABLE c () INHERITS (p);    -- c 自动带上 ck_val

ALTER TABLE p ADD CONSTRAINT uq_p_id UNIQUE (id);   -- 不会传播到 c

\d c
text
                 Table "public.c"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 id     | integer |           |          |
 val    | integer |           |          |
Check constraints:
    "p_val_check" CHECK (val > 0)     -- 继承了父表的 CHECK
Indexes:
    -- 没有任何索引,也没有唯一约束

三个必须记住的结论:

结论后果
父表 UNIQUE 不阻止子表出现与父表重复的值整棵子树的「全局唯一」必须自己保证
子表默认没有任何唯一约束子表内部也可能出现重复
外键不跨继承层次引用方与被引用方都要单独处理

语法与用法 ​

约束在继承下的相关语法:

text
-- CHECK:可继承,可用 NO INHERIT 阻断继承
ALTER TABLE 子表 ADD CONSTRAINT 名 CHECK (条件) NO INHERIT;

-- NOT NULL:随列继承,不能在子表单独取消
-- (NOT NULL 是列属性,继承列的属性由父表定义)

-- UNIQUE / PRIMARY KEY:不继承,必须逐表创建
ALTER TABLE 子表 ADD CONSTRAINT 名 UNIQUE (列);

-- 外键:不继承,逐表创建;引用继承表时默认不覆盖子表
ALTER TABLE 子表 ADD CONSTRAINT 名 FOREIGN KEY (列) REFERENCES 其他表(列);

-- ONLY 加约束:只加到当前表,不传播
ALTER TABLE ONLY 表 ADD CONSTRAINT 名 CHECK (条件);

最简可运行示例——验证 CHECK 继承、UNIQUE 不继承:

sql
DROP TABLE IF EXISTS ck_child, ck_parent CASCADE;

CREATE TABLE ck_parent (
  id  int,
  val int,
  CONSTRAINT ck_val_positive CHECK (val > 0)
) ;

CREATE TABLE ck_child () INHERITS (ck_parent);

-- CHECK 生效于子表
INSERT INTO ck_child VALUES (1, -5);
text
ERROR:  new row for relation "ck_child" violates check constraint "ck_val_positive"
DETAIL:  Failing row contains (1, -5).
sql
-- 但父表的 UNIQUE 管不到子表
ALTER TABLE ck_parent ADD CONSTRAINT uq_ck_parent_id UNIQUE (id);
INSERT INTO ck_parent VALUES (1, 10);
INSERT INTO ck_child  VALUES (1, 20);   -- 成功!
SELECT tableoid::regclass AS src, id, val FROM ck_parent ORDER BY src::text;
text
     src     | id | val
-------------+----+-----
 ck_child    |  1 |  20
 ck_parent   |  1 |  10
(2 rows)

详细说明 ​

CHECK 约束:会继承,可放宽 ​

父表的 CHECK 自动成为每个子表的 CHECK。子表可以另外加自己的 CHECK,但不能放宽父表的约束(除非在子表上用 NO INHERIT,或父表本身用 NO INHERIT 声明)。

sql
DROP TABLE IF EXISTS ck2_child, ck2_parent CASCADE;
CREATE TABLE ck2_parent (
  v int,
  CONSTRAINT ck2_v_range CHECK (v BETWEEN 0 AND 100)
);
CREATE TABLE ck2_child () INHERITS (ck2_parent);

INSERT INTO ck2_child VALUES (200);
text
ERROR:  new row for relation "ck2_child" violates check constraint "ck2_v_range"
DETAIL:  Failing row contains (200).

子表可以追加更严格的约束:

sql
ALTER TABLE ck2_child ADD CONSTRAINT ck2_v_even CHECK (v % 2 = 0);

INSERT INTO ck2_child VALUES (7);    -- 通过父表 CHECK,但违反子表 CHECK
text
ERROR:  new row for relation "ck2_child" violates check constraint "ck2_v_even"
DETAIL:  Failing row contains (7).

尝试在子表放宽父表约束是不允许的(不能用同名约束改写条件):

sql
ALTER TABLE ck2_child DROP CONSTRAINT ck2_v_range;
text
ERROR:  cannot drop inherited constraint "ck2_v_range" of relation "ck2_child"

要放宽,只能在父表上改,或让约束从一开始就 NO INHERIT。

NOT NULL:随列继承 ​

NOT NULL 是列属性,父表上定义为 NOT NULL 的列,子表自动也是 NOT NULL;而且不能在子表单独取消:

sql
DROP TABLE IF EXISTS nn_child, nn_parent CASCADE;
CREATE TABLE nn_parent (
  id  bigint NOT NULL,
  val int
);
CREATE TABLE nn_child () INHERITS (nn_parent);

INSERT INTO nn_child VALUES (NULL, 1);
text
ERROR:  null value in column "id" of relation "nn_child" violates not-null constraint
DETAIL:  Failing row contains (null, 1).
sql
ALTER TABLE nn_child ALTER COLUMN id DROP NOT NULL;
text
ERROR:  column "id" is marked NOT NULL in parent table "nn_parent"

在多父表场景下,合并列的 NOT NULL 取并集——只要任一来源是 NOT NULL,合并后就是 NOT NULL(文档原话:a merged column will be marked not-null if any one of the column definitions it came from is marked not-null):

sql
DROP TABLE IF EXISTS m_child, m_a, m_b CASCADE;
CREATE TABLE m_a (id int NOT NULL);
CREATE TABLE m_b (id int);
CREATE TABLE m_child () INHERITS (m_a, m_b);

-- 查看合并结果
SELECT attname, attnotnull FROM pg_attribute
WHERE attrelid = 'm_child'::regclass AND attname = 'id';
text
 attname | attnotnull
---------+------------
 id      | t
(1 row)

UNIQUE 与 PRIMARY KEY:完全不继承 ​

这是继承最严重的限制。文档明确写道:

This is true on both the referencing and referenced sides of a foreign key constraint. If we declared cities.name to be UNIQUE or a PRIMARY KEY, this would not stop the capitals table from having rows with names duplicating rows in cities.

sql
DROP TABLE IF EXISTS up_child, up_parent CASCADE;
CREATE TABLE up_parent (id int PRIMARY KEY);
CREATE TABLE up_child () INHERITS (up_parent);

\d up_child
text
                 Table "public.up_child"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 id     | integer |           | not null |
-- 注意:没有 Indexes 段,子表根本没有主键索引

注意子表连 NOT NULL 都没有显式标出是继承语义的自然结果——它继承了 id 的 NOT NULL(来自主键),但没有继承主键本身。子表就可以插入重复值:

sql
INSERT INTO up_child VALUES (1), (1), (1);   -- 全部成功
SELECT count(*) FROM up_child;
text
 count
-------
     3
(1 row)

给子表单独加唯一约束只能保证子表内部唯一,无法跨表:

sql
ALTER TABLE up_child ADD CONSTRAINT uq_up_child_id UNIQUE (id);
INSERT INTO up_child VALUES (1);   -- 现在被拦住
text
ERROR:  duplicate key value violates unique constraint "uq_up_child_id"
DETAIL:  Key (id)=(1) already exists.

但父表仍可插入相同的 id:

sql
INSERT INTO up_parent VALUES (1);   -- 成功!父表只跟自己比
text
INSERT 0 1

结果就是「子表有唯一约束、父表有主键,但两边各存了一个 id = 1」——任何通过父表的查询都会看到两条 id 相同的行。

父表唯一约束不约束子表集合:著名坑 ​

把上一条推到极端:如果希望「整棵子树里 order_no 唯一」,没有任何声明式手段能做到。

sql
DROP TABLE IF EXISTS ord_y2025, ord_y2026, ord CASCADE;
CREATE TABLE ord (
  order_no text NOT NULL,
  amount   numeric(12,2)
);
ALTER TABLE ord ADD CONSTRAINT uq_ord_no UNIQUE (order_no);

CREATE TABLE ord_y2025 () INHERITS (ord);
CREATE TABLE ord_y2026 () INHERITS (ord);
ALTER TABLE ord_y2025 ADD CONSTRAINT uq_ord_y2025_no UNIQUE (order_no);
ALTER TABLE ord_y2026 ADD CONSTRAINT uq_ord_y2026_no UNIQUE (order_no);

INSERT INTO ord_y2025 VALUES ('A-001', 100);
INSERT INTO ord_y2026 VALUES ('A-001', 200);   -- 成功!跨年重复
sql
SELECT tableoid::regclass AS tbl, order_no, amount
FROM ord ORDER BY order_no, tbl::text;
text
    tbl     | order_no | amount
------------+----------+--------
 ord_y2025  | A-001    | 100.00
 ord_y2026  | A-001    | 200.00
(2 rows)

没有好的声明式解法。 可选方案:

方案做法代价
全局唯一来源表建一张普通表 order_no_registry(order_no PRIMARY KEY),写入前先插它多一次写入 + 业务逻辑
全局序列 / UUID用 nextval 或 uuidv7() 生成编号,天然全局唯一改变了编号语义
触发器校验BEFORE INSERT 触发器查询父表 ∪ 所有子表并发下有竞争,性能差
迁移到声明式分区分区表的唯一约束可覆盖全部分区(须含分区键)需要改造表结构

声明式分区下的行为对比:

sql
-- 声明式分区:唯一约束可以覆盖所有分区,但必须包含分区键
CREATE TABLE orders_p (
  order_no text,
  created  date,
  PRIMARY KEY (order_no, created)     -- 必须含分区键 created
) PARTITION BY RANGE (created);
text
-- 若只写 PRIMARY KEY (order_no) 会报错:
ERROR:  unique constraint on partitioned table must include all partitioning columns

换句话说,即便换成声明式分区,「全局唯一」也只能在「唯一键包含分区键」时才能声明式保证。这也是为什么很多系统的业务编号唯一性依赖外部手段。

外键约束:两个方向都不继承 ​

外键的限制分为「引用方」与「被引用方」两种,文档都明确点名:

方向一:父表的外键不会传播到子表。

sql
DROP TABLE IF EXISTS fk_detail, fk_order CASCADE;
CREATE TABLE fk_order (id int PRIMARY KEY);
CREATE TABLE fk_detail (
  order_id int REFERENCES fk_order(id)   -- 定义在 detail 侧
);

若把 fk_detail 变成子表,REFERENCES 不会自动出现在新子表上——必须逐表显式添加。

方向二:别人 REFERENCES 父表,不会覆盖子表。

sql
DROP TABLE IF EXISTS ref_t, base_parent, base_child CASCADE;
CREATE TABLE base_parent (id int PRIMARY KEY);
CREATE TABLE base_child () INHERITS (base_parent);
INSERT INTO base_child VALUES (42);

CREATE TABLE ref_t (pid int REFERENCES base_parent(id));

INSERT INTO ref_t VALUES (42);
text
ERROR:  insert or update on table "ref_t" violates foreign key constraint "ref_t_pid_fkey"
DETAIL:  Key (pid)=(42) is not present in table "base_parent".

即使 42 确实存在于 base_child(父表查询也能查到它),外键校验只查 base_parent 这一张物理表。文档对此的评价是:「There is no good workaround for this case.」

这带来一个重要的实践推论:不要用外键指向一张采用继承的父表,因为你会失去对子表数据的引用完整性。

父表索引不被子表继承 ​

索引与唯一约束一样,属于单张物理表:

sql
DROP TABLE IF EXISTS idx_child, idx_parent CASCADE;
CREATE TABLE idx_parent (id int, created date);
CREATE INDEX idx_parent_created ON idx_parent (created);
CREATE TABLE idx_child () INHERITS (idx_parent);

-- 查看父子各自有哪些索引
SELECT c.relname AS tbl, i.relname AS idx
FROM pg_index x
JOIN pg_class c ON c.oid = x.indrelid
JOIN pg_class i ON i.oid = x.indexrelid
WHERE c.relname IN ('idx_parent', 'idx_child')
ORDER BY c.relname, i.relname;
text
    tbl     |        idx
------------+--------------------
 idx_parent | idx_parent_created
(1 row)

子表一行索引都没有。查询 WHERE created = ... 走父表时会扫描子表,但没有可用索引:

sql
EXPLAIN (COSTS OFF) SELECT * FROM idx_parent WHERE created = DATE '2025-01-01';
text
 Append
   ->  Index Scan using idx_parent_created on idx_parent idx_parent_1
         Index Cond: (created = '2025-01-01'::date)
   ->  Seq Scan on idx_child idx_parent_2
         Filter: (created = '2025-01-01'::date)
(5 rows)

修复方式是给每个子表建相同索引——这也是老式分区维护脚本必须包含的一步:

sql
CREATE INDEX idx_child_created ON idx_child (created);

-- 或者批量生成 DDL
SELECT format('CREATE INDEX ON %I (created);', c.relname)
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
WHERE i.inhparent = 'idx_parent'::regclass;

NO INHERIT:阻断 CHECK 的继承 ​

在 CHECK 约束上加 NO INHERIT,可以让它只约束本表,不传播到子表。这是「子表需要放宽条件」的合法途径:

sql
DROP TABLE IF EXISTS ni_child, ni_parent CASCADE;
CREATE TABLE ni_parent (
  v int,
  CONSTRAINT ck_ni_small CHECK (v < 100) NO INHERIT   -- 只约束父表
);
CREATE TABLE ni_child () INHERITS (ni_parent);

-- 子表不受父表 NO INHERIT 的 CHECK 约束
INSERT INTO ni_child VALUES (500);       -- 成功
INSERT INTO ni_parent VALUES (500);      -- 被拦
text
ERROR:  new row for relation "ni_parent" violates check constraint "ck_ni_small"
DETAIL:  Failing row contains (500).

用系统目录确认 NO INHERIT 已生效:

sql
SELECT conname, conislocal, connoinherit, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid IN ('ni_parent'::regclass, 'ni_child'::regclass)
  AND contype = 'c'
ORDER BY conrelid::regclass::text;
text
   conname    | conislocal | connoinherit |      pg_get_constraintdef
--------------+------------+--------------+-------------------------------
 ck_ni_small  | t          | t            | CHECK ((v < 100)) NO INHERIT
(1 row)

connoinherit = t 就是 NO INHERIT 的标记;子表上完全没有这条约束记录,因为它没有继承过去。

ONLY 与约束检查的范围 ​

ALTER TABLE ONLY parent ADD CONSTRAINT ... 只给父表加约束、不传播;不加 ONLY 则传播到整棵子树:

sql
DROP TABLE IF EXISTS ch2, pa2 CASCADE;
CREATE TABLE pa2 (v int);
CREATE TABLE ch2 () INHERITS (pa2);

-- 传播给所有子表
ALTER TABLE pa2 ADD CONSTRAINT ck_pa2 CHECK (v > 0);

SELECT conrelid::regclass AS tbl, conname
FROM pg_constraint
WHERE conname = 'ck_pa2'
ORDER BY tbl::text;
text
  tbl  | conname
-------+---------
 ch2   | ck_pa2
 pa2   | ck_pa2
(2 rows)
sql
-- 只加到父表,子表不受影响
ALTER TABLE ONLY pa2 ADD CONSTRAINT ck_pa2_only CHECK (v < 1000);

SELECT conrelid::regclass AS tbl, conname
FROM pg_constraint WHERE conname = 'ck_pa2_only';
text
  tbl  |   conname
-------+-------------
 pa2   | ck_pa2_only
(1 row)

这里有个陷阱:用 ONLY 加的约束若在父表上被违反,子表插入却不会检查它——父表的约束检查只在本表插入时执行。所以「用 ALTER TABLE ONLY 给父表加约束、期望整棵树受约束」是错误预期。

用触发器替代唯一性约束 ​

当必须保证「跨子表唯一」时,只能靠触发器。示例:保证 order_no 在父表 ∪ 所有子表中唯一。

sql
CREATE OR REPLACE FUNCTION check_order_no_unique()
RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE
  dup_count bigint;
BEGIN
  -- 只在插入/更新 order_no 时检查;父表查询默认递归,天然覆盖整棵树
  EXECUTE format(
    'SELECT count(*) FROM %I WHERE order_no = $1 AND (ctid, tableoid) <> ($2, $3)',
    TG_TABLE_NAME
  ) INTO dup_count USING NEW.order_no, NEW.ctid, NEW.tableoid;

  IF dup_count > 0 THEN
    RAISE EXCEPTION 'order_no % already exists in inheritance tree', NEW.order_no;
  END IF;
  RETURN NEW;
END;
$$;

CREATE TABLE ord2 (order_no text NOT NULL, amount numeric(12,2));
CREATE TABLE ord2_y2025 () INHERITS (ord2);

CREATE TRIGGER trg_ord2_unique
BEFORE INSERT OR UPDATE OF order_no ON ord2
FOR EACH ROW EXECUTE FUNCTION check_order_no_unique();

必须注意:触发器只加在父表上是不生效的——继承不会传播触发器。若子表可能被直接写入,触发器要逐表创建:

sql
CREATE TRIGGER trg_ord2_y2025_unique
BEFORE INSERT OR UPDATE OF order_no ON ord2_y2025
FOR EACH ROW EXECUTE FUNCTION check_order_no_unique();

而且触发器方案有已知弱点:

  • 并发竞争:两个事务同时插入相同 order_no 可能都通过检查(没有唯一索引可依赖)。
  • 性能:每次写入都要扫整棵树。
  • 维护成本:新增子表时忘了加触发器就会留下漏洞。

因此生产系统更常用「唯一编号注册表 + 唯一索引」来保证全局唯一。

迁移到声明式分区时的约束注意点 ​

从继承迁到声明式分区能修复大部分约束问题,但有几个硬性规则:

约束声明式分区下的行为
PRIMARY KEY / UNIQUE可以定义在分区表上并覆盖所有分区,但必须包含全部分区键列
FOREIGN KEY可以定义在分区表上,自动应用到分区(PG 12+ 支持引用分区表)
CHECK定义在分区表上自动应用于所有分区;分区自己也可以有额外 CHECK
EXCLUDE定义在分区表上必须包含分区键列
NOT NULL正常继承

迁移验证示例:

sql
-- 继承结构
DROP TABLE IF EXISTS mig_y2025, mig CASCADE;
CREATE TABLE mig (id bigint, d date) ;
CREATE TABLE mig_y2025 () INHERITS (mig);

-- 目标:声明式分区结构(注意主键必须含分区键 d)
DROP TABLE IF EXISTS mig_p CASCADE;
CREATE TABLE mig_p (
  id bigint,
  d  date,
  PRIMARY KEY (id, d)            -- 必须包含分区键
) PARTITION BY RANGE (d);

CREATE TABLE mig_p_y2025 PARTITION OF mig_p
  FOR VALUES FROM (DATE '2025-01-01') TO (DATE '2026-01-01');
text
-- 若忽略分区键会报错:
ERROR:  unique constraint on partitioned table must include all partitioning columns

另外,继承结构里那些「手工 CHECK 范围约束」在声明式分区下无需保留——分区边界由 FOR VALUES 自动表达,且规划器用 relpartbound 做裁剪,比 CHECK 约束排除更强、更可靠。

实战示例 ​

场景:在一个按年继承的订单结构中,验证各类约束的真实行为,并用注册表方案补齐全局唯一。

第一步,搭结构与现有约束:

sql
DROP TABLE IF EXISTS sol_y2026, sol_y2025, sol CASCADE;

CREATE TABLE sol (
  order_no text   NOT NULL,
  amount   numeric(12,2) NOT NULL,
  created  date   NOT NULL DEFAULT current_date,
  CONSTRAINT ck_sol_amount CHECK (amount > 0)
);

CREATE TABLE sol_y2025 () INHERITS (sol);
CREATE TABLE sol_y2026 () INHERITS (sol);

ALTER TABLE sol_y2025 ADD CONSTRAINT ck_sol_y2025
  CHECK (created >= DATE '2025-01-01' AND created < DATE '2026-01-01');
ALTER TABLE sol_y2026 ADD CONSTRAINT ck_sol_y2026
  CHECK (created >= DATE '2026-01-01' AND created < DATE '2027-01-01');

ALTER TABLE sol ADD CONSTRAINT uq_sol_no UNIQUE (order_no);
ALTER TABLE sol_y2025 ADD CONSTRAINT uq_sol_y2025_no UNIQUE (order_no);
ALTER TABLE sol_y2026 ADD CONSTRAINT uq_sol_y2026_no UNIQUE (order_no);

第二步,验证 CHECK 继承与 UNIQUE 不跨表:

sql
-- CHECK 继承生效于子表
INSERT INTO sol_y2025 VALUES ('A-1', -5, '2025-03-01');
text
ERROR:  new row for relation "sol_y2025" violates check constraint "ck_sol_amount"
DETAIL:  Failing row contains (A-1, -5.00, 2025-03-01).
sql
-- 但跨年重复编号会成功
INSERT INTO sol_y2025 VALUES ('A-1', 100, '2025-03-01');
INSERT INTO sol_y2026 VALUES ('A-1', 200, '2026-03-01');

SELECT tableoid::regclass AS tbl, order_no, amount
FROM sol ORDER BY order_no, tbl::text;
text
    tbl     | order_no | amount
------------+----------+--------
 sol_y2025  | A-1      | 100.00
 sol_y2026  | A-1      | 200.00
(2 rows)

第三步,用「编号注册表」补齐全局唯一:

sql
DROP TABLE IF EXISTS order_no_registry;
CREATE TABLE order_no_registry (
  order_no text PRIMARY KEY,
  used_at  timestamptz NOT NULL DEFAULT now()
);

-- 分配的原子性由主键冲突保证
INSERT INTO order_no_registry (order_no) VALUES ('A-1');   -- 第一次 OK
INSERT INTO order_no_registry (order_no) VALUES ('A-1');   -- 第二次冲突
text
ERROR:  duplicate key value violates unique constraint "order_no_registry_pkey"
DETAIL:  Key (order_no)=(A-1) already exists.

第四步,核对索引与约束的实际分布:

sql
SELECT c.relname AS tbl,
       p.conname AS constraint,
       p.contype,
       p.connoinherit
FROM pg_constraint p
JOIN pg_class c ON c.oid = p.conrelid
WHERE c.relname IN ('sol', 'sol_y2025', 'sol_y2026')
  AND p.contype IN ('c', 'u', 'p')
ORDER BY c.relname, p.conname;
text
    tbl     |      constraint      | contype | connoinherit
------------+----------------------+---------+--------------
 sol        | ck_sol_amount        | c       | f
 sol        | uq_sol_no            | u       | f
 sol_y2025  | ck_sol_amount        | c       | f
 sol_y2025  | ck_sol_y2025         | c       | f
 sol_y2025  | uq_sol_y2025_no      | u       | f
 sol_y2026  | ck_sol_amount        | c       | f
 sol_y2026  | ck_sol_y2026         | c       | f
 sol_y2026  | uq_sol_y2026_no      | u       | f
(8 rows)

结果分析:

  • ck_sol_amount 在父表与两个子表各自出现——证明 CHECK 会被复制到子表。
  • uq_sol_no 只出现在 sol,两个子表各有自己的 uq_sol_yYYYY_no——证明唯一约束不继承、必须逐表创建。
  • 即便逐表都建了唯一约束,A-1 仍可跨年重复,因为没有任何单一索引能覆盖整棵树。
  • 注册表方案把「全局唯一」变成一次 PRIMARY KEY 冲突检查,可靠且并发安全,代价是多一次写入。

注意事项 ​

  • 错误!以为父表的 UNIQUE / PRIMARY KEY 能约束子表。 这是继承最著名的坑,父表约束只对本表生效。

    sql
    -- 错误认知:以为这能挡住子表重复
    ALTER TABLE sol ADD CONSTRAINT uq_sol_no UNIQUE (order_no);
  • 错误!把外键指向采用继承的父表。 外键只校验父表物理数据,子表数据被忽略,引用完整性出现空洞。

    sql
    -- 错误!子表中的 pid 无法被 fk 校验发现
    CREATE TABLE ref_t (pid int REFERENCES base_parent(id));
  • 错误!用 ALTER TABLE ONLY parent ADD CONSTRAINT ... CHECK 期望约束整棵树。 约束只加到父表。

    sql
    -- 不会传播;子表插入不受此约束
    ALTER TABLE ONLY pa2 ADD CONSTRAINT ck_pa2_only CHECK (v < 1000);
  • 错误!以为触发器会随继承传播。 触发器必须逐表创建,漏掉子表就有漏洞。

    sql
    CREATE TRIGGER trg ON ord2 ... ;   -- 只对 ord2 生效,子表没有
  • 错误!在子表上 DROP 继承来的约束或改继承列的 NOT NULL。 会被直接拒绝。

    sql
    -- 错误!
    ALTER TABLE ck2_child DROP CONSTRAINT ck2_v_range;
    ALTER TABLE nn_child ALTER COLUMN id DROP NOT NULL;
  • 注意 NO INHERIT 只能写在 CHECK 与 NOT NULL 上(NOT NULL 在 PG 18 中作为约束也有 NO INHERIT 语义),UNIQUE / PRIMARY KEY / FOREIGN KEY 没有「继承」这一说,自然也无从阻断。

  • 注意子表默认没有任何唯一索引。 迁移或重建子表后,务必核对每个子表是否都建了主键/唯一约束与索引。

  • 注意分区表的主键必须包含全部分区键列,这是硬限制,无法绕过。

最佳实践 ​

  • 把「唯一性」明确分层:单表内唯一用该表的 UNIQUE;跨子树(全局)唯一用独立注册表 + 唯一索引,或全局序列/UUID。

    sql
    CREATE TABLE order_no_registry (order_no text PRIMARY KEY, used_at timestamptz DEFAULT now());
  • 把可继承的通用约束放在父表(CHECK、NOT NULL),把不可继承的约束(唯一、外键、索引)写进「子表批量维护脚本」:

    sql
    SELECT format('ALTER TABLE %I ADD CONSTRAINT uq_%s_no UNIQUE (order_no);', c.relname, c.relname)
    FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid
    WHERE i.inhparent = 'sol'::regclass;
  • 不要用外键引用采用继承的父表。 若确有引用需求,改为引用具体的普通表,或改用声明式分区(PG 12+ 支持引用分区表)。

  • 用 connoinherit / contype 巡检约束分布,确认「该继承的确实继承了」「不该继承的每张表都补上了」:

    sql
    SELECT conrelid::regclass AS tbl, conname, contype, connoinherit
    FROM pg_constraint
    WHERE conrelid IN (SELECT inhrelid FROM pg_inherits WHERE inhparent = 'sol'::regclass)
       OR conrelid = 'sol'::regclass
    ORDER BY tbl::text, conname;
  • 触发器等每表对象要写进建表模板,新增子表时自动生成,避免遗漏;触发器做好并发评估(否则优先用注册表方案)。

  • 能用声明式分区就别用继承,尤其在需要唯一约束、外键、自动路由的场景——这些正是声明式分区相对继承补齐的能力。

  • 迁移继承到声明式分区时,把手工范围 CHECK 换成 FOR VALUES 分区边界,并给主键/唯一约束补齐分区键列:

    sql
    CREATE TABLE mig_p (
      id bigint, d date,
      PRIMARY KEY (id, d)     -- 必须包含分区键
    ) PARTITION BY RANGE (d);

下一节

继续学习:ONLY 关键字与查询

参考链接 ​