开发版本: 19 / devel
此文档适用于不受支持的 PostgreSQL 版本。
您可能需要查看当前版本的相同页面,或上面列出的其他受支持版本。

5.7. 时间表 #

时间表允许用户跟踪不同维度的历史。应用时间 跟踪世界中某个事物的历史,而系统时间跟踪数据库自身的历史。 (同时具备这两者的数据库也称为双时间数据库。)本节介绍如何在时间表中 表达和管理这类历史。

5.7.1. 应用时间 #

应用时间指的是表所描述实体的历史。在普通的非时间表中,每个实体只有一行。 在时间表中,一个实体可以有多行,只要这些行描述的是它历史中互不重叠的时间段即可。 应用时间要求每一行都具有开始时间和结束时间,用来表示该行在什么时候适用。

下面的 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

从原理上说,一个使用应用时间范围/多范围的表,等价于一个存储应用时间瞬点的表: 对每一秒、每一毫秒、每一纳秒,或者可用的最细粒度时间点都存一行。但这样的表会包含太多行, 所以范围/多范围提供了一种紧凑表示同样信息的优化方式。此外,范围和多范围也为常见的时间操作 提供了更方便的接口,因为这类记录变化往往不频繁,能以独立版本的形式持续保存很长时间。

5.7.1.1. 时间主键与唯一约束 #

具有应用时间的表对实体唯一性的理解与非时间表不同。时间实体唯一性可以通过时间主键来约束。 普通主键至少有一列,所有列都为 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 &&)。 唯一的区别是它们还会禁止空的应用时间。

5.7.1.2. 时间外键 #

时间外键是从一张应用时间表到另一张应用时间表的引用。普通引用要求被引用键存在, 时间引用也要求被引用键存在,但要求它至少在引用存在的那段历史期间一直存在。 因此,如果 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 的时间外键, 但不支持 RESTRICTCASCADESET NULLSET DEFAULT

5.7.2. 系统时间 #

系统时间指的是数据库表本身的历史,而不是它所描述的实体的历史。 它记录每一行何时被插入、更新或删除。

PostgreSQL 目前不支持系统时间,但可以借助触发器模拟, 而且也有一些外部扩展提供了这类功能。