5.12. 继承 #
PostgreSQL 实现了表继承,这对数据库设计者来说是一种有用的工具(SQL:1999 及其后的版本定义了一种类型继承特性,但和这里介绍的继承有很大的不同)。
让我们从一个示例开始:假设我们要为城市建立一个数据模型。每个州有很多城市,但只有一个首府。我们希望能够快速检索任意特定州的首府城市。这可以通过创建两个表来实现:一个用于州首府,另一个用于非首府城市。然而,当我们想要查询某个城市的数据,而不关心它是不是首府时,会发生什么?继承特性将有助于解决这个问题。我们可以将 capitals 表定义为继承自 cities 表:
CREATE TABLE cities (
name text,
population float,
elevation int -- 单位为英尺
);
CREATE TABLE capitals (
state char(2)
) INHERITS (cities);
在这种情况下,capitals 表继承了它的父表 cities 的所有列。州首府还有一个额外的列 state 用来表示它所属的州。
在 PostgreSQL 中,一个表可以从 0 个或者多个其他表继承,而对一个表的查询则可以引用一个表的所有行或者该表的所有行加上它所有的后代表。默认情况是后一种行为。例如,下面的查询将查找所有高度高于 500 英尺的城市的名称,包括州首府:
SELECT name, elevation
FROM cities
WHERE elevation > 500;
对于来自 PostgreSQL 教程(见第 2.1 节)的示例数据,它将返回:
name | elevation -----------+----------- Las Vegas | 2174 Mariposa | 1953 Madison | 845
另一方面,下面的查询将找到所有高度超过 500 英尺且不是州首府的城市:
SELECT name, elevation
FROM ONLY cities
WHERE elevation > 500;
name | elevation
-----------+-----------
Las Vegas | 2174
Mariposa | 1953
这里的 ONLY 关键词指示查询只被应用于 cities 上,而在继承层次中位于 cities 之下的其他表都不会被该查询涉及。很多我们已经讨论过的命令(如 SELECT、UPDATE 和 DELETE)都支持 ONLY 关键词。
我们也可以在表名后写上一个* 来显式地将后代表包括在查询范围内:
SELECT name, elevation
FROM cities*
WHERE elevation > 500;
写* 并非必需,因为这种行为始终是默认的。不过,为了兼容那些允许修改默认行为的较旧版本,现在仍然支持这种语法。
在某些情况下,我们可能希望知道一个特定行来自于哪个表。每个表中的系统列 tableoid 可以告诉我们行来自于哪个表:
SELECT c.tableoid, c.name, c.elevation FROM cities c WHERE c.elevation > 500;
将会返回:
tableoid | name | elevation ----------+-----------+----------- 139793 | Las Vegas | 2174 139793 | Mariposa | 1953 139798 | Madison | 845
(如果重新生成这个结果,可能会得到不同的 OID 数字。)通过与 pg_class 进行连接可以看到实际的表名:
SELECT p.relname, c.name, c.elevation FROM cities c, pg_class p WHERE c.elevation > 500 AND c.tableoid = p.oid;
将会返回:
relname | name | elevation ----------+-----------+----------- cities | Las Vegas | 2174 cities | Mariposa | 1953 capitals | Madison | 845
得到同样效果的另一种方法,是使用 regclass 别名类型,它会以符号形式打印表的 OID:
SELECT c.tableoid::regclass, c.name, c.elevation FROM cities c WHERE c.elevation > 500;
继承不会自动地将来自 INSERT 或 COPY 命令的数据传播到继承层次中的其他表中。在我们的示例中,下面的 INSERT 语句将会失败:
INSERT INTO cities (name, population, elevation, state)
VALUES ('Albany', NULL, NULL, 'NY');
我们也许会希望数据能以某种方式被路由到 capitals 表中,但这不会发生:INSERT 总是向指定的表中插入。在某些情况下,可以通过使用一个规则(见第 39 章)将插入动作重定向。但是这对上面的情况并没有帮助,因为 cities 表根本就不包含 state 列,因而这个命令会在触发规则之前就被拒绝。
父表上的所有检查约束和非空约束都将自动被它的后代所继承,除非显式地指定了 NO INHERIT 子句。其他类型的约束(唯一、主键和外键约束)则不会被继承。
一个表可以从多个父表继承,在这种情况下它拥有各个父表所定义的列的并集。任何定义在子表上的列也会被加入到其中。如果在这个集合中出现重名列,那么这些列将被“合并”,这样在子表中只会有一个这样的列。重名列能被合并的前提是这些列必须具有相同的数据类型,否则会导致错误。可继承的检查约束和非空约束会以类似的方式被合并。例如,如果合并成一个合并列的任一列定义被标记为非空,则该合并列会被标记为非空。如果检查约束的名称相同,则它们会被合并,但如果它们的条件不同则合并会失败。对于合并后的检查约束,会保留更严格的强制执行属性:只要有任一继承副本被强制执行,合并后的约束就会被强制执行。
表继承通常在创建子表时建立,即通过 CREATE TABLE 语句中的 INHERITS 子句。已经创建且定义兼容的表也可以通过 ALTER TABLE 的 INHERIT 变体再增加一个新的父表关系。要这么做,新子表必须已经包含与父表同名且数据类型相同的列。子表还必须包含名称和检查表达式与父表相同的检查约束,以及匹配的非空约束。类似地,也可以使用 ALTER TABLE 的 NO INHERIT 变体,从子表中移除一条继承链接。动态添加和移除继承链接可用于实现表分区(见第 5.13 节)。
一种方便的方法是在 CREATE TABLE 中使用 LIKE 子句,创建一个兼容的表,以便以后将其用作新的子表。这样会创建一个与源表具有相同列的新表。如果源表上定义了任何 CHECK 约束,就应指定 LIKE 的 INCLUDING CONSTRAINTS 选项,因为新子表必须具备与父表匹配的约束,才会被视为兼容。
只要还有任何子表存在,父表就不能被删除。子表中继承的列,以及继承的检查约束和非空约束,都不能直接删除。继承约束的某些属性可以更改,但更改后的每个约束必须仍与其继承来源的所有父约束兼容。如果希望移除一个表和它的所有后代,一种简单的方法是使用 CASCADE 选项删除父表(见第 5.16 节)。
ALTER TABLE 会把列定义以及可继承约束(检查约束和非空约束)上的变化沿着继承层次向下传播。同样,删除被其他表依赖的列只能使用 CASCADE 选项。对于重复的继承列和约束定义的合并与拒绝,ALTER TABLE 遵循与
CREATE TABLE 相同的规则。
继承的查询仅在父表上执行访问权限检查。例如,在 cities 表上授予 UPDATE 权限也隐含着通过 cities 访问时在 capitals 表中更新行的权限。这保留了数据(也)在父表中的样子。但是如果没有额外的授权,则不能直接更新 capitals 表。以类似的方式,父表的行安全性策略(见第 5.10 节)适用于继承查询期间来自于子表的行。只有当子表在查询中被明确提到时,其策略(如果有)才会被应用,在那种情况下,附着在其父表上的任何策略都会被忽略。
外部表(见第 5.14 节)也可以是继承层次中的一部分,既可作为父表也可作为子表,就像常规表一样。如果一个外部表是继承层次的一部分,那么任何不被该外部表支持的操作也不被整个层次所支持。
5.12.1. 注意事项 #
注意,并非所有 SQL 命令都能作用于继承层次。用于数据查询、数据修改或模式修改的命令(例如 SELECT、UPDATE、DELETE、大多数 ALTER TABLE 变体,但不包括 INSERT 或 ALTER TABLE ... RENAME)通常默认包含子表,并支持使用 ONLY 记法将其排除。大多数用于数据库维护和调优的命令(例如 REINDEX)只作用于独立的物理表,不支持沿继承层次递归。不过,VACUUM 和 ANALYZE 默认会包含子表,并支持使用 ONLY 记法将其排除。各条命令的具体行为都记录在相应参考页中(SQL 命令)。
继承特性的一个严重限制是,索引(包括唯一约束)和外键约束只作用于单个表,而不作用于其继承子表。对于外键约束的引用端和被引用端,这一点都成立。因此,沿用上面的示例:
如果我们把
cities.name声明为UNIQUE或PRIMARY KEY,这并不能阻止capitals表中出现与cities中城市同名的行。而且这些重复行默认还会出现在针对cities的查询结果中。事实上,默认情况下capitals根本没有唯一约束,因此它可以包含多行同名记录。你当然可以给capitals添加唯一约束,但这仍无法阻止相对于cities的重复。相似地,如果我们指定
cities.nameREFERENCES某个其他表,该约束不会自动地传播到capitals。在此种情况下,我们可以变通地在capitals上手工创建一个相同的REFERENCES约束。如果让另一个表的某列
REFERENCES cities(name),那么该表可以包含城市名称,但不能包含首府名称。对于这种情况,并没有什么好的变通办法。
某些未为继承层次实现的功能,却已经为声明式分区实现了。因此,在决定使用旧式继承分区是否适合你的应用时,需要非常小心。