sql >> Databasteknik >  >> RDS >> PostgreSQL

Beräkna nästa primärnyckel - av specifikt format

Detta ser ut som en variant av problemet med gapless sekvens; ses även här.

Gapless-sekvenser har allvarliga prestanda- och samtidighetsproblem.

Tänk mycket noga på vad som kommer att hända när flera insättningar händer samtidigt. Du måste vara beredd på att försöka igen misslyckade insättningar, eller LOCK TABLE myTable IN EXCLUSIVE MODE före INSERT så bara en INSERT kan flyga åt gången.

Använd en sekvenstabell med radlåsning

Vad jag skulle göra i den här situationen är:

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

sedan för att få ett ID:

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

Detta tillvägagångssätt innebär att endast en transaktion någonsin kan infoga en rad med ett givet (nivå, läge) par åt gången, men jag tror att det är rasfritt.

Se upp för dödlägen

Det finns fortfarande ett problem där två samtidiga transaktioner kan låsa sig om de försöker infoga rader i en annan ordning. Det finns ingen enkel lösning för detta; du måste antingen beställa dina skär så att du alltid sätter in låg nivå och läge innan hög, gör en insättning per transaktion, eller leva med dödläge och försök igen. Personligen skulle jag göra det senare.

Exempel på problemet, med två psql-sessioner. Inställningen är:

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

sedan i två sessioner:

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

Du kommer att märka att den andra infogningen i session 2 kommer att hänga utan att återvända, eftersom den väntar på ett lås som hålls av session 1. När session 1 fortsätter att försöka få ett lås som hålls av session 2 i dess andra insert kommer det också att hänga. Inga framsteg kan göras, så efter en eller två sekunder kommer PostgreSQL att upptäcka dödläget och avbryta en av transaktionerna, vilket tillåter den andra att fortsätta:

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

Din kod måste antingen vara förberedd för att hantera detta och försöka igen hela transaktionen , eller så måste den undvika dödläget genom att använda transaktioner med en enkel infogning eller noggrann beställning.

Skapar automatiskt obefintliga (nivå, kod) par

BTW, om du vill ha (nivå,kod) kombinationer som inte redan finns i sequence_numbers tabell som ska skapas vid första användningen, det är förvånansvärt komplicerat att få rätt eftersom det är en variant av upsert-problemet. Jag skulle personligen ändra get_next_seqno att se ut så här:

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;

$$;

Den här koden kan misslyckas, så du måste alltid vara beredd på att göra om transaktioner. Som den depesz-artikeln förklarar är mer robusta tillvägagångssätt möjliga men vanligtvis inte värt det. Som skrivet ovan, om två transaktioner samtidigt försöker lägga till samma nya (nivå,kod) par, kommer en att misslyckas med:

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


  1. Funktion för att returnera dynamisk uppsättning kolumner för given tabell

  2. Åtgärda dataförlust med logfrakt med försenad återställning

  3. Postgres-fel vid uppdatering av kolumndata

  4. SQLException:Ingen lämplig drivrutin hittades för jdbc:oracle:thin:@//localhost:1521/orcl