如何在 PostgreSQL 中有效地检查序列中已使用和未使用的值

How to check a sequence efficiently for used and unused values in PostgreSQL

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

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;

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

如何做到这一点?

在PostgreSQL中,一个SEQUENCE保证了你说的两个需求,即:

  1. 无重复
  2. 分配后无变化

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

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

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

但同样,只有当您真正知道自己在做什么时,才应该尝试这样做!人们不自己做 SEQUENCE 是有原因的。有一些极端情况(例如并发插入),很可能你过度设计了你的问题案例,这可能可以用更好/更干净的方式解决。

序列号通常没有意义,所以为什么要担心?但是,如果您真的想要这个,请按照以下繁琐的步骤进行操作。请注意,它 不是 高效的;唯一有效的选择是忘记漏洞并使用序列。

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

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 JOINcharts table 添加到它并找到没有相应 charts 数据的记录,这意味着该系列的值未用作 chart_number.

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

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();

在某个时候,带有未使用图表编号的 table 将是空的,此时您可以 (1) ALTER TABLE charts 使用序列而不是 [=14= 的 integer ]; (2) 删除两个触发器; (3) table 未使用的图表编号;全部在一次交易中完成。

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

如果你还是坚持填空,这里有一个比较有效的解决办法:

1. 为了避免在 table 的大部分区域搜索下一个缺失的 chart_number,创建一个助手 table差距 一次:

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; 

详情:

  • How to reset postgres' primary key sequence when it falls out of sync?
  • Safely and cleanly rename tables that use serial primary key columns in Postgres?

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
...

详细解释:

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

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

虽然您想要的是可能的,但仅使用 SEQUENCE 是无法完成的,它需要 table 上的独占锁或重试循环才能工作。

您需要:

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

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

您可以使用 AFTER DELETE .. FOR EACH ROW 触发器来减少这种情况,该触发器跟踪您删除的条目,方法是 INSERT 将它们放入单列 table 以跟踪备用身份证。然后,您可以在 default 列的 ID 分配函数中 SELECT 来自 table 的最低 ID,避免需要显式 table 锁,left joingenerate_seriesmax 电话上。交易仍将在锁定免费 ID table 时序列化。在 PostgreSQL 中,您甚至可以使用 SELECT ... FOR UPDATE SKIP LOCKED 来解决这个问题。因此,如果您使用的是 9.5,您实际上可以让它变得不那么糟糕,尽管它仍然很慢。

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