5.8. 继承
PostgreSQL实现了表继承, 这个特性对数据库设计人员来说是一个很有效的工具。SQL99 及以后的标准定义了类型继承特性, 和我们在这里描述的很多特性有区别。
让我们从一个例子开始:假设我们试图制作一个城市数据模型。每个州都有许多城市, 但是只有一个首府。我们希望能够迅速检索任何州的首府。这个任务可以通过创建两个表来实现, 一个是州府表,一个是非州府表。不过,如果我们不管什么城市都想查该怎么办? 继承的特性可以帮助我们解决这个问题。我们定义capitals
表, 它继承自cities
表:
CREATE TABLE cities (
name text,
population float,
altitude int -- 英尺
);
CREATE TABLE capitals (
state char(2)
) INHERITS (cities);
在这种情况下,capitals
表继承它的父表cities
中的所有属性。州首府有一个额外的state
属性显示其所在的州。
在PostgreSQL里,一个表可以从零个或多个其它表中继承属性, 而且一个查询既可以引用一个表中的所有行, 也可以引用一个表及其所有后代表的行(后面这个是缺省行为)。比如, 下面的查询查找所有海拔 500 英尺以上的城市名,包括州首府:
SELECT name, altitude
FROM cities
WHERE altitude > 500;
使用PostgreSQL教程里面的数据(参阅Section 2.1),它返回:
name | altitude
-----------+----------
Las Vegas | 2174
Mariposa | 1953
Madison | 845
另一方面,如果要找出不包括州首府的所有海拔超过 500 英尺的城市,查询应该是这样的:
SELECT name, altitude
FROM ONLY cities
WHERE altitude > 500;
name | altitude
-----------+----------
Las Vegas | 2174
Mariposa | 1953
cities
前面的ONLY
表明该查询应该只针对cities
而不包括其后代。许多我们已经讨论过的命令(SELECT
, UPDATE
和 DELETE
)都支持ONLY
关键字。
你也可以在表名后面写一个*
显示指定包括所有后代表:
SELECT name, altitude
FROM cities*
WHERE altitude > 500;
因为这个行为是默认的,所以写*
并不是必须的(除非你已经改变了 sql_inheritance里面的配置选项)。然而,写*
可以用于强调搜索额外的表。
有时候你可能想知道某个行版本来自哪个表。在每个表里我们都有一个tableoid
系统属性可以告诉你源表是谁:
SELECT c.tableoid, c.name, c.altitude
FROM cities c
WHERE c.altitude > 500;
结果如下(你可能会得到不同的 OID):
tableoid | name | altitude
----------+-----------+----------
139793 | Las Vegas | 2174
139793 | Mariposa | 1953
139798 | Madison | 845
通过和pg_class
做一个连接,就可以看到实际的表名字:
SELECT p.relname, c.name, c.altitude
FROM cities c, pg_class p
WHERE c.altitude > 500 AND c.tableoid = p.oid;
它返回:
relname | name | altitude
----------+-----------+----------
cities | Las Vegas | 2174
cities | Mariposa | 1953
capitals | Madison | 845
对于INSERT
或COPY
,继承并不自动影响其后代表。 在我们的例子里,下面的INSERT
语句将会失败:
INSERT INTO cities (name, population, altitude, state)
VALUES ('New York', NULL, NULL, 'NY');
我们可能希望数据被传递到capitals
表里面去, 但这是不会发生的:INSERT
总是插入明确声明的那个表。 在某些情况下,我们可以使用规则进行重定向插入(参阅 Chapter 38)。 不过它不能对上面的例子有什么帮助,因为cities
表并不包含state
字段,因此命令在规则施加之前就会被拒绝掉。
所有父表的检查约束和非空约束都会自动被所有子表继承。 不过其它类型的约束(唯一、主键、外键约束)不会被继承。
一个子表可以从多个父表继承,这种情况下它将拥有所有父表字段的总和, 并且子表中定义的字段也会加入其中。如果同一个字段名出现在多个父表中, 或者同时出现在父表和子表的定义里,那么这些字段就会被"融合", 这样在子表里就只有一个这样的字段。要想融合,字段的数据类型必须相同, 否则就会抛出一个错误。融合的字段将会拥有其父字段的所有检查约束, 并且如果某个父字段存在非空约束,那么融合后的字段也必须是非空的。
表继承通常使用带INHERITS
子句的CREATE TABLE语句定义。 另外,一个已经用此方法定义的子表可以使用带INHERIT
的ALTER TABLE 命令添加一个新父表。注意:该子表必须已经包含新父表的所有字段且类型一致, 此外新父表的每个约束的名字及其表达式都必须包含在此子表中。同样, 一个继承链可以使用带NO INHERIT
的ALTER TABLE
命令从子表上删除。 允许动态添加和删除继承链对基于继承关系的表分区(参见Section 5.9)很有用。
创建一个将要作为子表的新表的便利途径是使用带LIKE
子句的 CREATE TABLE
命令。它将创建一个与源表字段相同的新表。 如果源表中存在约束,那么应该指定LIKE
的INCLUDING CONSTRAINTS
选项,因为子表必须包含源表中的CHECK
约束。
任何存在子表的父表都不能被删除,同样,子表中任何从父表继承的字段或约束也不能被删除或修改。 如果你想删除一个表及其所有后代,最简单的办法是使用CASCADE
选项删除父表。
ALTER TABLE会把所有数据定义和检查约束传播到后代里面去。另外, 只有在使用CASCADE
选项的情况下,才能删除依赖于其他表的字段。 ALTER TABLE
在重复字段融合和拒绝方面和CREATE TABLE
的规则相同。
请注意表访问权限是如何处理的。访问父表会自动访问在子表中的数据,而不需要更多的访问权限检查。 这保留了父表中数据的表现。然而,直接访问子表不会自动允许访问父表,要访问父表需要更进一步的权限被授予。
5.8.1. 警告
注意,不是所有的 SQL 命令可以在所有的继承层次上正常工作。数据查询,数据修改, 模式修改的命令(比如SELECT
,UPDATE
,DELETE
, ALTER TABLE
的大多数变型,但不是INSERT
和 ALTER TABLE ... RENAME
)典型的默认包括子表和支持ONLY
符号来排除它们。 为数据库维护和调优的命令(例如REINDEX
,VACUUM
) 通常只对个别工作,物理表格不支持递归超过继承层次结构。单独命令各自的行为记录在了它们的参考页中(Reference I, SQL 命令)。
继承的一个严重局限性是索引(包括唯一约束)和外键约束只能用于单个表, 而不能包括它们的子表(不管对外键约束的引用表还是被引用表都是如此),因此,在上面的例子里:
即使我们声明
cities
.name
为UNIQUE
或PRIMARY KEY
, 也不会阻止capitals
表拥有重复名字的cities
数据行。 并且这些重复的行在查询cities
表的时候会显示出来。实际上, 缺省时capitals
将完全没有唯一约束,因此可能包含带有同名的多个行。 你应该给capitals
增加唯一约束,但即使这样做也不能避免与cities
的重复。类似的,即使我们声明
cities
.name
参照(REFERENCES
) 某些其它的表,这个约束也不会自动传播到capitals
表。在这种条件下, 你可以通过手工给capitals
表增加同样的REFERENCES
约束来做到这点。声明一个其它表的字段为
REFERENCES cities(name)
将允许其它表包含城市名, 但是不包含首府名。这种情况下没有很好的绕开办法。
这些缺点很可能在将来的版本中修补,但同时你也需要考虑一下,继承是否对你的应用真正有用。