【问题标题】:PostgreSQL gapless sequencesPostgreSQL 无间隙序列
【发布时间】:2012-04-16 13:43:27
【问题描述】:

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

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

【问题讨论】:

  • MySQL 不应重复使用自动增量 ID,除非您删除最高 ID。
  • 啊!谢谢,没错。好的-我可以忍受:)
  • 无论如何,您都不应该关心 ID。它们只是毫无意义的数字。
  • 是的,我知道 - 但不可否认,我对切换有点偏执!

标签: ruby-on-rails postgresql sequence


【解决方案1】:

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

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

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

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

最简单的方法是使用计数器表和获取下一个 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 时,所有其他尝试调用 get_next_id 的事务将阻塞,直到第一个事务提交或回滚。这是不可避免的,对于无缝 ID 而言,这是设计使然。

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

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

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

如果您确实必须这样做,您可以通过在感兴趣的表上添加一个ON INSERT OR UPDATE OR DELETE 触发器来实现,该触发器将已删除的 ID 添加到空闲列表边表中,并在它们为 @ 时将它们从空闲列表中删除987654331@ed。将UPDATE 视为DELETE,后跟INSERT。现在修改上面的 ID 生成函数,使其执行SELECT free_id INTO next_value FROM free_ids FOR UPDATE LIMIT 1,如果找到,DELETEs 该行。 IF NOT FOUND 照常从生成器表中获取新 ID。这是先前功能的未经测试的扩展,以支持重用:

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;

【讨论】:

  • "特别是你不能有任何并发​​" --- 执行插入,检查是否执行成功。没有理由锁定整个表(omg)
  • @zerkms 您是否建议您使用INSERT INTO some_table (id, ...) VALUES ( (SELECT max(id)+1 FROM some_table), ...) 之类的东西并重新尝试重复键错误?如果是这样,当然,您可以这样做,但它的性能不会比使用表级或行级锁定来生成密钥的方法好,通常由于重复工作而更糟。从根本上说,它不能比基于锁定的方法表现更好,因为它仍然只能在任何给定时间成功写入一个事务。
  • 不,我的意思是填补空白。这就是你说的,对吧? id 生成器函数应该是非阻塞的,就像插入过程一样,但是具有唯一的约束违规处理。
  • “从根本上说,它不能比基于锁定的方法执行得更好,因为它仍然只能在任何给定时间成功写入一个事务。” --- ?锁定写入表意味着您将无法执行甚至更新。我无法想象比这更糟糕的事情了。
  • @zerkms 你误会了。如果使用锁定,则锁定在用于 ID 生成的侧表上,因此它仅由插入竞争。
猜你喜欢
  • 2013-10-01
相关资源
最近更新 更多