PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Calcular a próxima chave primária - de formato específico


Isso parece uma variante do problema da sequência sem intervalos; também visto aqui.

Sequências sem intervalos têm sérios problemas de desempenho e simultaneidade.

Pense muito sobre o que acontecerá quando várias inserções ocorrerem ao mesmo tempo. Você precisa estar preparado para tentar novamente inserções com falha ou LOCK TABLE myTable IN EXCLUSIVE MODE antes do INSERT então apenas um INSERT pode estar em vôo de cada vez.

Use uma tabela de sequência com bloqueio de linha


O que eu faria nessa situação é:
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;
$$;

então para obter um ID:
INSERT INTO myTable (sequence_number, blah)
VALUES (get_next_seqno(2,777), blah);

Essa abordagem significa que apenas uma transação pode inserir uma linha com qualquer par (nível, modo) de cada vez, mas acho que é livre de corrida.

Cuidado com os impasses


Ainda há um problema em que duas transações simultâneas podem travar se tentarem inserir linhas em uma ordem diferente. Não há solução fácil para isso; você deve ordenar suas inserções para que você sempre insira o nível baixo e o modo antes do alto, faça uma inserção por transação ou viva com impasses e tente novamente. Pessoalmente, eu faria o último.

Exemplo do problema, com duas sessões psql. A configuração é:
CREATE TABLE myTable(seq_no integer primary key);
INSERT INTO sequence_numbers VALUES (1,666)

depois em duas sessões:
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));

Você notará que a segunda inserção na sessão 2 travará sem retornar, porque está aguardando um bloqueio mantido pela sessão 1. Quando a sessão 1 tentar obter um bloqueio mantido pela sessão 2 em sua segunda inserção, ela também aguentar. Nenhum progresso pode ser feito, portanto, após um ou dois segundos, o PostgreSQL detectará o deadlock e abortará uma das transações, permitindo que a outra prossiga:
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

Seu código deve estar preparado para lidar com isso e tentar novamente toda a transação , ou deve evitar o deadlock usando transações de inserção única ou ordenação cuidadosa.

Criando automaticamente pares inexistentes (nível, código)


BTW, se você quiser (nível, código) combinações que ainda não existem no sequence_numbers table a ser criada no primeiro uso, isso é surpreendentemente complicado de acertar, pois é uma variante do problema upsert. Eu modificaria pessoalmente get_next_seqno para ficar assim:
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;

$$;

Esse código pode falhar, portanto, você sempre deve estar preparado para repetir as transações. Como esse artigo do depesz explica, abordagens mais robustas são possíveis, mas geralmente não valem a pena. Conforme escrito acima, se duas transações tentarem simultaneamente adicionar o mesmo novo par (nível, código), uma falhará com:
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