Sequence

羁鸟

2022/08/22

30 min

介绍

Sequence 作为数据库中的一个特别的表级对象,可以根据用户设定的不同属性,产生一系列有规则的整数,从而起到发号器的作用。

在使用方面,可以设置永不重复的 Sequence 用来作为一张表的主键,也可以通过不同表共享同一个 Sequence 来记录多个表的总插入行数。根据 ANSI 标准,一个 Sequence 对象在数据库要具备以下特征:

  1. 独立的数据库对象 (CREATE SEQUENCE),和表、视图同一层级
  2. 可以设置生成属性:初始值 (star value),步长 (increment),最大/小值 (max/min),循环产生 (cycle),缓存 (cache)等
  3. Sequence 对象在当前值的基础上进行递增或者递减,当前值被初始化为初始值
  4. 在设置循环后,当前值的变化具有周期性;不设置循环下,当前值的变化具有单调性,当前值到达最值后不可再变化

为了解释上述特性,我们分别定义 ab 两种序列来举例其具体的行为。

CREATE SEQUENCE a start with 5 minvalue -1 increment -2;
CREATE SEQUENCE b start with 2 minvalue 1 maxvalue 4 cycle;

两个 Sequence 对象提供的序列值,随着序列申请次数的变化,如下所示:

单调序列与循环序列

PostgreSQLOracleSQLSERVERMySQLMariaDBDB2SybaseHive
支持支持支持仅支持自增字段支持支持仅支持自增字段不支持

为了更进一步了解 PostgreSQL 中的 Sequence 对象,我们先来了解 Sequence 的用法,并从用法中透析 Sequence 背后的设计原理。

使用方法

PostgreSQL 提供了丰富的 Sequence 调用接口,以及组合使用的场景,以充分支持开发者的各种需求。

SQL 接口

PostgreSQL 对 Sequence 对象也提供了类似于 的访问方式,即 DQL、DML 以及 DDL。我们从下图中可一览对外提供的 SQL 接口。

SQL接口

分别来介绍以下这几个接口:

currval

该接口的含义为,返回 Session 上次使用的某一 Sequence 的值。

postgres=# select nextval('seq');
 nextval
---------
       2
(1 row)

postgres=# select currval('seq');
 currval
---------
       2
(1 row)

需要注意的是,使用该接口必须使用过一次 nextval 方法,否则会提示目标 Sequence 在当前 Session 未定义。

postgres=# select currval('seq');
ERROR:  currval of sequence "seq" is not yet defined in this session

lastval

该接口的含义为,返回 Session 上次使用的 Sequence 的值。

postgres=# select nextval('seq');
 nextval
---------
       3
(1 row)

postgres=# select lastval();
 lastval
---------
       3
(1 row)

同样,为了知道上次用的是哪个 Sequence 对象,需要用一次 nextval('seq'),让 Session 以全局变量的形式记录下上次使用的 Sequence 对象。

lastvalcurval 两个接口仅仅只是参数不同,currval 需要指定是哪个访问过的 Sequence 对象,而 lastval 无法指定,只能是最近一次使用的 Sequence 对象。

nextval

该接口的含义为,取 Sequence 对象的下一个序列值。

通过使用 nextval 方法,可以让数据库基于 Sequence 对象的当前值,返回一个递增了 increment 数量的一个序列值,并将递增后的值作为 Sequence 对象当前值。

postgres=# CREATE SEQUENCE seq start with 1 increment 2;
CREATE SEQUENCE
postgres=# select nextval('seq');
 nextval
---------
       1
(1 row)

postgres=# select nextval('seq');
 nextval
---------
       3
(1 row)

increment 称作 Sequence 对象的步长,Sequence 的每次以 nextval 的方式进行申请,都是以步长为单位进行申请的。同时,需要注意的是,Sequence 对象创建好以后,第一次申请获得的值,是 start value 所定义的值。对于 start value 的默认值,有以下 PostgreSQL 规则:

start_value=1,if:increment>0; start\_value = 1, if:increment > 0;

start_value=1,if:increment<0; start\_value = -1,if:increment < 0;

另外,nextval 是一种特殊的 DML,其不受事务所保护,即:申请出的序列值不会再回滚。

postgres=# BEGIN;
BEGIN
postgres=# select nextval('seq');
 nextval
---------
       1
(1 row)

postgres=# ROLLBACK;
ROLLBACK
postgres=# select nextval('seq');
 nextval
---------
       2
(1 row)

PostgreSQL 为了 Sequence 对象可以获得较好的并发性能,并没有采用多版本的方式来更新 Sequence 对象,而是采用了原地修改的方式完成 Sequence 对象的更新,这种不用事务保护的方式几乎成为所有支持 Sequence 对象的 RDMS 的通用做法,这也使得 Sequence 成为一种特殊的表级对象。

setval

该接口的含义是,设置 Sequence 对象的序列值。

postgres=# select nextval('seq');
 nextval
---------
       4
(1 row)

postgres=# select setval('seq', 1);
 setval
--------
      1
(1 row)

postgres=# select nextval('seq');
 nextval
---------
       2
(1 row)

该方法可以将 Sequence 对象的序列值设置到给定的位置,同时可以将第一个序列值申请出来。如果不想申请出来,可以采用加入 false 参数的做法。

postgres=# select nextval('seq');
 nextval
---------
       4
(1 row)

postgres=# select setval('seq', 1, false);
 setval
--------
      1
(1 row)

postgres=# select nextval('seq');
 nextval
---------
       1
(1 row)

SQL接口

通过在 setval 来设置好 Sequence 对象的值以后,同时来设置 Sequence 对象的 is_called 属性。nextval 就可以根据 Sequence 对象的 is_called 属性来判断要返回的是否要返回设置的序列值。即:如果 is_calledfalsenextval 接口会去设置 is_calledtrue,而不是进行 increment。

CREATE/ALTER SEQUENCE

CREATEALTER SEQUENCE 用于创建/变更 Sequence 对象,其中 Sequence 属性也通过 CREATEALTER SEQUENCE 接口进行设置,前面已简单介绍部分属性,下面将详细描述具体的属性。

CREATE [ TEMPORARY | TEMP ] SEQUENCE [ IF NOT EXISTS ] name
    [ AS data_type ]
    [ INCREMENT [ BY ] increment ]
    [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ]
    [ START [ WITH ] start ] [ CACHE cache ] [ [ NO ] CYCLE ]
    [ OWNED BY { table_name.column_name | NONE } ]
ALTER SEQUENCE [ IF EXISTS ] name
    [ AS data_type ]
    [ INCREMENT [ BY ] increment ]
    [ MINVALUE minvalue | NO MINVALUE ] [ MAXVALUE maxvalue | NO MAXVALUE ]
    [ START [ WITH ] start ]
    [ RESTART [ [ WITH ] restart ] ]
    [ CACHE cache ] [ [ NO ] CYCLE ]
    [ OWNED BY { table_name.column_name | NONE } ]
  • AS:设置 Sequence 的数据类型,只可以设置为 smallintintbigint;与此同时也限定了 minvaluemaxvalue 的设置范围,默认为 bigint 类型(注意,只是限定,而不是设置,设置的范围不得超过数据类型的范围)。
  • INCREMENT:步长,nextval 申请序列值的递增数量,默认值为 1。
  • MINVALUE / NOMINVALUE:设置/不设置 Sequence 对象的最小值,如果不设置则是数据类型规定的范围,例如 bigint 类型,则最小值设置为 PG_INT64_MIN(-9223372036854775808)
  • MAXVALUE / NOMAXVALUE:设置/不设置 Sequence 对象的最大值,如果不设置,则默认设置规则如上。
  • START:Sequence 对象的初始值,必须在 MINVALUEMAXVALUE 范围之间。
  • RESTART:ALTER 后,可以重新设置 Sequence 对象的序列值,默认设置为 start value。
  • CACHE / NOCACHE:设置 Sequence 对象使用的 Cache 大小,NOCACHE 或者不设置则默认为 1。
  • OWNED BY:设置 Sequence 对象归属于某张表的某一列,删除列后,Sequence 对象也将删除。

特殊场景下的序列回滚

下面描述了一种序列回滚的场景

CREATE SEQUENCE
postgres=# BEGIN;
BEGIN
postgres=# ALTER SEQUENCE seq maxvalue 10;
ALTER SEQUENCE
postgres=# select nextval('seq');
 nextval
---------
       1
(1 row)

postgres=# select nextval('seq');
 nextval
---------
       2
(1 row)

postgres=# ROLLBACK;
ROLLBACK
postgres=# select nextval('seq');
 nextval
---------
       1
(1 row)

与之前描述的不同,此处 Sequence 对象受到了事务的保护,序列值发生了发生回滚。实际上,此处事务保护的是 ALTER SEQUENCE(DDL),而非 nextval(DML),因此此处发生的回滚是将 Sequence 对象回滚到 ALTER SEQUENCE 之前的状态,故发生了序列回滚现象。

DROP/TRUNCATE

  • DROP SEQUENCE,如字面意思,去除数据库中的 Sequence 对象。
  • TRUNCATE,准确来讲,是通过 TRUNCATE TABLE 完成 RESTART SEQUENCE
postgres=# CREATE TABLE tbl_iden (i INTEGER, j int GENERATED ALWAYS AS IDENTITY);
CREATE TABLE
postgres=# insert into tbl_iden values (100);
INSERT 0 1
postgres=# insert into tbl_iden values (1000);
INSERT 0 1
postgres=# select * from tbl_iden;
  i   | j
------+---
  100 | 1
 1000 | 2
(2 rows)

postgres=# TRUNCATE TABLE tbl_iden RESTART IDENTITY;
TRUNCATE TABLE
postgres=# insert into tbl_iden values (1234);
INSERT 0 1
postgres=# select * from tbl_iden;
  i   | j
------+---
 1234 | 1
(1 row)

此处相当于在 TRUNCATE 表的时候,执行 ALTER SEQUENCE RESTART

Sequence 组合使用场景

SEQUENCE 除了作为一个独立的对象时候以外,还可以组合其他 PostgreSQL 其他组件进行使用,我们总结了一下几个常用的场景。

组合调用

显式调用

CREATE SEQUENCE seq;
CREATE TABLE tbl (i INTEGER PRIMARY KEY);
INSERT INTO tbl (i) VALUES (nextval('seq'));
SELECT * FROM tbl ORDER BY 1 DESC;
   tbl
---------
       1
(1 row)

触发器调用

CREATE SEQUENCE seq;
CREATE TABLE tbl (i INTEGER PRIMARY KEY, j INTEGER);
CREATE FUNCTION f()
RETURNS TRIGGER AS
$$
BEGIN
NEW.i := nextval('seq');
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';

CREATE TRIGGER tg
BEFORE INSERT ON tbl
FOR EACH ROW
EXECUTE PROCEDURE f();

INSERT INTO tbl (j) VALUES (4);

SELECT * FROM tbl;
 i | j
---+---
 1 | 4
(1 row)

DEFAULT 调用

显式 DEFAULT 调用:

CREATE SEQUENCE seq;
CREATE TABLE tbl(i INTEGER DEFAULT nextval('seq') PRIMARY KEY, j INTEGER);

INSERT INTO tbl (i,j) VALUES (DEFAULT,11);
INSERT INTO tbl(j) VALUES (321);
INSERT INTO tbl (i,j) VALUES (nextval('seq'),1);

SELECT * FROM tbl;
 i |  j
---+-----
 2 | 321
 1 |  11
 3 |   1
(3 rows)

SERIAL 调用:

CREATE TABLE tbl (i SERIAL PRIMARY KEY, j INTEGER);
INSERT INTO tbl (i,j) VALUES (DEFAULT,42);

INSERT INTO tbl (j) VALUES (25);

SELECT * FROM tbl;
 i | j
---+----
 1 | 42
 2 | 25
(2 rows)

注意,SERIAL 并不是一种类型,而是 DEFAULT 调用的另一种形式,只不过 SERIAL 会自动创建 DEFAULT 约束所要使用的 Sequence。

AUTO_INC 调用

CREATE TABLE tbl (i int GENERATED ALWAYS AS IDENTITY,
                  j INTEGER);
INSERT INTO tbl(i,j) VALUES (DEFAULT,32);

INSERT INTO tbl(j) VALUES (23);

SELECT * FROM tbl;
 i | j
---+----
 1 | 32
 2 | 23
(2 rows)

AUTO_INC 调用对列附加了自增约束,与 default 约束不同,自增约束通过查找 dependency 的方式找到该列关联的 Sequence,而 default 调用仅仅是将默认值设置为一个 nextval 表达式。

原理剖析

Sequence 在系统表与数据表中的描述

在 PostgreSQL 中有一张专门记录 Sequence 信息的系统表,即 pg_sequence。其表结构如下:

postgres=# \d pg_sequence
             Table "pg_catalog.pg_sequence"
    Column    |  Type   | Collation | Nullable | Default
--------------+---------+-----------+----------+---------
 seqrelid     | oid     |           | not null |
 seqtypid     | oid     |           | not null |
 seqstart     | bigint  |           | not null |
 seqincrement | bigint  |           | not null |
 seqmax       | bigint  |           | not null |
 seqmin       | bigint  |           | not null |
 seqcache     | bigint  |           | not null |
 seqcycle     | boolean |           | not null |
Indexes:
    "pg_sequence_seqrelid_index" PRIMARY KEY, btree (seqrelid)

不难看出,pg_sequence 中记录了 Sequence 的全部的属性信息,该属性在 CREATE/ALTER SEQUENCE 中被设置,Sequence 的 nextval 以及 setval 要经常打开这张系统表,按照规则办事。

对于 Sequence 序列数据本身,其实现方式是基于 heap 表实现的,heap 表共计三个字段,其在表结构如下:

typedef struct FormData_pg_sequence_data
{
    int64		last_value;
    int64		log_cnt;
    bool		is_called;
} FormData_pg_sequence_data;
  • last_value 记录了 Sequence 的当前的序列值,我们称之为页面值(与后续的缓存值相区分)
  • log_cnt 记录了 Sequence 在 nextval 申请时,预先向 WAL 中额外申请的序列次数,这一部分我们放在序列申请机制剖析中详细介绍。
  • is_called 标记 Sequence 的 last_value 是否已经被申请过,例如 setval 可以设置 is_called 字段:
-- setval false
postgres=# select setval('seq', 10, false);
 setval
--------
     10
(1 row)

postgres=# select * from seq;
 last_value | log_cnt | is_called
------------+---------+-----------
         10 |       0 | f
(1 row)

postgres=# select nextval('seq');
 nextval
---------
      10
(1 row)

-- setval true
postgres=# select setval('seq', 10, true);
 setval
--------
     10
(1 row)

postgres=# select * from seq;
 last_value | log_cnt | is_called
------------+---------+-----------
         10 |       0 | t
(1 row)

postgres=# select nextval('seq');
 nextval
---------
      11
(1 row)

每当用户创建一个 Sequence 对象时,PostgreSQL 总是会创建出一张上面这种结构的 heap 表,来记录 Sequence 对象的数据信息。当 Sequence 对象因为 nextvalsetval 导致序列值变化时,PostgreSQL 就会通过原地更新的方式更新 heap 表中的这一行的三个字段。

setval 为例,下面的逻辑解释了其具体的原地更新过程。

static void
do_setval(Oid relid, int64 next, bool iscalled)
{

    /* 打开并对Sequence heap表进行加锁 */
    init_sequence(relid, &elm, &seqrel);

    ...

    /* 对buffer进行加锁,同时提取tuple */
    seq = read_seq_tuple(seqrel, &buf, &seqdatatuple);

    ...

    /* 原地更新tuple */
    seq->last_value = next;		/* last fetched number */
    seq->is_called = iscalled;
    seq->log_cnt = 0;

    ...

    /* 释放buffer锁以及表锁 */
    UnlockReleaseBuffer(buf);
    relation_close(seqrel, NoLock);
}

可见,do_setval 会直接去设置 Sequence heap 表中的这一行元组,而非普通 heap 表中的删除 + 插入的方式来完成元组更新,对于 nextval 而言,也是类似的过程,只不过 last_value 的值需要计算得出,而非用户设置。

序列申请机制剖析

讲清楚 Sequence 对象在内核中的存在形式之后,就需要讲清楚一个序列值是如何发出的,即 nextval 方法。其在内核的具体实现在 sequence.c 中的 nextval_internal 函数,其最核心的功能,就是计算 last_value 以及 log_cnt

last_valuelog_cnt 的具体关系如下图:

页面值与wal关系

其中 log_cnt 是一个预留的申请次数。默认值为 32,由下面的宏定义决定:

/*
 * We don't want to log each fetching of a value from a sequence,
 * so we pre-log a few fetches in advance. In the event of
 * crash we can lose (skip over) as many values as we pre-logged.
 */
#define SEQ_LOG_VALS	32

每当将 last_value 增加一个 increment 的长度时,log_cnt 就会递减 1。

页面值递增

log_cnt 为 0,或者发生 checkpoint 以后,就会触发一次 WAL 日志写入,按下面的公式设置 WAL 日志中的页面值,并重新将 log_cnt 设置为 SEQ_LOG_VALS

wal_value=last_value+incrementSEQ_LOG_VALS wal\_value = last\_value+increment*SEQ\_LOG\_VALS

通过这种方式,PostgreSQL 每次通过 nextval 修改页面中的 last_value 后,不需要每次都写入 WAL 日志。这意味着:如果 nextval 每次都需要修改页面值的话,这种优化将会使得写 WAL 的频率降低 32 倍。其代价就是,在发生 crash 前如果没有及时进行 checkpoint,那么会丢失一段序列。如下面所示:

postgres=# create sequence seq;
CREATE SEQUENCE
postgres=# select nextval('seq');
 nextval
---------
       1
(1 row)

postgres=# select * from seq;
 last_value | log_cnt | is_called
------------+---------+-----------
          1 |      32 | t
(1 row)

-- crash and restart

postgres=# select * from seq;
 last_value | log_cnt | is_called
------------+---------+-----------
         33 |       0 | t
(1 row)

postgres=# select nextval('seq');
 nextval
---------
      34
(1 row)

显然,crash 以后,Sequence 对象产生了 2-33 这段空洞,但这个代价是可以被接受的,因为 Sequence 并没有违背唯一性原则。同时,在特定场景下极大地降低了写 WAL 的频率。

Sequence 缓存机制

通过上述描述,不难发现 Sequence 每次发生序列申请,都需要通过加入 buffer 锁的方式来修改页面,这意味着 Sequence 的并发性能是比较差的。

针对这个问题,PostgreSQL 使用对 Sequence 使用了 Session Cache 来提前缓存一段序列,来提高并发性能。如下图所示:

Session Cache

Sequence Session Cache 的实现是一个 entry 数量固定为 16 的哈希表,以 Sequence 的 OID 为 key 去检索已经缓存好的 Sequence 序列,其缓存的 value 结构如下:

typedef struct SeqTableData
{
    Oid			relid;			/* Sequence OID(hash key) */
    int64		last;			/* value last returned by nextval */
    int64		cached;			/* last value already cached for nextval */
    int64		increment;		/* copy of sequence's increment field */
} SeqTableData;

其中 last 即为 Sequence 在 Session 中的当前值,即 current_value,cached 为 Sequence 在 Session 中的缓存值,即 cached_value,increment 记录了步长,有了这三个值即可满足 Sequence 缓存的基本条件。

对于 Sequence Session Cache 与页面值之间的关系,如下图所示:

cache与页面关系

类似于 log_cntcache_cnt 即为用户在定义 Sequence 时,设置的 Cache 大小,最小为 1。只有当 cache domain 中的序列用完以后,才会去对 buffer 加锁,修改页中的 Sequence 页面值。调整过程如下所示:

cache申请

例如,如果 CACHE 设置的值为 20,那么当 cache 使用完以后,就会尝试对 buffer 加锁来调整页面值,并重新申请 20 个 increment 至 cache 中。对于上图而言,有如下关系:

cached_value=NEW current_value cached\_value = NEW\ current\_value

NEW current_value+20×INC=NEW cached_value NEW\ current\_value+20\times INC=NEW\ cached\_value

NEW last_value=NEW cached_value NEW\ last\_value = NEW\ cached\_value

在 Sequence Session Cache 的加持下,nextval 方法的并发性能得到了极大的提升,以下是通过 pgbench 进行压测的结果对比。

性能对比

总结

Sequence 在 PostgreSQL 中是一类特殊的表级对象,提供了简单而又丰富的 SQL 接口,使得用户可以更加方便的创建、使用定制化的序列对象。不仅如此,Sequence 在内核中也具有丰富的组合使用场景,其使用场景也得到了极大地扩展。

本文详细介绍了 Sequence 对象在 PostgreSQL 内核中的具体设计,从对象的元数据描述、对象的数据描述出发,介绍了 Sequence 对象的组成。本文随后介绍了 Sequence 最为核心的 SQL 接口——nextval,从 nextval 的序列值计算、原地更新、降低 WAL 日志写入三个方面进行了详细阐述。最后,本文介绍了 Sequence Session Cache 的相关原理,描述了引入 Cache 以后,序列值在 Cache 中,以及页面中的计算方法以及对齐关系,并对比了引入 Cache 前后,nextval 方法在单序列和多序列并发场景下的对比情况。