Cela ressemble à une variante du problème de séquence sans interruption; également vu ici.
Les séquences sans interruption présentent de sérieux problèmes de performances et de simultanéité.
Réfléchissez bien à ce qui se passera lorsque plusieurs insertions se produiront en même temps. Vous devez être prêt à réessayer les insertions ayant échoué, ou LOCK TABLE myTable IN EXCLUSIVE MODE
avant le INSERT
donc un seul INSERT
peut être en vol à la fois.
Utiliser une table de séquence avec verrouillage de ligne
Ce que je ferais dans cette situation est :
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;
$$;
puis pour obtenir un ID :
INSERT INTO myTable (sequence_number, blah)
VALUES (get_next_seqno(2,777), blah);
Cette approche signifie qu'une seule transaction peut insérer une ligne avec une paire donnée (niveau, mode) à la fois, mais je pense que c'est sans course.
Attention aux interblocages
Il y a toujours un problème où deux transactions simultanées peuvent se bloquer si elles essaient d'insérer des lignes dans un ordre différent. Il n'y a pas de solution facile pour cela; vous devez soit commander vos insertions de manière à toujours insérer le niveau bas et le mode avant le niveau haut, faire une insertion par transaction, ou vivre avec des blocages et réessayer. Personnellement, je ferais ce dernier.
Exemple du problème, avec deux sessions psql. La configuration est :
CREATE TABLE myTable(seq_no integer primary key);
INSERT INTO sequence_numbers VALUES (1,666)
puis en deux séances :
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));
Vous remarquerez que la deuxième insertion de la session 2 se bloquera sans retour, car elle attend un verrou détenu par la session 1. Lorsque la session 1 essaie d'obtenir un verrou détenu par la session 2 dans sa deuxième insertion, elle aussi pendre. Aucune progression ne peut être effectuée, donc après une seconde ou deux, PostgreSQL détectera le blocage et abandonnera l'une des transactions, permettant à l'autre de continuer :
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
Votre code doit soit être prêt à gérer cela et réessayer la transaction entière , ou il doit éviter l'impasse en utilisant des transactions à insertion unique ou une commande soignée.
Créer automatiquement des paires (niveau, code) inexistantes
BTW, si vous voulez des combinaisons (niveau, code) qui n'existent pas déjà dans les sequence_numbers
table à créer à la première utilisation, c'est étonnamment compliqué à mettre en place car c'est une variante du problème d'upsert. Je modifierais personnellement get_next_seqno
ressembler à ceci :
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;
$$;
Ce code peut échouer, vous devez donc toujours être prêt à réessayer les transactions. Comme l'explique cet article de Depesz, des approches plus robustes sont possibles mais ne valent généralement pas la peine. Comme écrit ci-dessus, si deux transactions tentent simultanément d'ajouter la même nouvelle paire (niveau, code), l'une échouera avec :
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