You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PostgreSQL复用自动生成ID的可行性及相关技术问题咨询

PostgreSQL复用已删除ID的可行性评估

场景:每日会删除大量记录,希望表的ID标识符不超过9999,现评估复用自动生成ID列中已删除ID的可行性,并提出以下问题:

  1. 向表中插入数据时是否会出现并发问题?(每日约插入100条记录)
  2. 是否存在正确复用ID的方式?
  3. 针对该场景是否应采用其他ID分配方案?
  4. 是否该彻底放弃开发?

实操代码示例

创建表

CREATE TABLE IF NOT EXISTS test(
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
data TEXT);

创建获取可用ID的函数

CREATE OR REPLACE function get_id_test()
returns bigint
language plpgsql
as
$$
DECLARE
min_id bigint;
previous bigint;
next_id bigint;
f record;
BEGIN
   SELECT MIN(id) into min_id FROM test;
   IF  min_id > 1 THEN
       RETURN 1;
   END IF;

   FOR f
   IN select t.id from test t order by t.id
   LOOP
       IF  f.id = min_id THEN
           previous = f.id;
           CONTINUE;
       END IF;
       IF  f.id > (previous + 1) THEN
           RETURN previous + 1;
       ELSE 
           previous = f.id;
       END IF;
   END LOOP;
   next_id = previous + 1;
   RETURN nextval(pg_get_serial_sequence('test', 'id'));
end;
$$;

插入5行测试数据

INSERT INTO test (id,data) VALUES (get_id_test(),'test'),
                                  (get_id_test(),'test'),
                                  (get_id_test(),'test'),
                                  (get_id_test(),'test'),
                                  (get_id_test(),'test');

删除ID为2和4的记录

DELETE FROM test WHERE id=2 OR id=4;

插入行(预期复用ID=2)

INSERT INTO test (id,data) VALUES (get_id_test(),'test expecting id 2');

插入行(预期复用ID=4)

INSERT INTO test (id,data) VALUES (get_id_test(),'test expecting id 4');

插入行(预期使用序列生成的下一个ID=6)

INSERT INTO test (id,data) VALUES (get_id_test(),'test expecting id 6');

问题解答

1. 插入数据时是否会出现并发问题?

会出现并发问题。当前的get_id_test函数没有任何并发控制机制:当多个事务同时执行该函数时,可能会同时检测到同一个空闲ID(比如删除后的ID=2),然后多个事务都返回这个ID,最终插入时因为主键唯一性约束报错。

虽然每日仅插入100条记录,并发量不算高,但只要存在并行插入的情况,就有概率触发主键冲突。比如两个请求同时调用get_id_test,都查到ID=2是空闲的,就会导致其中一个插入失败。

2. 是否存在正确复用ID的方式?

有,但需要解决并发冲突和序列同步的问题,推荐两种方案:

方案一:在函数中添加表级锁

修改get_id_test函数,在查询空闲ID前先锁定整个表,避免其他事务同时修改或查询:

CREATE OR REPLACE function get_id_test()
returns bigint
language plpgsql
as
$$
DECLARE
min_id bigint;
previous bigint;
next_id bigint;
f record;
BEGIN
   -- 锁定表,防止并发修改
   LOCK TABLE test IN EXCLUSIVE MODE;

   SELECT MIN(id) into min_id FROM test;
   IF  min_id > 1 THEN
       RETURN 1;
   END IF;

   FOR f
   IN select t.id from test t order by t.id
   LOOP
       IF  f.id = min_id THEN
           previous = f.id;
           CONTINUE;
       END IF;
       IF  f.id > (previous + 1) THEN
           -- 复用空闲ID后,确保序列值不小于当前最大ID
           PERFORM setval(pg_get_serial_sequence('test', 'id'), GREATEST((SELECT MAX(id) FROM test), currval(pg_get_serial_sequence('test', 'id'))));
           RETURN previous + 1;
       ELSE 
           previous = f.id;
       END IF;
   END LOOP;
   next_id = previous + 1;
   RETURN nextval(pg_get_serial_sequence('test', 'id'));
end;
$$;

表级锁会降低并发插入的性能,但每日100条的量完全可以接受。同时添加了setval来同步序列值,避免后续nextval生成的ID和手动插入的ID冲突。

方案二:维护空闲ID池

创建一个单独的表存储已删除的ID,删除记录时将ID插入该表,插入新记录时优先从这个表取ID:

-- 创建空闲ID表
CREATE TABLE IF NOT EXISTS free_ids (id BIGINT PRIMARY KEY);

-- 修改删除操作,将ID存入空闲表
CREATE OR REPLACE FUNCTION delete_test_record(p_id BIGINT)
RETURNS VOID AS $$
BEGIN
   DELETE FROM test WHERE id = p_id;
   INSERT INTO free_ids(id) VALUES(p_id);
END;
$$ LANGUAGE plpgsql;

-- 获取可用ID的函数
CREATE OR REPLACE FUNCTION get_id_test()
RETURNS BIGINT AS $$
DECLARE
    free_id BIGINT;
BEGIN
    -- 尝试从空闲池取ID(带锁避免并发)
    SELECT id INTO free_id FROM free_ids LIMIT 1 FOR UPDATE SKIP LOCKED;
    IF free_id IS NOT NULL THEN
        DELETE FROM free_ids WHERE id = free_id;
        RETURN free_id;
    END IF;

    -- 没有空闲ID时,检查是否超过9999
    IF currval(pg_get_serial_sequence('test', 'id')) >= 9999 THEN
        -- 若已达上限,尝试找最小的未使用ID
        SELECT MIN(t.id + 1) INTO free_id FROM test t WHERE NOT EXISTS (SELECT 1 FROM test WHERE id = t.id + 1);
        IF free_id IS NULL OR free_id > 9999 THEN
            RAISE EXCEPTION 'ID已达到上限9999,无可用ID';
        END IF;
        RETURN free_id;
    ELSE
        RETURN nextval(pg_get_serial_sequence('test', 'id'));
    END IF;
END;
$$ LANGUAGE plpgsql;

这种方式更高效,锁的粒度更小,适合更高并发的场景。

3. 针对该场景是否应采用其他ID分配方案?

如果核心需求是ID不超过9999且循环复用,可以考虑以下更简单的方案:

方案一:使用带CYCLE的序列

放弃IDENTITY列,手动创建支持循环的序列,当序列达到9999后自动从1开始(需处理可能的主键冲突):

-- 创建循环序列,最大值9999
CREATE SEQUENCE test_id_seq START 1 MINVALUE 1 MAXVALUE 9999 CYCLE;

CREATE TABLE IF NOT EXISTS test(
id BIGINT PRIMARY KEY DEFAULT nextval('test_id_seq'),
data TEXT);

注意:序列循环后可能生成已存在的ID,需要在插入时捕获主键冲突并重试,或者结合空闲ID池使用。

方案二:基于范围的ID复用

如果数据有明确的生命周期,可以按时间段分配ID范围,当旧数据被删除后,重新使用对应范围的ID。这种方式适合批量删除、批量插入的场景。

4. 是否该彻底放弃开发?

完全没必要放弃。这个需求是合理的,无论是通过改进现有复用逻辑,还是换用其他ID分配方案,都能实现“ID不超过9999且复用已删除ID”的目标。只要解决并发冲突和序列同步的问题,就能稳定运行,每日100条的插入量完全在PostgreSQL的处理能力范围内。


内容的提问来源于stack exchange,提问作者albertochigua

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 21:39:21