表继承概述
表继承(Table Inheritance)是 PostgreSQL 独有的一项数据建模能力:可以让一张表继承另一张表的全部列,从而在「公共属性」与「专有属性」之间形成父子(parent/child)层次。它由 INHERITS 子句声明,查询父表时默认会把所有后代表的行一并返回,用 ONLY 可以把范围收回到父表本身。
本节介绍继承的核心语义、它与声明式分区的历史渊源、继承与分区的适用边界,以及如何通过 pg_inherits 等系统目录观察继承结构。理解这些是后续正确使用继承、避免踩坑的前提。
前置知识
阅读本节前,建议先了解:创建表(CREATE TABLE)
基础概念
表继承是什么。 子表通过 INHERITS (parent) 获得父表的全部列定义(顺序也一致),并可以额外声明自己的列。子表在物理上是完全独立的表——有自己的数据文件、自己的索引、自己的约束,父子之间没有共享存储。
CREATE TABLE cities (
name text,
population real,
elevation int
);
CREATE TABLE capitals (
state char(2)
) INHERITS (cities);上面 capitals 自动拥有 name、population、elevation 三列,再加自己的 state。四列的顺序是「父表列在前,子表新增列在后」。
三个必须记住的事实:
| 事实 | 说明 | 后果 |
|---|---|---|
| 子表是独立的物理表 | 父子不共享存储、不共享索引 | 父表索引不会覆盖子表数据 |
| 父表查询默认向下递归 | 不写修饰符时自动包含所有后代 | 统计父表得到的是「整棵子树」的和 |
| 插入不会自动路由 | INSERT INTO parent 只写父表 | 插错表不会报错,但数据落在错误的层级 |
相关术语。
| 术语 | 含义 | 系统目录体现 |
|---|---|---|
| 父表(parent) | 被继承的表 | pg_inherits.inhparent |
| 子表(child) | 执行 INHERITS 的表 | pg_inherits.inhrelid |
| 直接子表 | 只隔一层 | pg_inherits 中的一条记录 |
| 后代表(descendant) | 任意深度的子/孙表 | 沿 pg_inherits 链递归 |
| 多重继承 | 一个子表有多个父表 | 同一 inhrelid 多条记录,inhseqno 排序 |
继承的实现基础。 PostgreSQL 在查询规划阶段把对父表的引用展开成对「父表 + 所有后代表」的 Append(附加)操作。因此继承并不是语法糖,而是规划器层面的一等特性;这也是为什么它天然支持在父表上做一次查询就能扫到全部子树。
语法与用法
继承相关的语法集中在 CREATE TABLE 与 ALTER TABLE 两条语句上:
-- 建表时声明继承
CREATE TABLE 子表 (
[ 子表专有列定义 ... ],
[ 表级约束 ... ]
) INHERITS (父表1 [, 父表2 ...]);
-- 给已存在的表追加/解除父关系(表结构必须兼容)
ALTER TABLE 子表 INHERIT 父表;
ALTER TABLE 子表 NO INHERIT 父表;
-- 引用父表时控制是否递归
SELECT ... FROM 父表; -- 默认:父表 + 全部后代
SELECT ... FROM ONLY 父表; -- 仅父表
SELECT ... FROM 父表 *; -- 显式表示包含后代(与默认等价)
-- 建一个结构与源表相同、便于后续 INHERIT 的表
CREATE TABLE 新表 (LIKE 源表 INCLUDING CONSTRAINTS);最简可运行示例——建父子表、插数据、观察默认递归与 ONLY:
CREATE TABLE cities (
name text,
population real,
elevation int
);
CREATE TABLE capitals (
state char(2)
) INHERITS (cities);
INSERT INTO cities VALUES ('Las Vegas', 670.0, 2174),
('Mariposa', 17.0, 1953);
INSERT INTO capitals VALUES ('Madison', 256.0, 845, 'WI');
-- 默认:包含 capitals
SELECT name, elevation FROM cities WHERE elevation > 500 ORDER BY name;
-- 仅父表
SELECT name, elevation FROM ONLY cities WHERE elevation > 500 ORDER BY name; name | elevation
-----------+-----------
Las Vegas | 2174
Madison | 845
Mariposa | 1953
(3 rows)
name | elevation
-----------+-----------
Las Vegas | 2174
Mariposa | 1953
(2 rows)第二次查询因为加了 ONLY,Madison(存在 capitals 中)不再出现。这说明默认递归是继承最需要时刻警惕的行为。
详细说明
什么是表继承:列合并规则
子表继承父表的列顺序与类型,并允许追加自己的列。多父表时,列取并集:
CREATE TABLE t_a (id int, a text);
CREATE TABLE t_b (b text);
CREATE TABLE t_c (
c text
) INHERITS (t_a, t_b);SELECT attnum, attname, atttypid::regtype
FROM pg_attribute
WHERE attrelid = 't_c'::regclass AND attnum > 0 AND NOT attisdropped
ORDER BY attnum; attnum | attname | atttypid
--------+---------+----------
1 | id | integer
2 | a | text
3 | b | text
4 | c | text
(4 rows)列合并有两条硬规则:
- 同名同类型会合并:若多个父表(或父表与子表定义)都有同名列,只要类型相同,就只保留一列;类型不同则直接报错。
- 合并后的
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)。
-- 类型冲突:合并失败
CREATE TABLE t_a2 (id int);
CREATE TABLE t_b2 (id bigint); -- 与 int 不同
CREATE TABLE t_bad () INHERITS (t_a2, t_b2);ERROR: column "id" has a type conflict
DETAIL: integer versus bigint父表查询默认向下递归
SELECT、UPDATE、DELETE 以及大多数 ALTER TABLE 变体,默认作用范围是父表及其所有后代。规划器会把它们展开成 Append 节点:
EXPLAIN (COSTS OFF) SELECT name FROM cities; Append
-> Seq Scan on cities cities_1
-> Seq Scan on capitals cities_2
(3 rows)注意别名被改写为 cities_1、cities_2:父表自身是 cities_1,capitals 被当作 cities_2。这是「父表视角」的直接证据。
UPDATE / DELETE 同样递归,这一点极其重要——一条看似只针对父表的 DELETE,可能删掉所有子表的数据:
DELETE FROM cities WHERE elevation > 2000; -- 会删 cities 与 capitals 中的行
-- 若只想删父表:
-- DELETE FROM ONLY cities WHERE elevation > 2000;不是所有命令都支持递归。文档明确指出:
| 命令类别 | 是否默认递归 | 是否支持 ONLY |
|---|---|---|
SELECT / UPDATE / DELETE | 是 | 支持 |
大多数 ALTER TABLE 变体 | 是 | 支持 |
VACUUM / ANALYZE | 是 | 支持 |
INSERT | 否(总是写指定表) | 不适用 |
ALTER TABLE ... RENAME | 否 | 不适用 |
REINDEX 等维护命令 | 否(只能针对单张物理表) | 不适用 |
ONLY 与 * 后缀:两种互补写法
ONLY 收窄到父表自身;* 显式表示「含后代」,与默认行为等价、仅为兼容旧版本保留:
-- 三者关系
SELECT * FROM ONLY cities; -- 只有 cities 自己
SELECT * FROM cities; -- 默认:cities + 全部后代
SELECT * FROM cities *; -- 显式写 *,与上一句等价ONLY 的使用位置很关键——它修饰的是表引用,而不是查询:
-- 正确:ONLY 紧贴表名
SELECT * FROM ONLY cities;
-- 也支持在 UPDATE/DELETE 的表引用处
UPDATE ONLY cities SET population = population * 1.1 WHERE name = 'Mariposa';
DELETE FROM ONLY cities WHERE name = 'Las Vegas';
-- 在 ALTER TABLE 中同样可用
ALTER TABLE ONLY cities ADD COLUMN note text;
ALTER TABLE ONLY cities DROP COLUMN note;-- ALTER TABLE 加列的影响范围
-- 不加 ONLY:列被加到 cities 及所有后代
-- 加 ONLY :只加到 cities,后代需要自己补关于 * 后缀:文档说明「Writing * is not necessary, since this behavior is always the default」,历史原因是 7.1 之前 sql_inheritance 参数可改变默认行为。该参数已在 PostgreSQL 10 中被移除(Release Notes: "Remove sql_inheritance server parameter"),所以在 PostgreSQL 18 上默认递归是不可关闭的语义。
表继承与声明式分区的历史关系
在 PostgreSQL 10 引入声明式分区(declarative partitioning)之前,继承是唯一的实现分区手段:
| 时期 | 手段 | 特点 |
|---|---|---|
| 10 之前 | 继承 + CHECK 约束 + 触发器/规则 | 完全手工,规划器只能靠 CHECK 做约束排除 |
| 10 及之后 | PARTITION BY 声明式分区 | 元数据驱动,规划期裁剪,约束/索引可级联 |
| 至今 | 两者并存 | 继承仍可用于「非分区」的建模需求 |
声明式分区在内部使用继承机制(分区表在 pg_inherits 中同样有记录),但对外提供额外保证:分区键约束自动生成、ATTACH/DETACH 原子化、分区上的唯一/主键约束可被分区表统一管理。文档在继承章节也说明:「Some functionality not implemented for inheritance hierarchies is implemented for declarative partitioning。」
-- 两者在 pg_inherits 中都有记录,可用同一张目录观察
SELECT parent.relname AS parent, child.relname AS child, i.inhseqno
FROM pg_inherits i
JOIN pg_class parent ON parent.oid = i.inhparent
JOIN pg_class child ON child.oid = i.inhrelid
WHERE parent.relnamespace = 'public'::regnamespace
ORDER BY parent, i.inhseqno;继承的典型用途
继承不是「面向对象建模的万能钥匙」,真正适合它的场景相对有限:
- 公共属性 + 少量差异:如
cities/capitals,或persons/employees。 - 多租户/多来源的「集合查询」:把结构相同的多张表建成同一父表的子表,从而一条 SQL 查询全部来源。
- 按时间或维度切分的旧式分区:在声明式分区不可用时(老版本、特殊需求)的替代方案。
- 日志/归档分层:
logs父表 +logs_2025、logs_2026子表,查询父表即查全部历史。 - 逻辑上聚合、物理上隔离的表族:如按区域隔离存储但需要统一报表。
-- 报表场景:多来源结构一致,父表上一条 SQL 汇总
CREATE TABLE events_all (
id bigint,
created_at timestamptz,
payload jsonb
);
CREATE TABLE events_web () INHERITS (events_all);
CREATE TABLE events_mobile () INHERITS (events_all);
SELECT date_trunc('day', created_at) AS d, count(*)
FROM events_all -- 自动包含 events_web 与 events_mobile
GROUP BY 1
ORDER BY 1 DESC;继承 vs 声明式分区:如何选择
| 维度 | 表继承 | 声明式分区 |
|---|---|---|
| 声明方式 | INHERITS (parent) | PARTITION BY RANGE/LIST/HASH |
| 分区键约束 | 需手工 CHECK | 自动生成 |
| 唯一约束传播 | 不传播(父子各自独立) | 分区表约束可覆盖全部分区 |
| 行的路由 | INSERT 不自动路由 | 自动路由到正确分区 |
| 裁剪优化 | 依赖 CHECK 约束排除 | 规划期分区裁剪,更强 |
ATTACH / DETACH | 手工 INHERIT / NO INHERIT(会加锁校验) | 有专门语法,DETACH ... CONCURRENTLY |
| 默认分区 | 需自建「兜底」子表并靠规则路由 | 可建 DEFAULT 分区 |
| 适用建议 | 建模需求(公共属性/表族) | 海量数据按维度切分 |
结论:要做分区,优先用声明式分区。 继承只应在「确实需要层次建模」或「有明确历史包袱」时使用。
继承的局限(务必逐条确认)
文档把继承的局限集中在 Caveats 一节,其中最重要的是索引与唯一约束不跨表:
- 唯一约束、主键不继承:父表
UNIQUE无法阻止子表出现与父表重复的值;子表默认甚至没有任何唯一约束。 - 外键约束不继承:父表的外键不会自动出现在子表上;反之,别人
REFERENCES父表也不会覆盖子表的值。 - 索引不继承:父表的索引不会覆盖子表数据,每个子表需要独立索引。
CHECK与NOT NULL会继承:这是唯二会向下传播的约束类型(可用NO INHERIT阻断)。- 删除父表受限:只要还有子表,父表就不能被
DROP(除非CASCADE)。 - 列不能单独从子表删除:继承来的列必须从父表层面操作。
- 权限只查父表:文档说明「Inherited queries perform access permission checks on the parent table only」——有父表
UPDATE权限即意味着能通过父表改子表数据,但直接改子表还需额外授权。 - 行级安全策略:父表的 RLS 策略会作用于来自子表的行;子表自己的策略只在该子表被直接引用时生效。
-- 著名陷阱演示:父表 UNIQUE 不约束子表
CREATE TABLE cities_u (name text UNIQUE, elevation int);
CREATE TABLE capitals_u () INHERITS (cities_u);
INSERT INTO cities_u VALUES ('Springfield', 500);
INSERT INTO capitals_u VALUES ('Springfield', 500); -- 成功!父表唯一约束管不到子表
SELECT name, tableoid::regclass FROM cities_u; name | tableoid
-------------+--------------
Springfield | cities_u
Springfield | capitals_u
(2 rows)用系统目录观察继承关系
pg_inherits 是继承关系的权威来源,pg_class.relhassubclass 提供快速判断,tableoid 提供行级来源:
\d+ pg_inherits Column | Type | ...
-----------------+------+----
inhrelid | oid | 子表/子索引 OID
inhparent | oid | 父表/父索引 OID
inhseqno | int4 | 多父时的列排列顺序,从 1 开始
inhdetachpending| bool | 分离中的分区为 true-- 列出某父表的直接子表
SELECT child.relname AS child, i.inhseqno
FROM pg_inherits i
JOIN pg_class child ON child.oid = i.inhrelid
WHERE i.inhparent = 'cities'::regclass
ORDER BY i.inhseqno;
-- 快速判断是否有子表
SELECT relname, relhassubclass FROM pg_class WHERE relname IN ('cities', 'capitals');
-- 行级来源:tableoid 转 regclass 后直接显示表名
SELECT tableoid::regclass AS src, name, elevation
FROM cities
WHERE elevation > 500
ORDER BY src, name; child | inhseqno
----------+----------
capitals | 1
(1 row)
relname | relhassubclass
----------+----------------
capitals | f
cities | t
(2 rows)
src | name | elevation
-----------+-----------+-----------
capitals | Madison | 845
cities | Las Vegas | 2174
cities | Mariposa | 1953
(3 rows)注意 capitals.relhassubclass = f(它没有子表),而 cities.relhassubclass = t。另外文档提到这个标志表示「has (or once had) children」,历史上存在过子表也可能保持为真。
遍历整棵继承树
pg_inherits 只记录直接父子关系,要拿到任意深度的后代需要递归查询:
CREATE TABLE c1 (id int);
CREATE TABLE c2 () INHERITS (c1);
CREATE TABLE c3 () INHERITS (c2); -- 孙表
WITH RECURSIVE tree AS (
SELECT 'c1'::regclass AS rel, 0 AS depth
UNION ALL
SELECT i.inhrelid::regclass, t.depth + 1
FROM tree t
JOIN pg_inherits i ON i.inhparent = t.rel
)
SELECT rel, depth FROM tree ORDER BY depth, rel::text; rel | depth
-----+-------
c1 | 0
c2 | 1
c3 | 2
(3 rows)这个递归模式是巡检「继承树有多深」「哪些表是叶子」的通用写法,也是把继承结构可视化、生成文档的基础:
-- 找出所有叶子表(没有子表)
SELECT rel::regclass AS leaf
FROM pg_class
WHERE relnamespace = 'public'::regnamespace
AND relkind = 'r'
AND NOT relhassubclass
AND EXISTS (SELECT 1 FROM pg_inherits i WHERE i.inhrelid = pg_class.oid);继承与类型系统的区别
SQL:1999 定义了「类型继承」,PostgreSQL 的表继承与它在多个方面不同,混用概念会带来错误的预期:
| 维度 | PostgreSQL 表继承 | SQL:1999 类型继承 |
|---|---|---|
| 继承对象 | 表(列集合) | 类型 |
| 子表能否直接查询父表列 | 能(列被物理复制) | 取决于类型系统实现 |
| 约束传播 | 仅 CHECK / NOT NULL | 由类型系统规定 |
| 是否要求区分子类型 | 不强制 | 通常要求 ONLY 语义 |
| 行是否属于父表 | 不属于,只在子表 | 概念上属于超类型 |
文档明确指出:「(SQL:1999 and later define a type inheritance feature, which differs in many respects from the features described here.)」因此不要把继承当成「面向对象的 extends」,它更接近物理表 + 查询展开的组合。
实战示例
场景:把一张混装的城市表拆成继承结构,并验证访问路径与数据来源。
第一步,建结构并插入数据:
DROP TABLE IF EXISTS capitals_demo, cities_demo;
CREATE TABLE cities_demo (
name text,
population real,
elevation int
);
CREATE TABLE capitals_demo (
state char(2)
) INHERITS (cities_demo);
INSERT INTO cities_demo VALUES
('Las Vegas', 670.0, 2174),
('Mariposa', 17.0, 1953),
('Fresno', 542.0, 308);
INSERT INTO capitals_demo VALUES
('Madison', 256.0, 845, 'WI'),
('Sacramento', 525.0, 30, 'CA');第二步,验证「默认递归 vs ONLY」的差别:
SELECT 'all' AS scope, count(*) FROM cities_demo
UNION ALL
SELECT 'only', count(*) FROM ONLY cities_demo; scope | count
-------+-------
all | 5
only | 3
(2 rows)第三步,验证数据来源与访问路径:
EXPLAIN (COSTS OFF)
SELECT name FROM cities_demo WHERE elevation > 500;
SELECT tableoid::regclass AS src, name, elevation
FROM cities_demo
WHERE elevation > 500
ORDER BY elevation DESC; Append
-> Seq Scan on cities_demo cities_demo_1
Filter: (elevation > 500)
-> Seq Scan on capitals_demo cities_demo_2
Filter: (elevation > 500)
(5 rows)
src | name | elevation
-------------+------------+-----------
cities_demo | Las Vegas | 2174
cities_demo | Mariposa | 1953
capitals_demo | Madison | 845
(3 rows)结果分析:
Append节点下有两条Seq Scan,分别对应父表与子表——继承查询在执行计划层面就是「多表并集」。tableoid::regclass精确告诉我们每一行来自哪张物理表,是排查「数据插错表」的第一手段。- 若把
WHERE elevation > 500改成只针对子表的条件(如state IS NOT NULL),父表分支仍会被扫描——因为父表没有state列,无法在父表分支上应用该条件,这也是继承相比原生分区在裁剪上的差距。
场景二:用 ALTER TABLE ... NO INHERIT 把子表从层次中摘出,验证查询范围随之变化。
ALTER TABLE capitals_demo NO INHERIT cities_demo;
SELECT count(*) FROM cities_demo; -- 只剩父表
SELECT count(*) FROM capitals_demo; -- 子表数据仍在,但已不属于层次
-- 重新挂回(表结构仍兼容)
ALTER TABLE capitals_demo INHERIT cities_demo; count
-------
3
(1 row)
count
-------
2
(1 row)摘出与重挂都不会搬动数据,只是增删 pg_inherits 中的一条记录;这也是老式分区「加/卸分区」的实现原理。
注意事项
错误!以为
INSERT INTO parent会自动把行放进子表。 继承不会做数据路由,插入永远写进指定的那张表,而且不会报错。sql-- 不推荐:以为插 cities 会按 state 自动进 capitals INSERT INTO cities (name, population, elevation) VALUES ('Albany', NULL, NULL); -- 这行永远留在 cities,不会进 capitals错误!依赖父表唯一约束保证整棵子树唯一。 父表
UNIQUE/PRIMARY KEY对子表完全无效。sql-- 错误认知:以为 cities_u.name 的 UNIQUE 能挡住 capitals_u 的重复 INSERT INTO capitals_u VALUES ('Springfield', 500); -- 实际会成功错误!以为父表索引能加速子表查询。 索引属于单张物理表;子表必须各自建索引。
sqlCREATE INDEX ON cities_demo (elevation); -- 只对 cities_demo 生效错误!只给父表授权就以为覆盖全部。 通过父表访问子表走父表权限检查,但直接访问子表需要子表自己的权限。授权时要把子表一起考虑。
注意
DELETE/UPDATE的默认递归范围。 生产环境做批量清理时优先写ONLY,确认范围后再决定是否放开。sql-- 危险:会删除整棵子树中符合条件的行 DELETE FROM cities_demo WHERE elevation < 100;注意父表不能被直接删除。 存在子表时需
DROP TABLE parent CASCADE(会连子表一起删)。sql_inheritance参数已不存在。 该参数在 PostgreSQL 10 已被移除,不要试图在postgresql.conf里配置它;默认递归是不可更改的语义。注意继承来的列不能在子表单独
DROP COLUMN,必须从父表层面操作。
最佳实践
需要分区就先选声明式分区,继承只用于真正的层次建模需求(公共属性 + 少量专有属性、结构一致的表族统一查询)。
始终显式决定递归范围。 查询父表时,明确写下
ONLY或依赖默认递归,并在代码评审中作为检查项;对维护类语句一律先ONLY。用
tableoid::regclass做数据来源审计,尤其在「一个父表下挂多张来源表」的场景:sqlSELECT tableoid::regclass AS src, count(*) FROM events_all GROUP BY 1 ORDER BY 2 DESC;为每个子表独立建索引,不要指望父表索引覆盖子树;如果查询总带统一条件,可考虑在父表上也建一份相同索引以统一维护脚本。
用
pg_inherits写继承结构巡检,把「未预期的父子关系」暴露出来:sqlSELECT p.relname AS parent, c.relname AS child FROM pg_inherits i JOIN pg_class p ON p.oid = i.inhparent JOIN pg_class c ON c.oid = i.inhrelid WHERE p.relkind = 'r' AND c.relkind = 'r' ORDER BY 1, 2;用
CREATE TABLE ... (LIKE parent INCLUDING CONSTRAINTS)构造兼容表,再ALTER TABLE ... INHERIT,避免手工比对列与CHECK约束。把「公共列」都放在父表,子表只加真正专有的列;否则默认递归查询会失去意义。
为继承表族写文档并配巡检 SQL,特别是「哪些表是兄弟、谁负责写入哪张表」——继承的坑几乎都来自「以为会自动路由/自动约束」。
下一节
继续学习:创建与使用继承表