2

我有一个表,其中包含一个 ID 列表,以及 IDName 等各种其他列。

表的主键是 ID 本身,但它不是 auto_increment。所以,我希望能够生成/计算下一个主键,但是有一个转折:

主键应采用特定格式,即 8 位 ID 由三部分组成:
<the level><a code><a sequence #>,例如<2><777><0123> = 27770123

因此,当我为表创建新 ID 时,我想要特定级别和代码的下一个序列号。例如,按照上面的示例,我可能想知道代码 777 的级别 2 的下一个序列号,结果应该是 ID 27770124(0124 是序列中的下一个)。

任何帮助将非常感激。

4

3 回答 3

6

这看起来像是无间隙序列问题的变体;也看到这里

无缝序列存在严重的性能和并发问题。

仔细想想当多个插入同时发生时会发生什么。您必须准备好重试失败的插入,或者LOCK TABLE myTable IN EXCLUSIVE MODE在此之前INSERT一次INSERT只能进行一次。

使用带有行锁定的序列表

在这种情况下我会做的是:

CREATE TABLE sequence_numbers(
    level integer,
    code integer,
    next_value integer DEFAULT 0 NOT NULL,
    PRIMARY KEY (level,code),
    CONSTRAINT level_must_be_one_digit CHECK (level BETWEEN 0 AND 9),
    CONSTRAINT code_must_be_three_digits CHECK (code BETWEEN 0 AND 999),
    CONSTRAINT value_must_be_four_digits CHECK (next_value BETWEEN 0 AND 9999)
);

INSERT INTO sequence_numbers(level,code) VALUES (2,777);

CREATE OR REPLACE FUNCTION get_next_seqno(level integer, code integer)
RETURNS integer LANGUAGE 'SQL' AS $$
    UPDATE sequence_numbers 
    SET next_value = next_value + 1
    WHERE level = $1 AND code = $2
    RETURNING (to_char(level,'FM9')||to_char(code,'FM000')||to_char(next_value,'FM0000'))::integer;
$$;

然后获取ID:

INSERT INTO myTable (sequence_number, blah)
VALUES (get_next_seqno(2,777), blah);

这种方法意味着一次只有一个事务可以插入具有任何给定(级别,模式)对的行,但我认为它是无竞争的。

谨防死锁

如果两个并发事务尝试以不同的顺序插入行,仍然存在一个问题。对此没有简单的解决方法。您必须对插入进行排序,以便始终在高级别之前插入低级别和模式,每个事务执行一次插入,或者忍受死锁并重试。我个人会做后者。

问题示例,有两个 psql 会话。设置是:

CREATE TABLE myTable(seq_no integer primary key);
INSERT INTO sequence_numbers VALUES (1,666)

然后在两个会话中:

SESSION 1                       SESSION 2

BEGIN;
                                BEGIN;

INSERT INTO myTable(seq_no)
VALUES(get_next_seqno(2,777));
                                INSERT INTO myTable(seq_no)
                                VALUES(get_next_seqno(1,666));

                                INSERT INTO myTable(seq_no)
                                VALUES(get_next_seqno(2,777));

INSERT INTO myTable(seq_no)
VALUES(get_next_seqno(1,666));

您会注意到会话 2 中的第二个插入将挂起而不返回,因为它正在等待会话 1 持有的锁。当会话 1 继续尝试在其第二次插入中获取会话 2 持有的锁时,它也会悬挂。无法取得任何进展,因此一两秒后 PostgreSQL 将检测到死锁并中止其中一个事务,允许另一个事务继续:

ERROR:  deadlock detected
DETAIL:  Process 16723 waits for ShareLock on transaction 40450; blocked by process 18632.
Process 18632 waits for ShareLock on transaction 40449; blocked by process 16723.
HINT:  See server log for query details.
CONTEXT:  SQL function "get_next_seqno" statement 1

您的代码必须准备好处理此问题并重试整个事务,或者必须使用单插入事务或仔细排序来避免死锁。

自动创建不存在的(级别、代码)对

sequence_numbers顺便说一句,如果您希望在第一次使用时创建表中尚不存在的(级别,代码)组合,那么正确处理起来非常复杂,因为它是upsert 问题的变体。我个人会修改get_next_seqno为如下所示:

CREATE OR REPLACE FUNCTION get_next_seqno(level integer, code integer)
RETURNS integer LANGUAGE 'SQL' AS $$

    -- add a (level,code) pair if it isn't present.
    -- Racey, can fail, so you have to be prepared to retry
    INSERT INTO sequence_numbers (level,code)
    SELECT $1, $2
    WHERE NOT EXISTS (SELECT 1 FROM sequence_numbers WHERE level = $1 AND code = $2);

    UPDATE sequence_numbers 
    SET next_value = next_value + 1
    WHERE level = $1 AND code = $2
    RETURNING (to_char(level,'FM9')||to_char(code,'FM000')||to_char(next_value,'FM0000'))::integer;

$$;

此代码可能会失败,因此您必须始终准备好重试事务。正如那篇 depesz 文章所解释的,更强大的方法是可能的,但通常不值得。如上所述,如果两个事务同时尝试添加相同的新 (level,code) 对,其中一个将失败:

ERROR:  duplicate key value violates unique constraint "sequence_numbers_pkey"
DETAIL:  Key (level, code)=(0, 555) already exists.
CONTEXT:  SQL function "get_next_seqno" statement 1
于 2012-09-17T09:39:17.277 回答
0

好的,到目前为止我已经想出了,我认为这将满足我的要求(除非有任何关于更优化方法的建议?)

SELECT ID 
FROM myTable 
WHERE ID > 27770000 
  AND ID < 27780000 
ORDER BY ID DESC 
LIMIT 1
于 2012-09-17T09:20:45.010 回答
0

除非您的应用程序的需求非常高,否则冲突的数量将非常低。因此,如果引发关键错误,我会简单地重试。

select coalesce(max(id), 27770000) + 1
from myTable
where id / 10000 = 2777

如果该级别/代码尚不存在,则合并存在。

于 2012-09-17T11:13:46.930 回答