PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Calcola la prossima chiave primaria - di formato specifico

Questa sembra una variante del problema della sequenza gapless; visto anche qui.

Le sequenze gapless presentano seri problemi di prestazioni e simultaneità.

Pensa molto a cosa accadrà quando si verificano più inserti contemporaneamente. Devi essere pronto a riprovare gli inserimenti non riusciti, o LOCK TABLE myTable IN EXCLUSIVE MODE prima del INSERT quindi solo un INSERT può essere in volo alla volta.

Utilizzare una tabella di sequenza con blocco delle righe

Quello che farei in questa situazione è:

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;
$$;

quindi per ottenere un ID:

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

Questo approccio significa che solo una transazione può inserire una riga con una determinata coppia (livello, modalità) alla volta, ma penso che sia esente da gare.

Attenzione ai deadlock

C'è ancora un problema in cui due transazioni simultanee possono bloccarsi se tentano di inserire righe in un ordine diverso. Non c'è una soluzione facile per questo; devi ordinare i tuoi inserti in modo da inserire sempre il livello basso e la modalità prima del massimo, eseguire un inserimento per transazione o vivere con deadlock e riprovare. Personalmente farei quest'ultimo.

Esempio del problema, con due sessioni psql. La configurazione è:

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

poi in due sessioni:

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

Noterai che il secondo inserto nella sessione 2 si bloccherà senza tornare, perché è in attesa di un blocco tenuto dalla sessione 1. Quando la sessione 1 continua a cercare di ottenere un blocco trattenuto dalla sessione 2 nel suo secondo inserto, anch'esso si bloccherà appendere. Non è possibile fare progressi, quindi dopo un secondo o due PostgreSQL rileverà il deadlock e interromperà una delle transazioni, consentendo all'altra di procedere:

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

Il tuo codice deve essere preparato per gestirlo e riprovare l'intera transazione , oppure deve evitare il deadlock utilizzando transazioni a inserto singolo o un ordine accurato.

Creazione automatica di coppie (livello,codice) inesistenti

A proposito, se vuoi (livello, codice) combinazioni che non esistono già nel sequence_numbers tabella da creare al primo utilizzo, è sorprendentemente complicato da correggere in quanto è una variante del problema upsert. Modificherei personalmente get_next_seqno per assomigliare a questo:

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;

$$;

Questo codice può non riuscire, quindi devi sempre essere pronto a riprovare le transazioni. Come spiega l'articolo di depesz, sono possibili approcci più solidi ma di solito non ne vale la pena. Come scritto sopra, se due transazioni tentano contemporaneamente di aggiungere la stessa nuova coppia (livello, codice), una fallirà con:

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