【问题标题】:How to check a sequence efficiently for used and unused values in PostgreSQL如何有效地检查 PostgreSQL 中已使用和未使用值的序列
【发布时间】:2015-12-17 12:18:35
【问题描述】:

在 PostgreSQL (9.3) 中,我有一个表定义为:

CREATE TABLE charts
( recid serial NOT NULL,
  groupid text NOT NULL,
  chart_number integer NOT NULL,
  "timestamp" timestamp without time zone NOT NULL DEFAULT now(),
  modified timestamp without time zone NOT NULL DEFAULT now(),
  donotsee boolean,
  CONSTRAINT pk_charts PRIMARY KEY (recid),
  CONSTRAINT chart_groupid UNIQUE (groupid),
  CONSTRAINT charts_ichart_key UNIQUE (chart_number)
);

CREATE TRIGGER update_modified
  BEFORE UPDATE ON charts
  FOR EACH ROW EXECUTE PROCEDURE update_modified();

我想将 chart_number 替换为如下序列:

CREATE SEQUENCE charts_chartnumber_seq START 16047;

这样通过触发器或函数,添加新的图表记录会自动按升序生成新的图表编号。但是,现有的图表记录不能更改其图表编号,并且多年来在分配的图表编号中存在跳过。因此,在为新图表记录分配新图表编号之前,我需要确保尚未使用“新”图表编号,并且没有为任何具有图表编号的图表记录分配不同的编号。

如何做到这一点?

【问题讨论】:

  • 序列中的间隙无需担心。并且序列永远不会两次生成相同的数字(除非您手动弄乱它)
  • @a_horse_with_no_name 是的...但是随着时间的推移,我想填补序列中的空白,以便使用中的图表编号是连续的。 :) 谢谢。
  • recidserial,但chart_number 不是?序列charts_chartnumber_seq 存在这一事实本身并没有任何意义。 assigning a new chart number to a new chart record ..我们在谈论INSERT吗?
  • @Erwin Brandstetter 是的...我想在表格中插入一个新的图表记录,并让它自动分配一个未使用的图表编号。图表编号应来自序列中的间隙,直到使用完所有“间隙”编号。之后,简单的 nextval 将是正确的。

标签: postgresql sequence gaps-and-islands


【解决方案1】:

在 PostgreSQL 中,一个 SEQUENCE 确保了您提到的两个要求,即:

  1. 无重复
  2. 一旦分配就不会更改

但是由于 SEQUENCE 的工作方式(参见manual),它不能确保不跳过。其中,首先想到的两个原因是:

  1. SEQUENCE 如何使用 INSERTS 处理并发块(您还可以补充说,缓存的概念也使这成为不可能)
  2. 此外,用户触发的 DELETE 是 SEQUENCE 无法自行处理的不可控方面。

在这两种情况下,如果您仍然不想跳过,(并且如果您真的知道自己在做什么)您应该有一个单独的结构来分配 ID(而不是使用 SEQUENCE)。基本上是一个系统,该系统具有存储在 TABLE 中的“可分配”ID 列表,该列表具有以 FIFO 方式弹出 ID 的功能。这应该允许您控制 DELETE 等。

但同样,只有在您真正知道自己在做什么的情况下,才应该尝试这样做!人们不自己做序列是有原因的。存在困难的极端情况(例如并发 INSERT),并且很可能您过度设计了问题案例,这可能可以以更好/更清洁的方式解决。

【讨论】:

    【解决方案2】:

    序列号通常没有意义,何必担心呢?但是,如果您真的想要这个,请按照以下繁琐的程序进行操作。请注意,它不是高效的;唯一有效的选择是忘记孔并使用序列。

    为了避免在每次插入时都扫描charts 表,您应该扫描表一次并将未使用的chart_number 值存储在单独的表中:

    CREATE TABLE charts_unused_chart_number AS
      SELECT seq.unused
      FROM (SELECT max(chart_number) FROM charts) mx,
           generate_series(1, mx(max)) seq(unused)
      LEFT JOIN charts ON charts.chart_number = seq.unused
      WHERE charts.recid IS NULL;
    

    上面的查询生成一个从1到当前最大chart_number值的连续数字序列,然后LEFT JOINs把charts表放到它上面,找到没有对应charts数据的记录,意思是该系列的值未用作chart_number

    接下来,您将创建一个触发器,该触发器在 charts 表上的 INSERT 上触发。在触发器函数中,从上面步骤中创建的表中选择一个值:

    CREATE FUNCTION pick_unused_chart_number() RETURNS trigger AS $$
    BEGIN
      -- Get an unused chart number
      SELECT unused INTO NEW.chart_number FROM charts_unused_chart_number LIMIT 1;
    
      -- If the table is empty, get one from the sequence
      IF NOT FOUND THEN
        NEW.chart_number := next_val(charts_chartnumber_seq);
      END IF;
    
      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    
    CREATE TRIGGER tr_charts_cn
    BEFORE INSERT ON charts
    FOR EACH ROW EXECUTE PROCEDURE pick_unused_chart_number();
    

    简单。但是INSERT 可能会因为某些其他触发器中止过程或任何其他原因而失败。因此,您需要检查以确定确实插入了 chart_number

    CREATE FUNCTION verify_chart_number() RETURNS trigger AS $$
    BEGIN
      -- If you get here, the INSERT was successful, so delete the chart_number
      -- from the temporary table.
      DELETE FROM charts_unused_chart_number WHERE unused = NEW.chart_number;
    END;
    $$ LANGUAGE plpgsql;
    
    CREATE TRIGGER tr_charts_verify
    AFTER INSERT ON charts
    FOR EACH ROW EXECUTE PROCEDURE verify_chart_number();
    

    在某个时刻,未使用图表编号的表格将为空,因此您可以 (1) ALTER TABLE charts 使用序列而不是 integer 用于 chart_number; (2) 删除两个触发器; (3) 未使用图表编号的表格;一次交易。

    【讨论】:

    • 您还需要一个触发器,在删除时将inserts 值放入空闲号码表中。您不需要单独的后插入触发器,可以在前触发器中执行此操作,因为在提交之前它不会对其他 tx 可见。事实上,您需要确保多行插入不会获得重复的 ID。
    • @Patrick 这也是我需要的答案,但我不知道如何检查更多的一个答案。谢谢。
    • 不,您只能接受一个答案。您可以不接受 Erwin 给出的答案,然后接受这个答案,但这似乎有点过头了。
    【解决方案3】:

    考虑这样做。首先阅读这些相关答案:

    如果你仍然坚持填补空白,这里有一个相当有效的解决方案:

    1. 为避免在表格的大部分内容中搜索下一个缺失的chart_number,请创建一个包含所有当前间隙的帮助表一次

    CREATE TABLE chart_gap AS
    SELECT chart_number
    FROM   generate_series(1, (SELECT max(chart_number) - 1  -- max is no gap
                               FROM charts)) chart_number
    LEFT   JOIN charts c USING (chart_number)
    WHERE  c.chart_number IS NULL;
    

    2.charts_chartnumber_seq 设置为当前最大值并将chart_number 转换为实际的serial 列:

    SELECT setval('charts_chartnumber_seq', max(chart_number)) FROM charts;
    
    ALTER TABLE charts
       ALTER COLUMN chart_number SET NOT NULL
     , ALTER COLUMN chart_number SET DEFAULT nextval('charts_chartnumber_seq');
    
    ALTER SEQUENCE charts_chartnumber_seq OWNED BY charts.chart_number; 
    

    详情:

    3. 虽然chart_gap 不为空,但从那里获取下一个chart_number。 要解决并发事务的可能竞争条件,而不使事务等待,请使用建议锁:

    WITH sel AS (
       SELECT chart_number, ...  -- other input values
       FROM   chart_gap
       WHERE  pg_try_advisory_xact_lock(chart_number)
       LIMIT  1
       FOR    UPDATE
       )
    , ins AS (
       INSERT INTO charts (chart_number, ...) -- other target columns
       TABLE sel 
       RETURNING chart_number
       )
    DELETE FROM chart_gap c
    USING  ins i
    WHERE  i.chart_number = c.chart_number;
    

    或者,Postgres 9.5 或更高版本有方便的FOR UPDATE SKIP LOCKED 使这更简单、更快:

    ...
       SELECT chart_number, ...  -- other input values
       FROM   chart_gap
       LIMIT  1
       FOR    UPDATE SKIP LOCKED
    ...
    

    详细解释:

    检查结果。填写完所有行后,将返回 0 行受影响。 (您可以使用IF NOT FOUND THEN ... 签入plpgsql)。然后切换到一个简单的INSERT

       INSERT INTO charts (...)  -- don't list chart_number
       VALUES (...);  --  don't provide chart_number
    

    【讨论】:

      【解决方案4】:

      虽然您想要的都是可能的,但仅使用SEQUENCE 是无法完成的,它需要对表进行排他锁或重试循环才能工作。

      您需要:

      • LOCK thetable IN EXCLUSIVE MODE
      • 通过查询max id 然后在generate_series 上执行left join 来查找第一个免费条目来查找第一个免费 ID。如果有的话。
      • 如果有免费条目,请插入。
      • 如果没有免费进入,调用nextval并返回结果。

      性能将绝对糟糕,交易将被序列化。不会有并发。此外,除非LOCK 是您运行的第一个影响该表的东西,否则您将面临导致事务中止的死锁。

      您可以通过使用AFTER DELETE .. FOR EACH ROW 触发器来减少这种情况,该触发器通过INSERT 将您删除的条目跟踪到一个跟踪备用ID 的单列表中。然后,您可以在列的default 上的 ID 分配函数中 SELECT 表中的最低 ID,避免需要显式表锁定、generate_series 上的 left joinmax 调用。事务仍将在空闲 ID 表上的锁上进行序列化。在 PostgreSQL 中,您甚至可以使用 SELECT ... FOR UPDATE SKIP LOCKED 解决这个问题。所以如果你在 9.5 上,你实际上可以让它变得不可怕,尽管它仍然很慢。

      强烈建议您直接使用SEQUENCE,而不要为重复使用值而烦恼。

      【讨论】:

        猜你喜欢
        • 2020-06-18
        • 2018-09-11
        • 2015-04-13
        • 1970-01-01
        • 1970-01-01
        • 2020-10-22
        • 1970-01-01
        • 2012-03-11
        • 2013-05-03
        相关资源
        最近更新 更多