PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Berechnen Sie den nächsten Primärschlüssel - in einem bestimmten Format

Dies sieht aus wie eine Variante des Gapless-Folge-Problems; auch hier zu sehen.

Lückenlose Sequenzen haben schwerwiegende Leistungs- und Parallelitätsprobleme.

Denken Sie genau darüber nach, was passiert, wenn mehrere Einfügungen gleichzeitig erfolgen. Sie müssen darauf vorbereitet sein, fehlgeschlagene Einfügungen erneut zu versuchen, oder LOCK TABLE myTable IN EXCLUSIVE MODE vor dem INSERT also nur ein INSERT können gleichzeitig im Flug sein.

Verwenden Sie eine Sequenztabelle mit Zeilensperre

Was ich in dieser Situation tun würde, ist:

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

dann um eine ID zu bekommen:

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

Dieser Ansatz bedeutet, dass immer nur eine Transaktion eine Zeile mit einem bestimmten Paar (Ebene, Modus) gleichzeitig einfügen kann, aber ich denke, es ist wettlauffrei.

Vorsicht vor Deadlocks

Es gibt immer noch ein Problem, bei dem zwei gleichzeitige Transaktionen blockiert werden können, wenn sie versuchen, Zeilen in einer anderen Reihenfolge einzufügen. Dafür gibt es keine einfache Lösung; Sie müssen Ihre Einfügungen entweder so anordnen, dass Sie immer Low-Level und Mode vor High einfügen, eine Einfügung pro Transaktion durchführen oder mit Deadlocks leben und es erneut versuchen. Ich persönlich würde letzteres tun.

Beispiel für das Problem mit zwei psql-Sitzungen. Einrichtung ist:

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

dann in zwei Sitzungen:

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

Sie werden feststellen, dass das zweite Insert in Session 2 hängen bleibt, ohne zurückzukehren, weil es auf eine Sperre wartet, die von Session 1 gehalten wird. Wenn Session 1 weiter versucht, eine Sperre zu erhalten, die von Session 2 in ihrem zweiten Insert gehalten wird, wird sie es auch tun aufhängen. Es kann kein Fortschritt erzielt werden, daher erkennt PostgreSQL nach ein oder zwei Sekunden den Deadlock und bricht eine der Transaktionen ab, sodass die andere fortfahren kann:

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

Ihr Code muss entweder darauf vorbereitet sein und die gesamte Transaktion wiederholen , oder es muss den Deadlock mit Single-Insert-Transaktionen oder sorgfältiger Reihenfolge vermeiden.

Automatisches Erstellen nicht vorhandener (Level,Code)-Paare

Übrigens, wenn Sie (Level-, Code-)Kombinationen wollen, die noch nicht in den sequence_numbers vorhanden sind Tabelle, die bei der ersten Verwendung erstellt werden muss, ist überraschend kompliziert, da es sich um eine Variante des Upsert-Problems handelt. Ich persönlich würde get_next_seqno ändern so aussehen:

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;

$$;

Dieser Code kann fehlschlagen, daher müssen Sie immer darauf vorbereitet sein, Transaktionen erneut zu versuchen. Wie dieser depesz-Artikel erklärt, sind robustere Ansätze möglich, aber normalerweise nicht lohnenswert. Wie oben geschrieben, wenn zwei Transaktionen gleichzeitig versuchen, dasselbe neue Paar (Ebene, Code) hinzuzufügen, wird eine fehlschlagen mit:

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