继承
PostgreSQL 表继承允许子表继承父表的列,并让查询父表时默认包含子表数据。它与面向对象语言的类继承不同,也不等于关系模型中的“父类型/子类型”设计。
表继承是 PostgreSQL 专有特性,存在明显的约束传播和可移植性限制。现代项目通常优先使用普通主外键建模;需要拆分大表时优先使用声明式分区。
关于关系建模中的三种父子类型方案,可先阅读继承概念。本文集中讲 PostgreSQL 的 INHERITS 功能。
创建继承表
CREATE TABLE cities (
city_id BIGINT GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL,
population BIGINT NOT NULL CHECK (population >= 0),
CONSTRAINT pk_cities PRIMARY KEY (city_id)
);
创建一个额外保存海拔的子表:
CREATE TABLE capitals (
state TEXT NOT NULL,
elevation_m INTEGER
) INHERITS (cities);
capitals 会拥有自己的列以及继承来的 city_id、name 和 population。
插入子表:
INSERT INTO capitals (name, population, state, elevation_m)
VALUES ('示例城', 500000, '示例州', 120);
查询父表和子表
查询父表默认也会读取所有后代子表:
SELECT name, population
FROM cities;
只读取父表自身,使用 ONLY:
SELECT name, population
FROM ONLY cities;
查询子表只会返回该子表及其后代,不会自动包含父表的行:
SELECT name, state
FROM capitals;
可以用 tableoid 查看实际来源:
SELECT tableoid::regclass AS source_table, name
FROM cities;
修改和删除的范围
UPDATE、DELETE 与 SELECT 类似,作用于父表时默认会包括后代表:
UPDATE cities
SET population = population + 1000;
只修改父表自身:
UPDATE ONLY cities
SET population = population + 1000;
这种隐式范围是继承最容易造成误操作的地方之一。维护脚本应明确是否需要 ONLY,并在事务中先用同样条件查询目标行数。
列如何继承
子表继承父表的列。若多个父表或子表自身定义了同名且兼容的列,PostgreSQL 会合并定义;类型冲突则会拒绝创建。
CREATE TABLE regional_capitals (
region_code TEXT NOT NULL
) INHERITS (capitals, regional_entities);
多重继承虽然可用,但列来源、约束合并和后续修改会迅速变复杂。除非确有清晰模型和充分测试,不应仅因为语法支持就采用。
父表新增、删除或修改列时,变更可能传播到子表。子表也可拥有自己的额外列。执行结构迁移前应列出整个继承树,并核对目标 PostgreSQL 版本的传播规则。
约束继承并不完整
继承最重要的限制是:子表不会自动获得父表全部数据完整性规则。
通常会随列定义传播的包括:
NOT NULL;- 可继承的
CHECK约束。
不会像面向对象成员一样完整传播的包括:
- 主键;
- 唯一约束;
- 外键;
- 普通索引;
- 触发器;
- 行级安全策略与权限。
前面的 pk_cities 不会自动保证 capitals.city_id 与父表及所有兄弟子表全局唯一。即使每张表分别有唯一索引,也只能保证各表内部唯一。
这也是表继承不适合用来实现“所有子类型共享一个全局主键约束”的主要原因。
CHECK 约束传播
父表的普通 CHECK 约束会继承到子表,除非声明为不继承:
CHECK (population >= 0) NO INHERIT
同名且表达式兼容的继承检查约束可能合并。修改或删除父表约束时,要确认子表约束来自继承还是本地定义。
标识列、默认值和生成列
这些列属性与表继承的交互并不完全等同于普通列:默认值可能被子表覆盖,标识属性与生成表达式的继承/执行规则也具有版本细节。
设计继承层次时不要凭“子表继承列,所以一切属性都相同”推断。应在目标版本中使用 \d+ 或系统目录查看子表实际定义,并测试插入行为。
添加和移除继承关系
已有表可添加父表:
ALTER TABLE capitals
INHERIT cities;
子表必须已经具有兼容列和约束。移除关系:
ALTER TABLE capitals
NO INHERIT cities;
移除继承不会自动删除子表已有列或数据。变更前应检查查询父表的应用是否依赖子表行自动出现。
表继承与关系建模
若业务上有“人员分为员工和客户”,通常更清晰的设计是共享主表加一对一子表:
CREATE TABLE persons (
person_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
display_name TEXT NOT NULL
);
CREATE TABLE employees (
person_id BIGINT PRIMARY KEY
REFERENCES persons (person_id),
employee_number TEXT NOT NULL UNIQUE
);
这种设计使用标准主键和外键,能明确保证全局身份,也更容易被 ORM、迁移工具和其他数据库支持。
表继承与声明式分区
过去常用继承、检查约束和触发器手工实现分区。现代 PostgreSQL 的声明式分区提供:
- 自动路由插入;
- 明确的范围、列表和哈希分区键;
- 分区裁剪;
- 更一致的索引和约束管理;
- 专门的挂载、分离和维护语法。
新项目需要按数据范围拆表时,应优先使用 PARTITION BY,不要重新搭建继承式分区。
何时可能考虑表继承
表继承更适合以下少数情况:
- 项目确定长期绑定 PostgreSQL;
- 需要从父表查询结构相似的多个实体集合;
- 已充分接受跨子表唯一性和外键限制;
- 数据模型、权限和运维工具都能识别继承层次;
- 声明式分区与普通关系建模都不符合需求。
采用前应制作最小原型,实际测试 DDL 传播、权限、触发器、RLS、备份恢复和 ORM 行为。
常见误区
子表会继承父表主键和唯一性
不会完整继承。跨整个继承树的全局唯一性尤其难以用普通索引保证。
查询父表只返回父表自己的行
默认会包含后代表。需要只查父表时使用 ONLY。
表继承等于声明式分区
声明式分区基于专门的分区语义和维护命令。普通继承没有完整的自动路由与约束管理能力。
表继承是面向对象继承的数据库版本
两者语义和约束体系不同。关系模型通常通过主键、外键和组合表达类型关系。
使用检查清单
- 普通主外键建模是否已经足够;
- 需求是否其实是声明式分区;
- 查询、更新和删除父表时是否明确
ONLY范围; - 主键、唯一、外键、索引、触发器、权限和 RLS 是否在每个子表正确配置;
- 是否需要跨子表的全局唯一性;
- DDL 变更对整个继承树的传播是否经过测试;
- ORM、备份、复制和迁移工具是否支持;
- 团队是否接受 PostgreSQL 专用设计和长期维护成本。
表继承提供了独特能力,但它的限制比语法表面更重要。只有在确实需要父表查询包含多张异构子表,并能承担完整性与工具链成本时,才值得选择。