Skip to content

表继承概述 ​

表继承(Table Inheritance)是 PostgreSQL 独有的一项数据建模能力:可以让一张表继承另一张表的全部列,从而在「公共属性」与「专有属性」之间形成父子(parent/child)层次。它由 INHERITS 子句声明,查询父表时默认会把所有后代表的行一并返回,用 ONLY 可以把范围收回到父表本身。

本节介绍继承的核心语义、它与声明式分区的历史渊源、继承与分区的适用边界,以及如何通过 pg_inherits 等系统目录观察继承结构。理解这些是后续正确使用继承、避免踩坑的前提。

前置知识

阅读本节前,建议先了解:创建表(CREATE TABLE)

基础概念 ​

表继承是什么。 子表通过 INHERITS (parent) 获得父表的全部列定义(顺序也一致),并可以额外声明自己的列。子表在物理上是完全独立的表——有自己的数据文件、自己的索引、自己的约束,父子之间没有共享存储。

sql
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 两条语句上:

text
-- 建表时声明继承
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:

sql
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;
text
   name    | elevation
-----------+-----------
 Las Vegas |      2174
 Madison   |       845
 Mariposa  |      1953
(3 rows)

   name    | elevation
-----------+-----------
 Las Vegas |      2174
 Mariposa  |      1953
(2 rows)

第二次查询因为加了 ONLY,Madison(存在 capitals 中)不再出现。这说明默认递归是继承最需要时刻警惕的行为。

详细说明 ​

什么是表继承:列合并规则 ​

子表继承父表的列顺序与类型,并允许追加自己的列。多父表时,列取并集:

sql
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);
sql
SELECT attnum, attname, atttypid::regtype
FROM pg_attribute
WHERE attrelid = 't_c'::regclass AND attnum > 0 AND NOT attisdropped
ORDER BY attnum;
text
 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)。
sql
-- 类型冲突:合并失败
CREATE TABLE t_a2 (id int);
CREATE TABLE t_b2 (id bigint);       -- 与 int 不同
CREATE TABLE t_bad () INHERITS (t_a2, t_b2);
text
ERROR:  column "id" has a type conflict
DETAIL:  integer versus bigint

父表查询默认向下递归 ​

SELECT、UPDATE、DELETE 以及大多数 ALTER TABLE 变体,默认作用范围是父表及其所有后代。规划器会把它们展开成 Append 节点:

sql
EXPLAIN (COSTS OFF) SELECT name FROM cities;
text
 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,可能删掉所有子表的数据:

sql
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 收窄到父表自身;* 显式表示「含后代」,与默认行为等价、仅为兼容旧版本保留:

sql
-- 三者关系
SELECT * FROM ONLY cities;   -- 只有 cities 自己
SELECT * FROM cities;        -- 默认:cities + 全部后代
SELECT * FROM cities *;      -- 显式写 *,与上一句等价

ONLY 的使用位置很关键——它修饰的是表引用,而不是查询:

sql
-- 正确: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;
text
-- 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。」

sql
-- 两者在 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
-- 报表场景:多来源结构一致,父表上一条 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 策略会作用于来自子表的行;子表自己的策略只在该子表被直接引用时生效。
sql
-- 著名陷阱演示:父表 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;
text
    name     |   tableoid
-------------+--------------
 Springfield | cities_u
 Springfield | capitals_u
(2 rows)

用系统目录观察继承关系 ​

pg_inherits 是继承关系的权威来源,pg_class.relhassubclass 提供快速判断,tableoid 提供行级来源:

sql
\d+ pg_inherits
text
     Column      | Type | ...
-----------------+------+----
 inhrelid        | oid  | 子表/子索引 OID
 inhparent       | oid  | 父表/父索引 OID
 inhseqno        | int4 | 多父时的列排列顺序,从 1 开始
 inhdetachpending| bool | 分离中的分区为 true
sql
-- 列出某父表的直接子表
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;
text
  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 只记录直接父子关系,要拿到任意深度的后代需要递归查询:

sql
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;
text
 rel | depth
-----+-------
 c1  |     0
 c2  |     1
 c3  |     2
(3 rows)

这个递归模式是巡检「继承树有多深」「哪些表是叶子」的通用写法,也是把继承结构可视化、生成文档的基础:

sql
-- 找出所有叶子表(没有子表)
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」,它更接近物理表 + 查询展开的组合。

实战示例 ​

场景:把一张混装的城市表拆成继承结构,并验证访问路径与数据来源。

第一步,建结构并插入数据:

sql
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」的差别:

sql
SELECT 'all' AS scope, count(*) FROM cities_demo
UNION ALL
SELECT 'only', count(*) FROM ONLY cities_demo;
text
 scope | count
-------+-------
 all   |     5
 only  |     3
(2 rows)

第三步,验证数据来源与访问路径:

sql
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;
text
 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 把子表从层次中摘出,验证查询范围随之变化。

sql
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;
text
 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);   -- 实际会成功
  • 错误!以为父表索引能加速子表查询。 索引属于单张物理表;子表必须各自建索引。

    sql
    CREATE 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 做数据来源审计,尤其在「一个父表下挂多张来源表」的场景:

    sql
    SELECT tableoid::regclass AS src, count(*) FROM events_all GROUP BY 1 ORDER BY 2 DESC;
  • 为每个子表独立建索引,不要指望父表索引覆盖子树;如果查询总带统一条件,可考虑在父表上也建一份相同索引以统一维护脚本。

  • 用 pg_inherits 写继承结构巡检,把「未预期的父子关系」暴露出来:

    sql
    SELECT 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,特别是「哪些表是兄弟、谁负责写入哪张表」——继承的坑几乎都来自「以为会自动路由/自动约束」。

下一节

继续学习:创建与使用继承表

参考链接 ​