如何在 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保证了你说的两个需求,即:
- 无重复
- 分配后无变化
但是由于 SEQUENCE 的工作方式(参见 manual),它不能确保不跳过。其中,想到的前两个原因是:
- SEQUENCE 如何使用 INSERTS 处理并发块(您还可以补充一点,Cache 的概念也使这成为不可能)
- 此外,用户触发的 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 JOIN
将 charts
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 未使用的图表编号;全部在一次交易中完成。
考虑不这样做。先阅读这些相关答案:
- Gap-less sequence where multiple transactions with multiple tables are involved
- Compacting a sequence in PostgreSQL
如果你还是坚持填空,这里有一个比较有效的解决办法:
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 join
在 generate_series
和 max
电话上。交易仍将在锁定免费 ID table 时序列化。在 PostgreSQL 中,您甚至可以使用 SELECT ... FOR UPDATE SKIP LOCKED
来解决这个问题。因此,如果您使用的是 9.5,您实际上可以让它变得不那么糟糕,尽管它仍然很慢。
我强烈建议您直接使用SEQUENCE
,不要为重复使用值而烦恼。
在 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保证了你说的两个需求,即:
- 无重复
- 分配后无变化
但是由于 SEQUENCE 的工作方式(参见 manual),它不能确保不跳过。其中,想到的前两个原因是:
- SEQUENCE 如何使用 INSERTS 处理并发块(您还可以补充一点,Cache 的概念也使这成为不可能)
- 此外,用户触发的 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 JOIN
将 charts
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 未使用的图表编号;全部在一次交易中完成。
考虑不这样做。先阅读这些相关答案:
- Gap-less sequence where multiple transactions with multiple tables are involved
- Compacting a sequence in PostgreSQL
如果你还是坚持填空,这里有一个比较有效的解决办法:
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 join
在 generate_series
和 max
电话上。交易仍将在锁定免费 ID table 时序列化。在 PostgreSQL 中,您甚至可以使用 SELECT ... FOR UPDATE SKIP LOCKED
来解决这个问题。因此,如果您使用的是 9.5,您实际上可以让它变得不那么糟糕,尽管它仍然很慢。
我强烈建议您直接使用SEQUENCE
,不要为重复使用值而烦恼。