Postgres中的整数溢出

当计算机程序试图存储一个整数,但存储的值超过了用于存储该整数的数据类型所能表示的最大值时,就会发生整数溢出。

在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。

猜你喜欢

转载自blog.csdn.net/hezhou876887962/article/details/129390466