当计算机程序试图存储一个整数,但存储的值超过了用于存储该整数的数据类型所能表示的最大值时,就会发生整数溢出。
在Postgres中,有三种整数类型:
smallint | 2字节整数 | -32768-32767 |
integer | 4字节整数 | -2147483648-2147483647 |
bigint | 8字节整数 | -9223372036854775808-923372036847775807 |
在定义新表时,使用4字节整数作为主键并不罕见。如果要表示的值超过4个字节,这可能会导致问题。如果达到了序列的限制,可能会在日志中看到如下错误:
ERROR: nextval: reached maximum value of sequence "id" (2147483647)
不要慌!!有如下解决方案:
- 可以使用查询来检查序列号是否用完。
- 短期解决方案是更改为负数开始。
- 长期方案是更改为bigint。
- 在一个可能包含大量数据的新数据库时,当使用SERIAL请改用BIGSERIAL。
判断是否接近溢出的判断
以下查询将标识所有自动递增的列、其所属的序列数据类型的对象,返回列和序列数据类型对象名称,以及最后的值,序列值超过序列或列数据类型之前的百分比:
SELECT
seqs.relname AS sequence,
format_type(s.seqtypid, NULL) sequence_datatype,
CONCAT(tbls.relname, '.', attrs.attname) AS owned_by,
format_type(attrs.atttypid, atttypmod) AS column_datatype,
pg_sequence_last_value(seqs.oid::regclass) AS last_sequence_value,
TO_CHAR((
CASE WHEN format_type(s.seqtypid, NULL) = 'smallint' THEN
(pg_sequence_last_value(seqs.relname::regclass) / 32767::float)
WHEN format_type(s.seqtypid, NULL) = 'integer' THEN
(pg_sequence_last_value(seqs.relname::regclass) / 2147483647::float)
WHEN format_type(s.seqtypid, NULL) = 'bigint' THEN
(pg_sequence_last_value(seqs.relname::regclass) / 9223372036854775807::float)
END) * 100, 'fm9999999999999999999990D00%') AS sequence_percent,
TO_CHAR((
CASE WHEN format_type(attrs.atttypid, NULL) = 'smallint' THEN
(pg_sequence_last_value(seqs.relname::regclass) / 32767::float)
WHEN format_type(attrs.atttypid, NULL) = 'integer' THEN
(pg_sequence_last_value(seqs.relname::regclass) / 2147483647::float)
WHEN format_type(attrs.atttypid, NULL) = 'bigint' THEN
(pg_sequence_last_value(seqs.relname::regclass) / 9223372036854775807::float)
END) * 100, 'fm9999999999999999999990D00%') AS column_percent
FROM
pg_depend d
JOIN pg_class AS seqs ON seqs.relkind = 'S'
AND seqs.oid = d.objid
JOIN pg_class AS tbls ON tbls.relkind = 'r'
AND tbls.oid = d.refobjid
JOIN pg_attribute AS attrs ON attrs.attrelid = d.refobjid
AND attrs.attnum = d.refobjsubid
JOIN pg_sequence s ON s.seqrelid = seqs.oid
WHERE
d.deptype = 'a'
AND d.classid = 1259;
为了展示效果,新建一个整数主键测试表,其中的序列被人为地提升到了20亿:
postgres=# create table test(id serial primary key, value integer);
CREATE TABLE
postgres=# select setval('id', 2000000000);
setval
------------
2000000000
(1 row)
postgres=# \d test
Table "public.test"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+----------------------------------
id | integer | | not null | nextval('id'::regclass)
value | integer | | |
Indexes:
"test_pkey" PRIMARY KEY, btree (id)
现在,当运行上面的查询以查找整数溢出百分比时,我可以看到列和序列的数据类型都是整数,并且由于序列的下一个值是20亿,因此可接受范围的93%:
sequence | sequence_datatype | owned_by | column_datatype | last_sequence_value | sequence_percent | column_percent
-------------+-------------------+----------+-----------------+---------------------+------------------+----------------
test_id_seq | integer | test.id | integer | 2000000001 | 93.13% | 93.13%
(1 row)
更改为负数排序
由于Postgres中的整数类型包含负数,因此处理整数溢出的一个简单方法是使用负数进行排序。这可以通过给序列一个新的开始值-1,并给它一个负的增量值来转换为递减序列来实现:
alter sequence id no minvalue start with-1 increment-1 restart;
如果生成序列的目的纯粹是创建唯一性,负值是完全可以接受的,但在某些应用程序框架或其他用例中,负数可能是不可取的,或者根本不起作用。在这些情况下,我们可以完全更改字段类型。
请记住,任何引用此ID的字段都需要更改数据类型,否则它们也将超出范围。此外,在更新ID字段的类型后,还需要删除并重新应用外键约束。
改为负数方法的优点:
- 列结构无变化
- 非常快:只需更改序列开始编号
缺点:
- 负数可能不适用于应用程序框架
- 未彻底解决问题,可能很快就会再次发生范围溢出
但总的来说,这是一种短期解决方案,可以短期解决问题。
更改类型为bigint
更完整的解决方法是将序列耗尽改为bigint数据类型。
为了更改上述测试表的字段类型,我们将首先创建一个bigint类型的新ID,该ID将最终替换当前ID,并对其创建一个唯一的约束:
alter table test add column id_new bigint;
CREATE UNIQUE INDEX CONCURRENTLY test_id_new ON test (id_new);
新列还需要一个bigint类型的新序列。序列需要在记录的最新值之后的某个点开始。
CREATE SEQUENCE test_id_new_seq START 2147483776 AS bigint;
ALTER TABLE test ALTER COLUMN id_new SET DEFAULT nextval ('test_id_new_seq');
alter sequence test_id_new_seq owned by test.id_new;
现在,可以将新值添加到表中,但有两个不同的序列正在递增-旧的和新的,即:
postgres=# select * from test;
id | value | id_new
------------+-------+------------
2000000007 | |
2000000008 | |
2000000009 | |
2000000010 | |
2000000011 | | 2147483776
2000000012 | | 2147483777
2000000013 | | 2147483778
2000000014 | | 2147483779
在单个事务中,我们将删除旧的ID约束和默认值,重命名列,并在新的ID列上添加无效的“非空”约束:
BEGIN;
ALTER TABLE test DROP CONSTRAINT test_pkey;
ALTER TABLE test ALTER COLUMN id DROP DEFAULT;
ALTER TABLE test RENAME COLUMN id TO id_old;
ALTER TABLE test RENAME COLUMN id_new TO id;
ALTER TABLE test ALTER COLUMN id_old DROP NOT NULL;
ALTER TABLE test ADD CONSTRAINT id_not_null CHECK (id IS NOT NULL) NOT VALID;
COMMIT;
现在,新的ID被添加到表中。由于id上的NOT NULL约束,无法添加新的NULL值,但由于它也是NOT VALID,因此允许使用现有的NULL值。为了使id返回主键,必须回填id_old数据,以便使约束有效。即:
WITH unset_values AS (
SELECT
id_old
FROM
test
WHERE
id IS NULL
LIMIT 1000)
UPDATE
test
SET
id = unset_values.id_old
FROM
unset_values
WHERE
unset_values.id_old = test.id_old;
回填所有行后,可以验证NOT NULL约束,可以将id上的UNIQUE索引转换为主键,最后可以删除独立的NOT NULL约束:
ALTER TABLE test VALIDATE CONSTRAINT id_not_null;
ALTER TABLE test ADD CONSTRAINT test_pkey PRIMARY KEY USING INDEX test_id_new;
ALTER TABLE test DROP CONSTRAINT id_not_null;
现在,可以随时删除4字节的id_old列,因为bigint已经取代了它:
postgres=# ALTER table test drop column id_old;
ALTER TABLE
postgres=# \d test
Table "public.test"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+--------------------------------------
value | integer | | |
id | bigint | | not null | nextval('test_id_new_seq'::regclass)
Indexes:
"test_pkey" PRIMARY KEY, btree (id)
更换数据类型为bigint的优点:
- 这是一个长期的解决方案,很长一段时间内不必担心序列号会用完。
缺点:
- 可能需要将许多其他内容更新为更大的整数
- 需要与整个数据库协调。很可能是个大工程
SERIAL类型
在Postgres中,SERIAL数据类型(smallserial、SERIAL和bigserial)是用于创建自动递增标识符列的快捷方式,这些列的值被分配给Postgres 序列对象的下一个值。
创建SERIAL类型的列将默认为integer类型,同时创建一个由指定表列拥有的整数序列对象,并将其nextval()设置为该列的默认值。
对于新表,请考虑使用BIGSERIAL。