暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

POSTGRESQL主键自动生成值(UUID、序列或标识列)

飞象数据 2022-02-14
7284

有时客户会问我自动生成主键的最佳选择。在本文中,我将探讨这些选项并给出建议。

为什么自动生成主键?

每个表都需要一个主键。在关系数据库中,能够识别单个表行非常重要。如果你想知道为什么,可以在互联网上搜索到成千上万的从表格中删除重复条目的问题。

建议您选择一个主键,该主键不仅是唯一的,而且在表行的生命周期内不会更改。这是因为外键约束通常引用主键,而更改主键会导致麻烦或不必要的工作。

例如一个表有一个自然主键,一个国家公民的社会保险号。但通常情况下,没有这样的属性,您必须生成一个人工主键。有些人甚至认为,即使有一个自然主键,你也应该使用一个人工主键,但我不会卷入那场“圣战”。

PostgreSQL中自动生成主键的技术

基本有两种方法:

用序列生成键

序列是一个数据库对象,它的唯一用途是生成唯一的数字。它使用一个递增的内部计数器来实现这一点。

序列对并发访问进行了高度优化,它们永远不会两次发出相同的数字。尽管如此,从多个并发SQL语句中访问序列可能会成为瓶颈,因此有一个CACHE
选项可以使序列同时向数据库会话发送多个值。

序列不遵循正常的事务规则:如果事务回滚,序列不会重置其计数器。这是为了良好的性能,并不构成问题。如果你正在寻找一种生成无间隙数字序列的方法,序列不是正确的选择,你将不得不求助于效率更低、更复杂的技术。

要从序列中提取下一个值,可以使用如下nextval
函数:

SELECT nextval('sequence_name');
复制

生成UUID

UUID(通用唯一标识符)是使用有效保证唯一性算法生成的128位数字。有几种标准化的算法。在PostgreSQL中,有许多函数可以生成UUID:

  • uuid ossp
    扩展提供了生成UUID的函数。注意,由于名称中有连字符,因此必须在扩展名上打双引号(CREATE EXTENSION "uuid-ossp";
    )。

  • 从PostgreSQL v13开始,您可以使用核心函数gen_random_uuid()
    生成版本4(随机)uuid。

请注意,对于uuid,您应该始终使用PostgreSQL uuid
数据类型。不要试图将它们转换为字符串或numeric
——这样会浪费空间并降低性能。

定义自动生成的主键

有四种方法可以使用自动生成的值定义列:

使用DEFAULT子句

可以将此方法用于序列和UUID。以下是一些例子:

CREATE TABLE has_integer_pkey (
id bigint DEFAULT nextval('integer_id_seq') PRIMARY KEY,
...
);

CREATE TABLE has_uuid_pkey (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
...
);
复制

每当INSERT
语句没有显式插入该列时,PostgreSQL就会使用DEFAULT
值。

使用serial和bigserial伪类型

此方法是上面的定义序列+设置DEFAULT
子句的快捷方式。使用此方法,您可以定义如下表:

CREATE TABLE uses_serial (
id bigserial PRIMARY KEY,
...
);
复制

这相当于:

CREATE TABLE uses_serial (
id bigint PRIMARY KEY,
...
);

CREATE SEQUENCE uses_serial_id_seq
OWNED BY uses_serial.id;

ALTER TABLE uses_serial ALTER id
SET DEFAULT nextval('uses_serial_id_seq');
复制

OWNED BY
”子句在列和序列之间添加了一个依赖项,因此删除列会自动删除序列。

使用serial
将创建一个integer
列,而bigserial
将创建一个bigint
列。

使用标识列

这是使用序列的另一种方式,因为PostgreSQL使用“后台”序列来实现标识列。

CREATE TABLE uses_identity (
id bigint GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
...
);
复制

还有“GENERATED BY DEFAULT AS IDENTITY
”,区别是如果您尝试显式地为列插入一个值(与DEFAULT
子句非常类似),则不会收到错误消息。

可以为标识列指定序列选项:

CREATE TABLE uses_identity (
id bigint GENERATED ALWAYS AS IDENTITY
(MINVALUE 0 START WITH 0 CACHE 20)
PRIMARY KEY,
...
);
复制

使用BEFORE INSERT触发器

这类似于DEFAULT
值,但它允许您无条件地用生成的值覆盖用户插入的值。触发器的最大缺点是对性能的影响。

我应该使用integer(serial)还是bigint(bigserial)作为自动生成的主键?

你应该总是使用bigint

的确,integer
只需要四个字节,而bigint
需要八个字节。但是:

  • 如果您的表很小,确实integer
    就足够了,既然表很小,那么浪费的四个字节就无关紧要了。但并不是每一张你设计的小表都会一直小下去!

  • 如果您有一个大表,则可能会超过integer
    的最大值,即2147483647。请注意,如果表中包含的行少于这个值,也可能会发生这种情况:您可能会删除行,一些序列值可能会因回滚的事务而“丢失”。

    在活动数据库中将一个大表中的主键列从integer
    更改为bigint
    非常复杂,所以您应该省去这种痛苦。

有了bigint
,你肯定永远不会超过9223372036854775807的最大值:即使你每秒插入10000行不停顿,也要将近3000万年的时间才能达到这个极限。

我应该为自动生成的主键使用bigserial还是标识列?

除非必须支持旧的PostgreSQL版本,否则应该使用标识列。

PostgreSQL v11中引入了标识列,与bigserial
相比,它有两个优点:

  • 它符合SQL标准,而bigserial
    是PostgreSQL的专有语法。这将使您的代码更具可移植性。

  • 如果使用GENERATED ALWAYS AS IDENTITY
    ,如果试图通过显式插入数字来覆盖生成的值,则会收到错误消息。这避免了一个常见问题,即手动输入的值稍后会与生成的值冲突,从而导致意外的应用程序错误。

因此,除非您必须支持PostgreSQL v10或更低版本,否则没有理由使用bigserial

我应该使用bigint还是uuid作为自动生成的主键?

我的建议是使用序列,除非您使用数据库切分或有其他原因以“分散”的方式(在单个数据库之外)生成主键

实际差异

bigint
的优势显而易见:

  • bigint
    只使用8个字节,而uuid
    使用16个字节

  • 从序列中获取值比计算UUID便宜

使用序列的一个缺点是它是单个数据库中的单个对象。因此,如果使用分片,即跨多个数据库分发数据,则不能使用序列。在这种情况下,UUID是一个明显的选择。(您可以使用INCREMENT
大于1和不同的START
值定义的序列,但添加其他分片时可能会出现问题。)

当然,如果主键的值不是由数据库自动生成的,而是在分布在多个应用服务器上的应用程序创建的,那么您也会更喜欢UUID。

假想差异

有人认为UUID更好,因为它们将写操作分布在主键索引的不同页面上。这应该会减少争用,并导致一个更平衡或更少碎片化的索引。第一个是正确的,但这实际上可能是一个缺点,因为它需要缓存整个索引以获得良好的性能。第二个肯定是错误的,因为B树索引总是平衡的。另外,PostgreSQL v11中的一个变化确保单调递增的值比随机插入更有效地填充索引(但随后的删除当然会导致碎片)。简言之,uuid
使用了两倍的存储空间,这将使索引更大,导致更多的写操作,并占用更多的缓存。这一事减小了它的这些优势。

Benchmark:bigint VS uuid

在更大的连接中,uuid
bigint
慢。

我决定用这两个表运行一个仅限插入的benchmark测试:

CREATE UNLOGGED TABLE test_bigint (
id bigint GENERATED ALWAYS AS IDENTITY (CACHE 200) PRIMARY KEY
);

CREATE UNLOGGED TABLE test_uuid (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY
);
复制

我在我的笔记本电脑(SSD,8核)上用一个pgbench
定制脚本执行benchmark测试,该脚本有6个并发客户端,在5分钟内重复运行1000条准备好的INSERT
语句的事务:

INSERT INTO test_bigint /* or test_uuid */ DEFAULT VALUES;
复制

bigint
uuid
每秒插入数10709074947
单行索引增长30.5 bytes41.7 bytes

使用bigint
显然是成功的,但差别并不显著。

结论

序列生成的数字和UUID都可用作自动生成的主键。

除非需要在单个数据库之外生成主键,否则请使用标识列,并确保所有主键列都是bigint
类型。

文章转载自飞象数据,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论