继承与约束
约束在继承结构下的行为是分裂的:CHECK 约束与 NOT NULL 约束会自动被所有子表继承,而 UNIQUE、PRIMARY KEY、FOREIGN KEY 以及索引只作用于单张表,不会向子表传播。这个不对称是继承最著名、也最容易造成数据完整性事故的地方。
本节逐类分析约束在继承下的实际语义,重点讲清「父表唯一约束不约束子表集合」这个坑,并给出用触发器、NO INHERIT、迁移到声明式分区等手段的应对方案。
基础概念
约束传播速查表。 这是本节最核心的一张表:
| 约束类型 | 是否继承到子表 | 说明 |
|---|---|---|
CHECK | 是 | 可用 NO INHERIT 阻断;同名约束会被合并,条件必须一致 |
NOT NULL | 是 | 本质是列属性,随列一起复制 |
UNIQUE | 否 | 父表唯一性不覆盖子表;子表也不会自动获得唯一约束 |
PRIMARY KEY | 否 | 同上;复合主键也不传播 |
FOREIGN KEY | 否 | 引用方向与被引用方向都不传播 |
| 索引 | 否 | 索引属于单张物理表,子表需独立创建 |
-- 演示:父表 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 Table "public.c"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
id | integer | | |
val | integer | | |
Check constraints:
"p_val_check" CHECK (val > 0) -- 继承了父表的 CHECK
Indexes:
-- 没有任何索引,也没有唯一约束三个必须记住的结论:
| 结论 | 后果 |
|---|---|
父表 UNIQUE 不阻止子表出现与父表重复的值 | 整棵子树的「全局唯一」必须自己保证 |
| 子表默认没有任何唯一约束 | 子表内部也可能出现重复 |
| 外键不跨继承层次 | 引用方与被引用方都要单独处理 |
语法与用法
约束在继承下的相关语法:
-- 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 不继承:
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);ERROR: new row for relation "ck_child" violates check constraint "ck_val_positive"
DETAIL: Failing row contains (1, -5).-- 但父表的 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; src | id | val
-------------+----+-----
ck_child | 1 | 20
ck_parent | 1 | 10
(2 rows)详细说明
CHECK 约束:会继承,可放宽
父表的 CHECK 自动成为每个子表的 CHECK。子表可以另外加自己的 CHECK,但不能放宽父表的约束(除非在子表上用 NO INHERIT,或父表本身用 NO INHERIT 声明)。
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);ERROR: new row for relation "ck2_child" violates check constraint "ck2_v_range"
DETAIL: Failing row contains (200).子表可以追加更严格的约束:
ALTER TABLE ck2_child ADD CONSTRAINT ck2_v_even CHECK (v % 2 = 0);
INSERT INTO ck2_child VALUES (7); -- 通过父表 CHECK,但违反子表 CHECKERROR: new row for relation "ck2_child" violates check constraint "ck2_v_even"
DETAIL: Failing row contains (7).尝试在子表放宽父表约束是不允许的(不能用同名约束改写条件):
ALTER TABLE ck2_child DROP CONSTRAINT ck2_v_range;ERROR: cannot drop inherited constraint "ck2_v_range" of relation "ck2_child"要放宽,只能在父表上改,或让约束从一开始就 NO INHERIT。
NOT NULL:随列继承
NOT NULL 是列属性,父表上定义为 NOT NULL 的列,子表自动也是 NOT NULL;而且不能在子表单独取消:
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);ERROR: null value in column "id" of relation "nn_child" violates not-null constraint
DETAIL: Failing row contains (null, 1).ALTER TABLE nn_child ALTER COLUMN id DROP NOT NULL;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):
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'; 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.nameto beUNIQUEor aPRIMARY KEY, this would not stop thecapitalstable from having rows with names duplicating rows incities.
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 Table "public.up_child"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
id | integer | | not null |
-- 注意:没有 Indexes 段,子表根本没有主键索引注意子表连 NOT NULL 都没有显式标出是继承语义的自然结果——它继承了 id 的 NOT NULL(来自主键),但没有继承主键本身。子表就可以插入重复值:
INSERT INTO up_child VALUES (1), (1), (1); -- 全部成功
SELECT count(*) FROM up_child; count
-------
3
(1 row)给子表单独加唯一约束只能保证子表内部唯一,无法跨表:
ALTER TABLE up_child ADD CONSTRAINT uq_up_child_id UNIQUE (id);
INSERT INTO up_child VALUES (1); -- 现在被拦住ERROR: duplicate key value violates unique constraint "uq_up_child_id"
DETAIL: Key (id)=(1) already exists.但父表仍可插入相同的 id:
INSERT INTO up_parent VALUES (1); -- 成功!父表只跟自己比INSERT 0 1结果就是「子表有唯一约束、父表有主键,但两边各存了一个 id = 1」——任何通过父表的查询都会看到两条 id 相同的行。
父表唯一约束不约束子表集合:著名坑
把上一条推到极端:如果希望「整棵子树里 order_no 唯一」,没有任何声明式手段能做到。
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); -- 成功!跨年重复SELECT tableoid::regclass AS tbl, order_no, amount
FROM ord ORDER BY order_no, tbl::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 触发器查询父表 ∪ 所有子表 | 并发下有竞争,性能差 |
| 迁移到声明式分区 | 分区表的唯一约束可覆盖全部分区(须含分区键) | 需要改造表结构 |
声明式分区下的行为对比:
-- 声明式分区:唯一约束可以覆盖所有分区,但必须包含分区键
CREATE TABLE orders_p (
order_no text,
created date,
PRIMARY KEY (order_no, created) -- 必须含分区键 created
) PARTITION BY RANGE (created);-- 若只写 PRIMARY KEY (order_no) 会报错:
ERROR: unique constraint on partitioned table must include all partitioning columns换句话说,即便换成声明式分区,「全局唯一」也只能在「唯一键包含分区键」时才能声明式保证。这也是为什么很多系统的业务编号唯一性依赖外部手段。
外键约束:两个方向都不继承
外键的限制分为「引用方」与「被引用方」两种,文档都明确点名:
方向一:父表的外键不会传播到子表。
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 父表,不会覆盖子表。
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);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.」
这带来一个重要的实践推论:不要用外键指向一张采用继承的父表,因为你会失去对子表数据的引用完整性。
父表索引不被子表继承
索引与唯一约束一样,属于单张物理表:
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; tbl | idx
------------+--------------------
idx_parent | idx_parent_created
(1 row)子表一行索引都没有。查询 WHERE created = ... 走父表时会扫描子表,但没有可用索引:
EXPLAIN (COSTS OFF) SELECT * FROM idx_parent WHERE created = DATE '2025-01-01'; 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)修复方式是给每个子表建相同索引——这也是老式分区维护脚本必须包含的一步:
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,可以让它只约束本表,不传播到子表。这是「子表需要放宽条件」的合法途径:
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); -- 被拦ERROR: new row for relation "ni_parent" violates check constraint "ck_ni_small"
DETAIL: Failing row contains (500).用系统目录确认 NO INHERIT 已生效:
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; 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 则传播到整棵子树:
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; tbl | conname
-------+---------
ch2 | ck_pa2
pa2 | ck_pa2
(2 rows)-- 只加到父表,子表不受影响
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'; tbl | conname
-------+-------------
pa2 | ck_pa2_only
(1 row)这里有个陷阱:用 ONLY 加的约束若在父表上被违反,子表插入却不会检查它——父表的约束检查只在本表插入时执行。所以「用 ALTER TABLE ONLY 给父表加约束、期望整棵树受约束」是错误预期。
用触发器替代唯一性约束
当必须保证「跨子表唯一」时,只能靠触发器。示例:保证 order_no 在父表 ∪ 所有子表中唯一。
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();必须注意:触发器只加在父表上是不生效的——继承不会传播触发器。若子表可能被直接写入,触发器要逐表创建:
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 | 正常继承 |
迁移验证示例:
-- 继承结构
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');-- 若忽略分区键会报错:
ERROR: unique constraint on partitioned table must include all partitioning columns另外,继承结构里那些「手工 CHECK 范围约束」在声明式分区下无需保留——分区边界由 FOR VALUES 自动表达,且规划器用 relpartbound 做裁剪,比 CHECK 约束排除更强、更可靠。
实战示例
场景:在一个按年继承的订单结构中,验证各类约束的真实行为,并用注册表方案补齐全局唯一。
第一步,搭结构与现有约束:
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 不跨表:
-- CHECK 继承生效于子表
INSERT INTO sol_y2025 VALUES ('A-1', -5, '2025-03-01');ERROR: new row for relation "sol_y2025" violates check constraint "ck_sol_amount"
DETAIL: Failing row contains (A-1, -5.00, 2025-03-01).-- 但跨年重复编号会成功
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; tbl | order_no | amount
------------+----------+--------
sol_y2025 | A-1 | 100.00
sol_y2026 | A-1 | 200.00
(2 rows)第三步,用「编号注册表」补齐全局唯一:
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'); -- 第二次冲突ERROR: duplicate key value violates unique constraint "order_no_registry_pkey"
DETAIL: Key (order_no)=(A-1) already exists.第四步,核对索引与约束的实际分布:
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; 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);错误!以为触发器会随继承传播。 触发器必须逐表创建,漏掉子表就有漏洞。
sqlCREATE 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。sqlCREATE TABLE order_no_registry (order_no text PRIMARY KEY, used_at timestamptz DEFAULT now());把可继承的通用约束放在父表(
CHECK、NOT NULL),把不可继承的约束(唯一、外键、索引)写进「子表批量维护脚本」:sqlSELECT 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巡检约束分布,确认「该继承的确实继承了」「不该继承的每张表都补上了」:sqlSELECT 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分区边界,并给主键/唯一约束补齐分区键列:sqlCREATE TABLE mig_p ( id bigint, d date, PRIMARY KEY (id, d) -- 必须包含分区键 ) PARTITION BY RANGE (d);
下一节
继续学习:ONLY 关键字与查询