PostgreSQL 无缝序列

PostgreSQL gapless sequences

提问人:fatfrog 提问时间:4/3/2012 最后编辑:fatfrog 更新时间:2/18/2018 访问量:15255

问:

我正在从 MySql 迁移到 Postgres,我注意到当您从 MySql 中删除行时,当您创建新行时会重新使用这些行的唯一 ID。使用 Postgres 时,如果创建行并删除它们,则不会再次使用唯一 ID。

Postgres 中的这种行为有原因吗?在这种情况下,我可以让它更像 MySql 吗?

Ruby-on-rails PostgreSQL 序列

评论

5赞 ceejayoz 4/3/2012
MySQL 不应重复使用自动增量 ID,除非您删除最高的 ID。
0赞 fatfrog 4/3/2012
啊!谢谢,没错。好吧 - 我可以忍受这种:)
9赞 4/3/2012
无论如何,您都不应该关心 ID。它们只是毫无意义的数字。
0赞 fatfrog 4/3/2012
是的,我知道 - 但我承认我对开关有点偏执!
0赞 IMSoP 11/20/2013
另请参阅 stackoverflow.com/questions/19004453/...

答:

73赞 Craig Ringer 4/3/2012 #1

序列具有间隙,允许并发插入。试图避免间隙或重复使用已删除的 ID 会产生可怕的性能问题。请参阅 PostgreSQL wiki 常见问题解答

PostgreSQL SEQUENCE用于分配 ID。这些只会增加,并且它们不受通常的交易回滚规则的约束,以允许多个交易同时获取新 ID。这意味着,如果交易回滚,这些 ID 将被“丢弃”;没有保留“免费”ID 列表,只有当前的 ID 计数器。如果数据库以不干净的方式关闭,序列通常也会递增。

无论如何,合成密钥 (ID) 都毫无意义。它们的顺序并不重要,它们唯一重要的属性是唯一性。你无法有意义地衡量两个 ID 的“距离”,也无法有意义地说出一个 ID 是大于还是小于另一个。你所能做的就是说“相等”或“不相等”。其他任何事情都是不安全的。你不应该关心差距。

如果你需要一个重复使用已删除 ID 的无间隙序列,你可以有一个,你只需要为它放弃大量的性能 - 特别是,你根本不能在 s 上有任何并发性,因为你必须扫描表以查找最低的可用 ID,锁定表进行写入,这样其他事务就不能声明相同的 ID。 尝试搜索“postgresql gapless sequence”。INSERT

最简单的方法是使用计数器表和获取下一个 ID 的函数。这是一个通用版本,它使用计数器表来生成连续的无间隙 ID;不过,它不会重用 ID。

CREATE TABLE thetable_id_counter ( last_id integer not null );
INSERT INTO thetable_id_counter VALUES (0);

CREATE OR REPLACE FUNCTION get_next_id(countertable regclass, countercolumn text) RETURNS integer AS $$
DECLARE
    next_value integer;
BEGIN
    EXECUTE format('UPDATE %s SET %I = %I + 1 RETURNING %I', countertable, countercolumn, countercolumn, countercolumn) INTO next_value;
    RETURN next_value;
END;
$$ LANGUAGE plpgsql;

COMMENT ON get_next_id(countername regclass) IS 'Increment and return value from integer column $2 in table $1';

用法:

INSERT INTO dummy(id, blah) 
VALUES ( get_next_id('thetable_id_counter','last_id'), 42 );

请注意,当一个打开的事务获得 ID 时,所有其他尝试调用的事务都将被阻止,直到第一个事务提交或回滚。这是不可避免的,对于无缝 ID,并且是设计使然。get_next_id

如果要在一个表中存储多个用于不同目的的计数器,只需在上述函数中添加一个参数,在计数器表中添加一列,并在 中添加一个与该参数匹配的子句即可添加添加到添加的列中。这样,您可以拥有多个独立锁定的计数器行。不要只为新计数器添加额外的列。WHEREUPDATE

此功能不会重复使用已删除的 ID,它只是避免引入间隙。

要重复使用 ID,我建议......不重复使用 ID。

如果确实必须,可以通过在感兴趣的表上添加一个触发器来执行此操作,该触发器将已删除的 ID 添加到自由列表侧表中,并在编辑后将其从自由列表表中删除。将 视为 后跟 .现在修改上面的 ID 生成函数,以便它执行该行,如果找到,则执行该行。 像往常一样从生成器表中获取新 ID。下面是支持重用的先前函数的未经测试的扩展:ON INSERT OR UPDATE OR DELETEINSERTUPDATEDELETEINSERTSELECT free_id INTO next_value FROM free_ids FOR UPDATE LIMIT 1DELETEIF NOT FOUND

CREATE OR REPLACE FUNCTION get_next_id_reuse(countertable regclass, countercolumn text, freelisttable regclass, freelistcolumn text) RETURNS integer AS $$
DECLARE
    next_value integer;
BEGIN
    EXECUTE format('SELECT %I FROM %s FOR UPDATE LIMIT 1', freelistcolumn, freelisttable) INTO next_value;
    IF next_value IS NOT NULL THEN
        EXECUTE format('DELETE FROM %s WHERE %I = %L', freelisttable, freelistcolumn, next_value);
    ELSE
        EXECUTE format('UPDATE %s SET %I = %I + 1 RETURNING %I', countertable, countercolumn, countercolumn, countercolumn) INTO next_value;
    END IF;
    RETURN next_value;
END;
$$ LANGUAGE plpgsql;

评论

0赞 zerkms 12/3/2012
“特别是,你不能有任何并发” ---执行插入,请检查它是否成功执行。没有理由锁定整个表 (omg)
1赞 Craig Ringer 12/3/2012
@zerkms 您是否建议您使用类似的东西并重试重复的键错误?如果是这样,当然可以这样做,但它的性能不会比使用表级或行级锁定生成密钥的方法更好,通常由于重复工作而更糟。从根本上说,它不能比基于锁定的方法表现得更好,因为它在任何给定时间仍然只能有一个事务成功写入。INSERT INTO some_table (id, ...) VALUES ( (SELECT max(id)+1 FROM some_table), ...)
0赞 zerkms 12/3/2012
不,我的意思是填补空白。这就是你在说的,对吧?id 生成器函数应该是非阻塞的,以及插入过程,但具有唯一的约束冲突处理。
0赞 zerkms 12/3/2012
“从根本上说,它不能比基于锁定的方法表现得更好--- ?,因为它在任何给定时间仍然只能有一个交易成功写入。锁定写入表意味着您将无法执行甚至更新。我无法想象比这更糟糕的事情。
1赞 Craig Ringer 12/3/2012
@zerkms 你误会了。如果使用锁定,则锁定在用于生成 ID 的边桌上,因此只能通过插入来争夺。