时间表允许用户跟踪不同维度的历史。应用时间 跟踪世界中某个事物的历史,而系统时间跟踪数据库自身的历史。 (同时具备这两者的数据库也称为双时间数据库。)本节介绍如何在时间表中 表达和管理这类历史。
应用时间指的是表所描述实体的历史。在普通的非时间表中,每个实体只有一行。 在时间表中,一个实体可以有多行,只要这些行描述的是它历史中互不重叠的时间段即可。 应用时间要求每一行都具有开始时间和结束时间,用来表示该行在什么时候适用。
下面的 SQL 创建了一个可以存储应用时间的时间表:
CREATE TABLE products (
product_no integer,
price numeric,
valid_at daterange
);
时间表中的记录可以像Figure 5.1那样想象成时间线上的内容。 这里我们展示三条记录,描述两个产品。每条记录都是一个包含三个属性的元组:产品编号、价格和应用时间。 因此,产品 5 先是在 2020 年 1 月 1 日起以 5.00 的价格出售,后来又从 2022 年 1 月 1 日起变为 8.00。 它的第二条记录没有指定结束时间,表示它无限期有效,或者说对未来所有时间都有效。 最后一条记录显示产品 6 在 2021 年 1 月 1 日以 9.00 上市,然后在 2024 年 1 月 1 日取消。
Figure 5.1. 应用时间示例
在表中,这些记录会是:
product_no | price | valid_at
------------+-------+-------------------------
5 | 5.00 | [2020-01-01,2022-01-01)
5 | 8.00 | [2022-01-01,)
6 | 9.00 | [2021-01-01,2024-01-01)
这里使用范围类型记法来表示应用时间,因为它被存储为单个列(范围或多范围之一)。 范围包含起点但不包含终点。这样两个相邻范围就能无重叠地覆盖所有点。关于范围类型的更多信息, 见 Section 8.17。
从原理上说,一个使用应用时间范围/多范围的表,等价于一个存储应用时间“瞬点”的表: 对每一秒、每一毫秒、每一纳秒,或者可用的最细粒度时间点都存一行。但这样的表会包含太多行, 所以范围/多范围提供了一种紧凑表示同样信息的优化方式。此外,范围和多范围也为常见的时间操作 提供了更方便的接口,因为这类记录变化往往不频繁,能以独立“版本”的形式持续保存很长时间。
具有应用时间的表对实体唯一性的理解与非时间表不同。时间实体唯一性可以通过时间主键来约束。 普通主键至少有一列,所有列都为 NOT NULL,并且所有列组合起来的值必须唯一。 时间主键也至少有这样一列,不过它还额外带有一个最终列,该列是范围类型或多范围类型,用来表示这一行何时适用。 主键的普通部分在任意时刻都必须唯一,但如果应用时间不重叠,则允许出现非唯一行。
创建时间主键的语法如下:
CREATE TABLE products (
product_no integer,
price numeric,
valid_at daterange,
PRIMARY KEY (product_no, valid_at WITHOUT OVERLAPS)
);
在这个例子里,product_no 是键的非时间部分,而 valid_at 是包含应用时间的范围列。
WITHOUT OVERLAPS 列会隐式地成为 NOT NULL (和键的其他部分一样)。此外,它也不能包含空值,也就是 'empty' 范围或 {} 多范围。空的应用时间没有意义。
也可以创建一个不是主键的时间唯一约束。语法类似:
CREATE TABLE products (
product_no integer,
price numeric,
valid_at daterange,
UNIQUE (product_no, valid_at WITHOUT OVERLAPS)
);
时间唯一约束同样禁止其应用时间使用空范围/多范围,但该列可以为空(就像唯一约束中的其他列一样)。
时间主键和唯一约束由 GiST 索引(见 Section 65.2)而不是 B-Tree 索引来支持。 实际上,创建时间主键或约束需要安装 btree_gist 扩展,这样数据库才能为键的非时间部分提供 GiST 操作符类。
时间主键和唯一约束的行为与排他约束(见 Section 5.5.6)相同, 其中每个普通键部分按相等性比较,而应用时间按重叠关系比较,例如 EXCLUDE USING gist (id WITH =, valid_at WITH &&)。 唯一的区别是它们还会禁止空的应用时间。
时间外键是从一张应用时间表到另一张应用时间表的引用。普通引用要求被引用键存在, 时间引用也要求被引用键存在,但要求它至少在引用存在的那段历史期间一直存在。 因此,如果 products 表被 variants 表引用, 而产品 5 的某个变体具有 [2020-01-01,2026-01-01) 的应用时间, 那么产品 5 必须在整个期间都存在。
我们可以用下面的模式创建 variants 表(暂时还不带外键):
CREATE TABLE variants ( id integer, product_no integer, name text, valid_at daterange, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) );
这里包含了一个时间主键,作为最佳实践,不过外键并不严格要求必须有它。
Figure 5.2 把产品 5(绿色)和两个引用它的变体(黄色)画在同一条时间线上。 变体 8(Medium)先出现,随后是变体 9(XXL)。它们都满足外键约束,因为被引用的产品在它们整个历史期间都存在。
Figure 5.2. 时间外键示例
在表中,这些记录会是:
id | product_no | name | valid_at ----+------------+--------+------------------------- 8 | 5 | Medium | [2021-01-01,2023-06-01) 9 | 5 | XXL | [2022-03-01,2024-06-01)
注意,时间引用不一定要由被引用表中的单行满足。产品 5 在变体 8 的历史中间发生过价格变化, 但该引用仍然有效。用于判断被引用历史是否包含引用行的是所有匹配行的组合。
为表添加时间外键的语法如下:
CREATE TABLE variants (
id integer,
product_no integer,
name text,
valid_at daterange,
PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
FOREIGN KEY (product_no, PERIOD valid_at) REFERENCES products (product_no, PERIOD valid_at)
);
注意,在引用表和被引用表中,应用时间列都必须使用关键字 PERIOD。
被引用表上必须存在一个与被引用列匹配的时间主键或唯一约束。
PostgreSQL 支持动作 NO ACTION 的时间外键, 但不支持 RESTRICT、CASCADE、SET NULL 或 SET DEFAULT。
系统时间指的是数据库表本身的历史,而不是它所描述的实体的历史。 它记录每一行何时被插入、更新或删除。
PostgreSQL 目前不支持系统时间,但可以借助触发器模拟, 而且也有一些外部扩展提供了这类功能。