sql >> Base de Datos >  >> RDS >> PostgreSQL

Calcular la siguiente clave principal - de formato específico

Esto parece una variante del problema de la secuencia sin espacios; también visto aquí.

Las secuencias sin espacios tienen serios problemas de rendimiento y simultaneidad.

Piense muy bien en lo que sucederá cuando ocurran múltiples inserciones a la vez. Debe estar preparado para volver a intentar las inserciones fallidas, o LOCK TABLE myTable IN EXCLUSIVE MODE antes del INSERT entonces solo uno INSERT puede estar en vuelo a la vez.

Usar una tabla de secuencias con bloqueo de filas

Lo que yo haría en esta situación es:

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

luego para obtener una identificación:

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

Este enfoque significa que solo una transacción puede insertar una fila con cualquier par dado (nivel, modo) a la vez, pero creo que es libre de carreras.

Cuidado con los interbloqueos

Todavía hay un problema en el que dos transacciones simultáneas pueden bloquearse si intentan insertar filas en un orden diferente. No hay una solución fácil para esto; debe ordenar sus inserciones para que siempre inserte el nivel bajo y el modo antes que el alto, haga una inserción por transacción o viva con interbloqueos y vuelva a intentarlo. Personalmente, haría lo último.

Ejemplo del problema, con dos sesiones de psql. La configuración es:

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

luego en dos sesiones:

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

Notará que la segunda inserción en la sesión 2 se bloqueará sin regresar, porque está esperando un bloqueo retenido por la sesión 1. Cuando la sesión 1 continúa para intentar obtener un bloqueo retenido por la sesión 2 en su segunda inserción, también lo hará. colgar. No se puede hacer ningún progreso, por lo que después de uno o dos segundos, PostgreSQL detectará el interbloqueo y anulará una de las transacciones, permitiendo que la otra continúe:

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

Su código debe estar preparado para manejar esto y volver a intentar la transacción completa , o debe evitar el interbloqueo utilizando transacciones de inserción única o una ordenación cuidadosa.

Creación automática de pares inexistentes (nivel, código)

Por cierto, si desea combinaciones (nivel, código) que aún no existen en sequence_numbers table que se creará en el primer uso, eso es sorprendentemente complicado de hacer bien, ya que es una variante del problema upsert. Yo personalmente modificaría get_next_seqno verse así:

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;

$$;

Este código puede fallar, por lo que siempre debe estar preparado para volver a intentar transacciones. Como explica ese artículo de Depesz, son posibles enfoques más sólidos, pero generalmente no valen la pena. Como se escribió anteriormente, si dos transacciones intentan agregar simultáneamente el mismo par nuevo (nivel, código), una fallará 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