创建与使用继承表
在理解了继承的基本语义后,本节进入实操:如何完整地创建一张父表与多张子表,向父子表插入数据时行为有何差异,如何查询、如何用 ONLY 收窄范围、如何借助 tableoid 追溯数据来源,以及如何用 ALTER TABLE ... INHERIT / NO INHERIT 动态调整继承关系。
这些操作组合起来就是老式分区(legacy partitioning)的完整工具箱。掌握它们,既能正确使用继承,也能读懂现存系统中大量基于继承的历史设计。
前置知识
阅读本节前,建议先了解:表继承概述、创建表(CREATE TABLE)
基础概念
建表时的继承声明。 子表在 CREATE TABLE 中通过 INHERITS (parent) 声明,可继承一个或多个父表。父表必须已存在,且子表不能重复声明与父表同名的列(除非类型兼容,会被合并)。
CREATE TABLE measurements (
city_id int,
logdate date,
peaktemp int
);
CREATE TABLE measurements_y2025 (
note text
) INHERITS (measurements);插入语义:绝不路由。
| 语句 | 行为 | 是否影响其他表 |
|---|---|---|
INSERT INTO parent | 只写父表 | 否 |
INSERT INTO child | 只写子表 | 否 |
INSERT INTO parent 带子表专有列 | 直接报错(列不存在) | — |
COPY parent FROM ... | 只写父表 | 否 |
-- 试图把带 note 的行插进父表
INSERT INTO measurements (city_id, logdate, peaktemp, note) VALUES (1, '2025-01-01', 20, 'x');
ERROR: column "note" of relation "measurements" does not exist即使把 note 去掉、插入成功,数据也留在 measurements,不会「落到」拥有 note 列的 measurements_y2025。
查询语义:默认向下递归。
| 写法 | 作用范围 |
|---|---|
SELECT * FROM measurements | 父表 + 全部后代表 |
SELECT * FROM ONLY measurements | 仅父表 |
SELECT * FROM measurements * | 显式含后代(与默认等价) |
行来源识别。 每条行都有一个隐含的系统列 tableoid,可直接与 pg_class.oid 连接,或用 ::regclass 转换显示表名。
语法与用法
继承表的完整语法涉及建表、改写关系、查询三部分:
-- 1. 建父表
CREATE TABLE 父表 ( 公共列定义…, 公共约束… );
-- 2. 建子表(继承父表列 + 追加专有列)
CREATE TABLE 子表 (
专有列定义…,
[ 子表级 CHECK 约束… ]
) INHERITS (父表);
-- 3. 动态增删父子关系(表结构必须兼容)
ALTER TABLE 子表 INHERIT 父表;
ALTER TABLE 子表 NO INHERIT 父表;
-- 4. 已有表要变成子表时,先用 LIKE 复制结构
CREATE TABLE 新子表 (LIKE 父表 INCLUDING CONSTRAINTS);
-- 5. 查询范围控制
SELECT … FROM 父表; -- 递归
SELECT … FROM ONLY 父表; -- 不递归
SELECT … FROM 父表 *; -- 显式递归
-- 6. 行来源
SELECT tableoid, tableoid::regclass, … FROM 父表;最简可运行示例——父子表建好、插入、验证默认递归:
DROP TABLE IF EXISTS measurements_y2025, measurements;
CREATE TABLE measurements (
city_id int,
logdate date,
peaktemp int
);
CREATE TABLE measurements_y2025 () INHERITS (measurements);
INSERT INTO measurements VALUES (1, '2025-01-01', 20);
INSERT INTO measurements_y2025 VALUES (2, '2025-06-15', 31);
SELECT tableoid::regclass AS src, city_id, logdate, peaktemp
FROM measurements
ORDER BY logdate; src | city_id | logdate | peaktemp
------------------------+---------+------------+----------
measurements | 1 | 2025-01-01 | 20
measurements_y2025 | 2 | 2025-06-15 | 31
(2 rows)详细说明
完整建表示例:父表 + 多子表
一个贴近生产的例子:按年切分的测量表。父表放公共列与公共约束,子表只加自己的元信息:
DROP TABLE IF EXISTS meas_y2026, meas_y2025, meas_y2024, meas CASCADE;
CREATE TABLE meas (
city_id int NOT NULL,
logdate date NOT NULL,
peaktemp int,
CONSTRAINT ck_peaktemp_range CHECK (peaktemp IS NULL OR peaktemp BETWEEN -100 AND 100)
);
CREATE TABLE meas_y2024 () INHERITS (meas);
CREATE TABLE meas_y2025 () INHERITS (meas);
CREATE TABLE meas_y2026 () INHERITS (meas);
-- 每个子表加自己的「数据范围」检查约束,用于老式分区的约束排除
ALTER TABLE meas_y2024 ADD CONSTRAINT ck_y2024
CHECK (logdate >= DATE '2024-01-01' AND logdate < DATE '2025-01-01');
ALTER TABLE meas_y2025 ADD CONSTRAINT ck_y2025
CHECK (logdate >= DATE '2025-01-01' AND logdate < DATE '2026-01-01');
ALTER TABLE meas_y2026 ADD CONSTRAINT ck_y2026
CHECK (logdate >= DATE '2026-01-01' AND logdate < DATE '2027-01-01');观察结构:
\d meas Table "public.meas"
Column | Type | Collation | Nullable | Default
----------+---------+-----------+----------+---------
city_id | integer | | not null |
logdate | date | | not null |
peaktemp | integer | | |
Check constraints:
"ck_peaktemp_range" CHECK (peaktemp IS NULL OR peaktemp >= '-100'::integer AND peaktemp <= 100)
Number of child tables: 3 (Use \d+ to list them.)\d 的输出末尾会明确列出子表数量——这是确认继承关系是否建立成功最直观的方式。每个子表的 CHECK 约束独立存在,互不影响。
向父表与子表插入的语义
插入永远只作用于指定表,这是继承最容易误解的一点:
-- 插入父表:数据留在父表
INSERT INTO meas (city_id, logdate, peaktemp) VALUES (10, '2023-12-31', 5);
-- 插入子表:数据留在子表
INSERT INTO meas_y2025 (city_id, logdate, peaktemp) VALUES (20, '2025-03-01', 18);SELECT tableoid::regclass AS src, city_id, logdate
FROM meas ORDER BY logdate; src | city_id | logdate
----------------+---------+------------
meas | 10 | 2023-12-31
meas_y2025 | 20 | 2025-03-01
(2 rows)注意 2023-12-31 这行没有被路由到任何一年的子表,因为它本来就该进父表(父表才是那张「没有 CHECK 约束」的兜底表)。父表的 CHECK 只约束 peaktemp,不约束 logdate,所以任何日期都能进父表。
子表的 CHECK 约束会真的拦截越界数据:
INSERT INTO meas_y2025 (city_id, logdate, peaktemp) VALUES (30, '2024-05-01', 22);ERROR: new row for relation "meas_y2025" violates check constraint "ck_y2025"
DETAIL: Failing row contains (30, 2024-05-01, 22).这个约束保护在直接插子表时有效;但通过父表插入不会被任何子表约束检查——这正是继承式分区的最大隐患,也是为什么需要规则或触发器来重定向。
查询父表返回所有子表数据
默认递归是继承查询的核心能力,它让「一张父表 = 整个表族」:
SELECT count(*) FROM meas; -- 全部
SELECT count(*) FROM ONLY meas; -- 仅父表 count
-------
2
(1 row)
count
-------
1
(1 row)看执行计划可以验证规划器如何展开:
EXPLAIN (COSTS OFF) SELECT count(*) FROM meas; Aggregate
-> Append
-> Seq Scan on meas meas_1
-> Seq Scan on meas_y2024 meas_2
-> Seq Scan on meas_y2025 meas_3
-> Seq Scan on meas_y2026 meas_4
(6 rows)四个分支说明「父表 + 三个子表」都被纳入。这是继承查询的实际代价:即使子表为空,也要访问它们(空表扫描很便宜,但仍是规划与执行开销)。
ONLY 关键字:收窄到父表
SELECT / UPDATE / DELETE / 多数 ALTER TABLE 变体都支持 ONLY:
-- 只读父表
SELECT * FROM ONLY meas;
-- 只更新父表
UPDATE ONLY meas SET peaktemp = 6 WHERE city_id = 10;
-- 只删父表
DELETE FROM ONLY meas WHERE city_id = 10;
-- 只改父表结构(不传播给子表)
ALTER TABLE ONLY meas ADD COLUMN quality text;-- 注意:ALTER TABLE ONLY meas ADD COLUMN quality text;
-- 之后子表并没有 quality 列!这一点与「不加 ONLY 的 ALTER TABLE」形成鲜明对比——后者会把新列加到整棵子树。加 ONLY 后子表缺列,会导致父表查询报错:
SELECT quality FROM meas;ERROR: column "quality" does not exist原因:父表有这个列,子表没有,展开成 Append 时无法统一输出列集合。这解释了为什么 ALTER TABLE ONLY ... ADD COLUMN 在继承结构上几乎总是坏主意。
子表 CHECK 约束区分条件(老式分区)
老式分区的性能依赖规划器的约束排除(constraint exclusion):如果查询条件与子表的 CHECK 约束矛盾,该子表可被跳过。
SET constraint_exclusion = on; -- 让规划器利用 CHECK 约束排除
EXPLAIN (COSTS OFF)
SELECT * FROM meas WHERE logdate >= '2025-01-01' AND logdate < '2026-01-01'; Append
-> Seq Scan on meas meas_1
Filter: ((logdate >= '2025-01-01'::date) AND (logdate < '2026-01-01'::date))
-> Seq Scan on meas_y2025 meas_3
Filter: ((logdate >= '2025-01-01'::date) AND (logdate < '2026-01-01'::date))
(4 rows)meas_y2024 与 meas_y2026 被跳过,因为它们各自的 CHECK 与查询条件矛盾。但 meas(父表)无法被排除——它没有 logdate 范围的约束,可能含任意日期。这就是老式分区必须准备「兜底表」引发的代价:兜底表永远要扫。
-- 查看约束排除的配置
SHOW constraint_exclusion; constraint_exclusion
----------------------
partition
(1 row)父表 DEFAULT 的继承行为
父表列的 DEFAULT 会被子表继承(作为列的默认值复制到子表定义中):
DROP TABLE IF EXISTS d_parent, d_child CASCADE;
CREATE TABLE d_parent (
id int,
created_at timestamptz NOT NULL DEFAULT now(),
status text DEFAULT 'new'
);
CREATE TABLE d_child () INHERITS (d_parent);
\d d_child Table "public.d_child"
Column | Type | Nullable | Default
------------+--------------------------+----------+---------
id | integer | |
created_at | timestamp with time zone | not null | now()
status | text | | 'new'::text子表拿到了 now() 与 'new' 两个默认值。若之后修改父表的默认值,只有新加的列或之后显式同步的操作才会传播——ALTER TABLE parent ALTER COLUMN ... SET DEFAULT 会向下传播到子表:
ALTER TABLE d_parent ALTER COLUMN status SET DEFAULT 'draft';
SELECT table_name, column_default
FROM information_schema.columns
WHERE table_name IN ('d_parent', 'd_child') AND column_name = 'status'
ORDER BY table_name; table_name | column_default
------------+----------------
d_child | 'draft'::text
d_parent | 'draft'::text
(2 rows)同名同类型列的 DEFAULT 合并规则与列合并一致:子表若自己定义了不同默认值,以子表定义为准;父表后续的 ALTER 会覆盖子表的默认值。
INSERT ... RETURNING 与 tableoid
RETURNING 可以返回 tableoid,从而在「不知道数据进了哪张表」时立刻得知结果:
INSERT INTO meas (city_id, logdate, peaktemp)
VALUES (99, '2025-07-07', 25)
RETURNING tableoid::regclass AS inserted_into, city_id, logdate; inserted_into | city_id | logdate
---------------+---------+------------
meas | 99 | 2025-07-07
(1 row)如果你期望它进 meas_y2025,这个输出就是「没路由」的直接证据。批量写入时可以用 tableoid 做审计:
WITH ins AS (
INSERT INTO meas (city_id, logdate, peaktemp)
SELECT city_id, logdate, peaktemp FROM staging_meas
RETURNING tableoid::regclass AS tgt
)
SELECT tgt, count(*) FROM ins GROUP BY tgt;如果只有一行(meas),说明全部数据都堆进了父表——这是老式分区最常见的生产事故。
ALTER TABLE ... INHERIT / NO INHERIT
动态调整继承关系是继承式分区的「加/卸分区」手段。要求新子表结构兼容:同名同类型的列、同名同表达式的 CHECK 约束。
-- 准备一张兼容表
CREATE TABLE meas_y2027 (LIKE meas INCLUDING CONSTRAINTS);
ALTER TABLE meas_y2027 ADD CONSTRAINT ck_y2027
CHECK (logdate >= DATE '2027-01-01' AND logdate < DATE '2028-01-01');
-- 挂入继承树
ALTER TABLE meas_y2027 INHERIT meas;
-- 摘出继承树(数据与表都保留)
ALTER TABLE meas_y2027 NO INHERIT meas;若结构不兼容会报错:
CREATE TABLE bad_child (city_id bigint, logdate date, peaktemp int);
ALTER TABLE bad_child INHERIT meas;ERROR: child table is missing column "city_id"INHERIT 会对父子表加锁并扫描子表验证约束兼容性,大表上要安排在维护窗口。
-- 用系统目录确认 INHERIT / NO INHERIT 的结果
SELECT child.relname
FROM pg_inherits i
JOIN pg_class child ON child.oid = i.inhrelid
WHERE i.inhparent = 'meas'::regclass
ORDER BY child.relname;UPDATE / DELETE 的影响范围
这是继承最危险的部分:不加 ONLY 时,UPDATE / DELETE 会作用于整棵子树。
-- 危险:会改到所有子表
UPDATE meas SET peaktemp = 0 WHERE city_id > 0;
-- 只改父表
UPDATE ONLY meas SET peaktemp = 0 WHERE city_id > 0;
-- 危险:会删掉所有子表的行
DELETE FROM meas WHERE logdate < '2025-01-01';
-- 只删父表
DELETE FROM ONLY meas WHERE logdate < '2025-01-01';看 DELETE 的计划会更清楚:
EXPLAIN (COSTS OFF) DELETE FROM meas WHERE logdate < '2024-01-01'; Delete
-> Append
-> Seq Scan on meas meas_1
Filter: (logdate < '2024-01-01'::date)
-> Seq Scan on meas_y2024 meas_2
Filter: (logdate < '2024-01-01'::date)
-> Seq Scan on meas_y2025 meas_3
Filter: ...
-> Seq Scan on meas_y2026 meas_4
Filter: ...
(9 rows)注意 Delete 在 Append 之上——一次 DELETE 语句会删除多张物理表中的行。这在生产环境需要极其小心,尤其是「清理历史数据」类脚本。
子表上直接操作与继承列的只读性。 子表中继承来的列不能在子表层面删除或改类型:
ALTER TABLE meas_y2025 DROP COLUMN city_id;ERROR: cannot drop inherited column "city_id"ALTER TABLE meas_y2025 ALTER COLUMN city_id TYPE bigint;ERROR: cannot alter inherited column "city_id"必须在父表上操作,由 ALTER TABLE parent 向下传播:
-- 正确:在父表上改,子表跟着变
ALTER TABLE meas ALTER COLUMN peaktemp TYPE bigint; -- 会传播到所有子表子表可以删除自己新增的列:
-- 子表自己加的列可以自行删除
ALTER TABLE meas_y2025 ADD COLUMN src_system text;
ALTER TABLE meas_y2025 DROP COLUMN src_system; -- 成功用 pg_inherits 遍历与巡检
pg_inherits 记录直接父子关系,需要递归才能得到整棵树。下面是生产巡检常用的三个查询:
-- 1) 某父表的直接子表
SELECT child.relname AS child, i.inhseqno
FROM pg_inherits i
JOIN pg_class child ON child.oid = i.inhrelid
WHERE i.inhparent = 'meas'::regclass
ORDER BY i.inhseqno;
-- 2) 某子表的父表(回溯)
SELECT parent.relname AS parent
FROM pg_inherits i
JOIN pg_class parent ON parent.oid = i.inhparent
WHERE i.inhrelid = 'meas_y2025'::regclass;
-- 3) 递归列出整棵子树(含每一层的行数,便于发现「数据插错表」)
WITH RECURSIVE tree AS (
SELECT 'meas'::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::text AS tbl, depth FROM tree ORDER BY depth, tbl; tbl | depth
----------------+-------
meas | 0
meas_y2024 | 1
meas_y2025 | 1
meas_y2026 | 1
(4 rows)-- 巡检:哪些父表实际上没有任何子表(继承声明可能写错了)
SELECT p.relname AS parent
FROM pg_class p
WHERE p.relkind = 'r' AND p.relhassubclass = false
AND EXISTS (
SELECT 1 FROM pg_attribute a
WHERE a.attrelid = p.oid AND a.attname = 'id'
)
ORDER BY 1;实战示例
场景:为多租户系统建立「按租户归档」的继承结构,并安全地只查/只改指定租户的数据。
第一步,建父表与两个租户子表:
DROP TABLE IF EXISTS tenant_b_events, tenant_a_events, all_events CASCADE;
CREATE TABLE all_events (
id bigint,
tenant_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
payload jsonb
);
CREATE TABLE tenant_a_events () INHERITS (all_events);
CREATE TABLE tenant_b_events () INHERITS (all_events);
ALTER TABLE tenant_a_events ADD CONSTRAINT ck_tenant_a CHECK (tenant_id = 'tenant_a');
ALTER TABLE tenant_b_events ADD CONSTRAINT ck_tenant_b CHECK (tenant_id = 'tenant_b');
INSERT INTO tenant_a_events (id, tenant_id, payload) VALUES
(1, 'tenant_a', '{"action":"login"}'),
(2, 'tenant_a', '{"action":"purchase"}');
INSERT INTO tenant_b_events (id, tenant_id, payload) VALUES
(3, 'tenant_b', '{"action":"login"}');第二步,验证全局视图与单租户视图:
SELECT tableoid::regclass AS src, tenant_id, payload
FROM all_events ORDER BY id;
SELECT 'global' AS scope, count(*) FROM all_events
UNION ALL
SELECT 'tenant_a only', count(*) FROM ONLY tenant_a_events; src | tenant_id | payload
---------------------+-----------+------------------------
tenant_a_events | tenant_a | {"action": "login"}
tenant_a_events | tenant_a | {"action": "purchase"}
tenant_b_events | tenant_b | {"action": "login"}
(3 rows)
scope | count
--------------+-------
global | 3
tenant_a only | 2
(2 rows)第三步,验证约束排除与安全删除:
SET constraint_exclusion = on;
EXPLAIN (COSTS OFF)
SELECT * FROM all_events WHERE tenant_id = 'tenant_b'; Append
-> Seq Scan on all_events all_events_1
Filter: (tenant_id = 'tenant_b'::text)
-> Seq Scan on tenant_b_events all_events_3
Filter: (tenant_id = 'tenant_b'::text)
(4 rows)tenant_a_events 因 ck_tenant_a 与 tenant_id = 'tenant_b' 矛盾被排除,但父表 all_events 仍被扫描(父表无 tenant_id 约束)。
-- 安全删除:只删指定租户子表的过期数据
DELETE FROM ONLY tenant_b_events WHERE created_at < now() - interval '30 days';
-- 危险:会扫到所有租户
-- DELETE FROM all_events WHERE created_at < now() - interval '30 days';结果分析:
- 继承把「全局视图」与「租户隔离」统一在一套结构里,报表走
all_events,隔离操作走ONLY tenant_x_events。 - 约束排除减少了无关子表的扫描,但父表无法被排除,这是继承式隔离相比声明式分区最明显的性能损失。
tableoid::regclass是验证「数据是否落在正确租户子表」的唯一可靠手段;仅凭tenant_id列不足以说明问题,因为父表也可能存有该租户的数据。
场景二:通过 INHERIT / NO INHERIT 模拟「归档一个租户」。
-- 摘出:仍在层次外独立存在,数据保留
ALTER TABLE tenant_b_events NO INHERIT all_events;
SELECT count(*) FROM all_events; -- 只剩父表 + tenant_a
SELECT count(*) FROM tenant_b_events; -- 数据仍在
-- 挂回
ALTER TABLE tenant_b_events INHERIT all_events;
SELECT count(*) FROM all_events; -- 恢复 count
-------
2
(1 row)
count
-------
1
(1 row)
count
-------
3
(1 row)摘出与挂回都不搬数据、不加数据量,只改 pg_inherits。这就是「归档 = 卸下子表 + 移动到冷存储」的实现基础。
注意事项
错误!以为
INSERT INTO parent会按条件自动进入子表。 它永远写父表,且不报错。sql-- 不推荐:期望这行落到 meas_y2025 INSERT INTO meas (city_id, logdate, peaktemp) VALUES (1, '2025-06-01', 20);错误!在继承结构上使用
ALTER TABLE ONLY ... ADD COLUMN。 会造成父子列不一致,父表查询直接报错。sql-- 错误!子表不会有 quality 列 ALTER TABLE ONLY meas ADD COLUMN quality text;错误!用不带
ONLY的DELETE/UPDATE做数据清理。 会作用于整棵子树。sql-- 危险:一次删除多张物理表的数据 DELETE FROM meas WHERE logdate < '2024-01-01';错误!直接插子表却不加范围
CHECK约束。 少了这道闸门,越界数据会静默进入错误的年份。sql-- 推荐:每个子表都有 logdate 范围 CHECK ALTER TABLE meas_y2025 ADD CONSTRAINT ck_y2025 CHECK (logdate >= DATE '2025-01-01' AND logdate < DATE '2026-01-01');注意
INHERIT会加锁并校验全表。 大表上执行会长时间持锁,应在维护窗口进行,并先ANALYZE相关表。注意
ALTER TABLE parent ... ALTER COLUMN ... SET DEFAULT会覆盖子表默认值。 若子表有意使用不同默认值,改动父表后需复核。注意继承来的列不能在子表层面
DROP COLUMN/ 改类型,必须从父表操作;只有子表自增的列可以自行修改。注意
TRUNCATE ONLY child与TRUNCATE child的差别:后者在继承结构下默认会连子孙表一起清空,务必写ONLY。
最佳实践
老式分区场景下,每个子表都加「范围
CHECK约束」,并开启constraint_exclusion,让规划器能排除无关子表:sqlALTER TABLE meas_y2025 ADD CONSTRAINT ck_y2025 CHECK (logdate >= DATE '2025-01-01' AND logdate < DATE '2026-01-01');准备一张「兜底」父表或专门的 default 子表,并明确约定「越界数据进哪里」;老式分区必须解决这个问题,否则总有数据落在父表。
所有清理类语句强制写
ONLY,并在代码评审中检查:DELETE FROM ONLY/UPDATE ONLY/TRUNCATE ONLY。用
tableoid::regclass做写入后审计,尤其是批量导入和 ETL:sqlSELECT tableoid::regclass AS tgt, count(*) FROM meas GROUP BY 1;建立写入路由约定并在应用层落实(或使用规则/触发器),因为继承本身不做路由;文档中写清「哪张表由谁写」。
ALTER TABLE ONLY child DROP COLUMN只用于子表自增列;继承列一律从父表统一变更。定期跑继承树巡检 SQL,确认子表集合与预期一致、没有「孤儿父表」或漏挂的子表:
sqlSELECT p.relname AS parent, count(i.inhrelid) AS children FROM pg_class p LEFT JOIN pg_inherits i ON i.inhparent = p.oid WHERE p.relkind = 'r' AND p.relnamespace = 'public'::regnamespace GROUP BY p.relname HAVING count(i.inhrelid) > 0 ORDER BY 1;新项目优先使用声明式分区,只在确有「非分区型」层次建模需求时才用继承;把继承当作「表族的统一查询入口」而非分区手段。
下一节
继续学习:继承与约束